103 lines
4.0 KiB
TypeScript
103 lines
4.0 KiB
TypeScript
import { todayInTz, resolveTz, wallClockToUtc } from '@shared/timezone';
|
|
import { famMeta } from './fam';
|
|
import { pbAdmin } from '$lib/server/pocketbase';
|
|
|
|
function resolveServerTz(tz?: string): string {
|
|
return resolveTz(tz || 'auto');
|
|
}
|
|
|
|
// A `once` goal is finished the moment its reward is approved — complete the
|
|
// config here (not just in evaluateFam, which only runs on chore toggles) so
|
|
// the child dashboard stops offering it even before the next toggle.
|
|
// Config write via superuser: bonus_configs updates are parent-only.
|
|
// Outstanding (unclaimed/requested) rewards are untouched — they stay visible
|
|
// and collectible; only the approval moment completes the goal.
|
|
async function completeOnceConfig(famId: string, reward: any) {
|
|
if (!reward?.bonusConfigId) return;
|
|
try {
|
|
const cfg: any = await pbAdmin.getOne('bonus_configs', reward.bonusConfigId);
|
|
if (!cfg || cfg.famId !== famId) return;
|
|
if (cfg.occurrence !== 'once' || cfg.status === 'completed') return;
|
|
await pbAdmin.update('bonus_configs', cfg.id, {
|
|
status: 'completed',
|
|
completedAt: new Date().toISOString().slice(0, 10),
|
|
completedBy: reward.memberId || 'admin'
|
|
});
|
|
} catch {}
|
|
}
|
|
|
|
// Payday-gated rewards (claimable 'payday', e.g. pocket money) unlock on the
|
|
// settle DATE at the fam's configured payday TIME — not midnight. Anything
|
|
// else (claimable 'immediate'/unset, e.g. manual triggers) is always claimable.
|
|
export function assertPaydayUnlocked(reward: any, tz?: string, paydayTime?: string) {
|
|
if (!reward || reward.claimable !== 'payday' || !reward.settleDate) return;
|
|
const resolvedTz = resolveServerTz(tz);
|
|
const settle = (reward.settleDate as string).slice(0, 10);
|
|
const today = todayInTz(resolvedTz);
|
|
if (today < settle) {
|
|
throw new Error(`This bonus pays out on payday (${settle}) — hang tight!`);
|
|
}
|
|
if (today === settle && paydayTime && /^\d{2}:\d{2}$/.test(paydayTime)) {
|
|
const unlock = wallClockToUtc(settle, paydayTime, resolvedTz);
|
|
if (Date.now() < unlock) {
|
|
throw new Error(`This bonus unlocks at ${paydayTime} on payday — hang tight!`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Member claim → status 'requested' (pending parent approval).
|
|
export async function claim(pb: any, famId: string, id: string) {
|
|
const now = new Date().toISOString();
|
|
const { tz, paydayTime } = await famMeta(pb, famId);
|
|
const found = await pb
|
|
.collection('rewards')
|
|
.getFullList({ filter: `famId = '${famId}' && id = '${id}'` });
|
|
const reward = found?.[0];
|
|
if (!reward) throw new Error('Reward not found');
|
|
assertPaydayUnlocked(reward, tz, paydayTime);
|
|
return pb.collection('rewards').update(id, { status: 'requested', requestedAt: now });
|
|
}
|
|
|
|
// Admin approval → status 'claimed'.
|
|
export async function approve(pb: any, famId: string, id: string) {
|
|
const record = await pb.collection('rewards').update(id, {
|
|
status: 'claimed',
|
|
claimedAt: new Date().toISOString()
|
|
});
|
|
await completeOnceConfig(famId, record);
|
|
return record;
|
|
}
|
|
|
|
export async function requestAll(pb: any, famId: string, memberId: string) {
|
|
const now = new Date().toISOString();
|
|
const { tz, paydayTime } = await famMeta(pb, famId);
|
|
const rewards = await pb.collection('rewards').getFullList({
|
|
filter: `famId = '${famId}' && memberId = '${memberId}' && status = 'unclaimed'`
|
|
});
|
|
let count = 0;
|
|
for (const r of rewards) {
|
|
try {
|
|
assertPaydayUnlocked(r, tz, paydayTime);
|
|
} catch {
|
|
continue;
|
|
}
|
|
await pb.collection('rewards').update(r.id, { status: 'requested', requestedAt: now });
|
|
count++;
|
|
}
|
|
return { count };
|
|
}
|
|
|
|
export async function issueAll(pb: any, famId: string, memberId: string) {
|
|
if (!memberId) throw new Error('memberId required');
|
|
const now = new Date().toISOString();
|
|
const rewards = await pb.collection('rewards').getFullList({
|
|
filter: `famId = '${famId}' && memberId = '${memberId}' && rewardType = 'cash' && (status = 'unclaimed' || status = 'requested')`
|
|
});
|
|
let count = 0;
|
|
for (const r of rewards) {
|
|
const record = await pb.collection('rewards').update(r.id, { status: 'claimed', claimedAt: now });
|
|
await completeOnceConfig(famId, record);
|
|
count++;
|
|
}
|
|
return { count };
|
|
} |