/** * Billing / credit constants for Songs2VID hybrid pricing. * Plan enum in DB: FREE | PREMIUM (Pro = €5/mo "starter_5eur"). */ export const CREDIT_CURRENCY = "eur"; /** Free monthly allocation */ export const FREE_MONTHLY_CREDITS = 10; /** Pro monthly allocation */ export const PRO_MONTHLY_CREDITS = 50; /** Free-tier top-up: buyer picks 1–15 credits */ export const FREE_TOP_UP_MIN = 1; export const FREE_TOP_UP_MAX = 15; /** €0.25 per extra credit */ export const CREDIT_PRICE_CENTS = 25; /** @deprecated use FREE_TOP_UP_MAX */ export const FREE_TOP_UP_CREDITS = FREE_TOP_UP_MAX; /** @deprecated use creditPurchaseTotalCents(FREE_TOP_UP_MAX) */ export const FREE_TOP_UP_PRICE_CENTS = FREE_TOP_UP_MAX * CREDIT_PRICE_CENTS; /** Pro subscription €5 / month */ export const PRO_PRICE_CENTS = 500; /** Soft cap on never-expiring extra credits (Pro / legacy storage bound) */ export const EXTRA_CREDITS_MAX = 1000; /** * Hard cap on total accumulated credits (monthly remaining + extras). * On renewal, unused + new monthly is trimmed to this threshold. */ export const MAX_CREDIT_CAP = 30; /** * Free plan: extras balance cannot exceed 15. * After monthly (10) + extras (≤15) are used, upgrade to Pro is required. */ export const FREE_EXTRA_CREDITS_MAX = FREE_TOP_UP_MAX; /** Stripe Tax / Managed Payments SaaS personal use */ export const STRIPE_PRODUCT_TAX_CODE = "txcd_10103000"; export const CREDIT_PURCHASE_MIN = FREE_TOP_UP_MIN; export const CREDIT_PURCHASE_MAX = FREE_TOP_UP_MAX; export const CREDIT_BALANCE_MAX = EXTRA_CREDITS_MAX; /** Shown in quota UI errors so UpgradeProLink can attach a CTA */ export const PRO_UPGRADE_REQUIRED_SUFFIX = " Upload up to 50 videos with Pro!"; export function monthlyCreditsForPlan(plan: "FREE" | "PREMIUM"): number { return plan === "PREMIUM" ? PRO_MONTHLY_CREDITS : FREE_MONTHLY_CREDITS; } export function freeExtraCreditsCap(): number { return FREE_EXTRA_CREDITS_MAX; } export function formatEuroFromCents(cents: number): string { const amount = cents / 100; const formatted = amount % 1 === 0 ? String(amount) : amount.toFixed(2); return `€${formatted}`; } export function creditPurchaseTotalCents(credits: number): number { return credits * CREDIT_PRICE_CENTS; } export function formatCreditPrice(credits = 1): string { return formatEuroFromCents(creditPurchaseTotalCents(credits)); } export function validateFreeTopUpAmount( credits: number, currentExtraBalance: number, monthlyRemaining = 0, ): { ok: true; credits: number; amountCents: number } | { ok: false; error: string } { if (!Number.isInteger(credits)) { return { ok: false, error: "Credit amount must be a whole number." }; } if (credits < FREE_TOP_UP_MIN || credits > FREE_TOP_UP_MAX) { return { ok: false, error: `You can buy between ${FREE_TOP_UP_MIN} and ${FREE_TOP_UP_MAX} credits per top-up.`, }; } if (currentExtraBalance >= FREE_EXTRA_CREDITS_MAX) { return { ok: false, error: `Free plan extras are capped at ${FREE_EXTRA_CREDITS_MAX}. Upgrade to Pro for 50 videos every month.${PRO_UPGRADE_REQUIRED_SUFFIX}`, }; } const freeExtraRoom = FREE_EXTRA_CREDITS_MAX - currentExtraBalance; const totalRoom = Math.max(0, MAX_CREDIT_CAP - monthlyRemaining - currentExtraBalance); const room = Math.min(freeExtraRoom, totalRoom); if (room <= 0) { return { ok: false, error: `Your total credit balance cannot exceed ${MAX_CREDIT_CAP}. Upgrade to Pro or use existing credits first.${PRO_UPGRADE_REQUIRED_SUFFIX}`, }; } if (credits > room) { return { ok: false, error: `You can buy up to ${room} more credits under the ${MAX_CREDIT_CAP}-credit account cap.`, }; } return { ok: true, credits, amountCents: creditPurchaseTotalCents(credits) }; }