101 lines
3.9 KiB
TypeScript
101 lines
3.9 KiB
TypeScript
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 }
|
|
};
|
|
} |