import { PLAN_DEFINITIONS, TRIAL_DAYS, type Entitlements, type SubscriptionPlan, type SubscriptionStatus, } from "@app/shared-types"; export interface SubscriptionSnapshot { plan: SubscriptionPlan; status: SubscriptionStatus; expiresAt?: Date | null; gracePeriodExpiresAt?: Date | null; } export interface TrialSnapshot { startedAt: Date; endsAt: Date; } export interface UsageSnapshot { aiScansUsedThisMonth: number; } /** * Backend är source of truth (spec §47, §61.14). Denna funktion är den enda * platsen som avgör vad en användare får göra. * * Prioritet: aktiv betald prenumeration (inkl. grace) > pågående trial > free. */ export function computeEntitlements( subscription: SubscriptionSnapshot | null, trial: TrialSnapshot | null, usage: UsageSnapshot, now: Date = new Date(), ): Entitlements { // 1. Betald prenumeration if (subscription) { const withinPeriod = subscription.expiresAt == null || subscription.expiresAt > now; const withinGrace = subscription.gracePeriodExpiresAt != null && subscription.gracePeriodExpiresAt > now; const active = (subscription.status === "active" || subscription.status === "trial") && withinPeriod; const inGrace = subscription.status === "in_grace" && withinGrace; if (active || inGrace) { const def = PLAN_DEFINITIONS[subscription.plan]; return { plan: subscription.plan, status: inGrace ? "in_grace" : subscription.status, maxHouseholdMembers: def.maxHouseholdMembers, aiScansPerMonth: def.aiScansPerMonth, aiScansUsedThisMonth: usage.aiScansUsedThisMonth, weekPlanning: def.weekPlanning, advancedNutrition: def.advancedNutrition, communityPublish: def.communityPublish, expiresAt: subscription.expiresAt?.toISOString(), graceUntil: subscription.gracePeriodExpiresAt?.toISOString(), }; } } // 2. Trial: 7 dagar full tillgång utan kort (spec §46). Family-nivå under trial. if (trial && trial.endsAt > now) { const def = PLAN_DEFINITIONS.family; return { plan: "family", status: "trial", maxHouseholdMembers: def.maxHouseholdMembers, aiScansPerMonth: def.aiScansPerMonth, aiScansUsedThisMonth: usage.aiScansUsedThisMonth, weekPlanning: true, advancedNutrition: true, communityPublish: true, expiresAt: trial.endsAt.toISOString(), }; } // 3. Gratisnivå: användbar men begränsad (spec §46). const free = PLAN_DEFINITIONS.free; return { plan: "free", status: "free", maxHouseholdMembers: free.maxHouseholdMembers, aiScansPerMonth: free.aiScansPerMonth, aiScansUsedThisMonth: usage.aiScansUsedThisMonth, weekPlanning: free.weekPlanning, advancedNutrition: free.advancedNutrition, communityPublish: free.communityPublish, }; } export function trialEndsAt(startedAt: Date): Date { return new Date(startedAt.getTime() + TRIAL_DAYS * 86_400_000); } /** Har användaren AI-skanningar kvar denna månad? (fair use, spec §45) */ export function canUseAiScan(entitlements: Entitlements): boolean { return entitlements.aiScansUsedThisMonth < entitlements.aiScansPerMonth; } export function resolvePlanFromProductId(productId: string): SubscriptionPlan | null { for (const def of Object.values(PLAN_DEFINITIONS)) { if (def.appleProductId === productId || def.googleProductId === productId) return def.plan; } return null; }