Files
songs2vid/lib/credits.ts
T
Atakan Doğan Özban 9404efd86c Align preview typography with FFmpeg output, add video/song title split, and ship OSS updates.
Separate YouTube video titles from on-video song/artist fields with Pro gating, serve curated fonts and watermark assets for 1:1 preview parity, and include billing/API/docs/deploy stack for self-hosted release.
2026-07-27 16:26:19 +02:00

114 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 115 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) };
}