Initial commit (unpacked platform)
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user