feature plus ux rearrangements
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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 }
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user