import { pbAdmin } from '$lib/server/pocketbase'; // How a family has access. 'none' = signed up with no code/sub yet (gated). export type PaymentMode = 'none' | 'code' | 'sub' | 'canceled'; export interface FamAccess { disabled: boolean; mode: PaymentMode; reason: '' | 'no_access' | 'canceled' | 'subscription_inactive' | 'code_disabled' | 'code_expired'; } // UTC timestamp `months` months after `iso`. Used for the code's global expiry // (from the code's createdAt) and the duration clock (from entry date). export function addMonthsUTC(iso: string | Date, months: number): number { const d = new Date(iso); d.setUTCMonth(d.getUTCMonth() + months); return d.getTime(); } // A code is usable iff it exists, is not globally disabled, is not past its own // createdAt+expiry window (expiry 0 = never), and the duration clock from the // fam's entry date hasn't run out (duration 0 = continuous). export function codeIsValid(code: any, enteredAt?: string): boolean { if (!code) return false; if (code.active === false) return false; const now = Date.now(); const expiryMonths = Number(code.expiry) || 0; if (expiryMonths > 0 && now >= addMonthsUTC(code.createdAt, expiryMonths)) return false; const durationMonths = Number(code.duration) || 0; if (durationMonths > 0 && enteredAt) { if (now >= addMonthsUTC(enteredAt, durationMonths)) return false; } return true; } // Deterministic decision from a fam + its linked code (no I/O). `active` on the // fam is the single source of truth for "usable right now". export function computeFamAccess(fam: any, code: any): FamAccess { const mode: PaymentMode = fam.paymentMode || 'none'; switch (mode) { case 'code': { if (code?.active === false) return { disabled: true, mode, reason: 'code_disabled' }; return codeIsValid(code, fam.accessCodeEnteredAt) ? { disabled: false, mode, reason: '' } : { disabled: true, mode, reason: 'code_expired' }; } case 'sub': return fam.active === false ? { disabled: true, mode, reason: 'subscription_inactive' } : { disabled: false, mode, reason: '' }; case 'canceled': return { disabled: true, mode, reason: 'canceled' }; case 'none': default: return { disabled: true, mode, reason: 'no_access' }; } } // Read the fam + linked code and persist `active` if it drifted. Called on every // [fam] layout load (both roles) and wherever access state must be re-evaluated. export async function ensureFamAccess(famId: string): Promise<{ fam: any; access: FamAccess }> { const fam = await pbAdmin.getOne('fams', famId); if (!fam) return { fam: null, access: { disabled: true, mode: 'none', reason: 'no_access' } }; let code: any = null; if (fam.accessCodeId) { try { code = await pbAdmin.getOne('accesscodes', fam.accessCodeId); } catch { code = null; } } const access = computeFamAccess(fam, code); if (fam.active !== !access.disabled) { await pbAdmin.update('fams', famId, { active: !access.disabled }); } return { fam: { ...fam, active: !access.disabled }, access }; } // Apply an access code value to a fam. Automatic (no approval) — validates // against the accesscodes collection and sets paymentMode='code' on success. export async function applyAccessCode(famId: string, value: string) { const val = String(value || '').trim(); if (!val) return { error: 'Enter an access code' }; const list = await pbAdmin.getList('accesscodes', `value = '${val}'`); const code = list?.[0]; if (!code) return { error: 'That access code is not recognised' }; if (code.active === false) return { error: 'That access code is disabled' }; const now = new Date().toISOString(); const valid = codeIsValid(code, now); await pbAdmin.update('fams', famId, { paymentMode: 'code', accessCodeId: code.id, accessCodeEnteredAt: now, active: valid }); return { ok: true, active: valid, code: { name: code.name, value: code.value, duration: code.duration, expiry: code.expiry } }; }