Files

163 lines
4.5 KiB
TypeScript
Raw Permalink Normal View History

/**
* Plan catalog — the single source of truth for tiers, limits, and features.
* Provider-agnostic: Stripe and PayPal both map onto these plan keys, and
* `enforceLimit` / feature-gating read from here (never from a payment provider).
*
* Prices are in cents. A limit of `UNLIMITED` (-1) means no cap.
*/
export const UNLIMITED = -1;
export type PlanKey = "free" | "creator" | "pro" | "agency";
export type FeatureKey =
| "multi_voice"
| "cover_art"
| "content_repurposing"
| "all_languages"
| "ai_cohost"
| "series_generator"
| "api_access"
| "team_workspace"
| "white_label"
| "custom_branding"
| "priority_generation";
export type UsageMetric = "script" | "audio" | "art" | "repurpose";
export interface PlanLimits {
/** Monthly caps per metric; UNLIMITED (-1) = no cap. */
script: number;
audio: number;
art: number;
repurpose: number;
/** Seats in the team workspace (Agency). 1 for individual plans. */
seats: number;
/** Max length of a generated episode, in minutes. */
maxEpisodeMinutes: number;
}
export interface Plan {
key: PlanKey;
name: string;
/** Short marketing tagline. */
tagline: string;
priceMonthly: number; // cents
priceYearly: number; // cents (≈ 2 months free)
highlight?: boolean; // "most popular"
limits: PlanLimits;
features: FeatureKey[];
/** Human-readable bullet points for the pricing page. */
bullets: string[];
}
export const PLANS: Record<PlanKey, Plan> = {
free: {
key: "free",
name: "Free",
tagline: "Try it out, no card required.",
priceMonthly: 0,
priceYearly: 0,
limits: { script: 3, audio: 1, art: 3, repurpose: 1, seats: 1, maxEpisodeMinutes: 5 },
features: ["multi_voice", "cover_art"],
bullets: [
"3 scripts / month",
"1 audio generation / month",
"Up to 5-minute episodes",
"2 narrator voices",
"Cover art generation",
],
},
creator: {
key: "creator",
name: "Creator",
tagline: "For solo creators shipping regularly.",
priceMonthly: 900,
priceYearly: 9000,
limits: { script: 50, audio: 20, art: 50, repurpose: 50, seats: 1, maxEpisodeMinutes: 20 },
features: ["multi_voice", "cover_art", "content_repurposing", "all_languages"],
bullets: [
"50 scripts / month",
"20 audio generations / month",
"Up to 20-minute episodes",
"All 14+ voices",
"13+ languages",
"Content repurposing (blog, social, newsletter)",
],
},
pro: {
key: "pro",
name: "Pro",
tagline: "For serious podcasters and small studios.",
priceMonthly: 2900,
priceYearly: 29000,
highlight: true,
limits: { script: UNLIMITED, audio: 100, art: UNLIMITED, repurpose: UNLIMITED, seats: 1, maxEpisodeMinutes: 45 },
features: [
"multi_voice",
"cover_art",
"content_repurposing",
"all_languages",
"ai_cohost",
"series_generator",
"api_access",
"priority_generation",
],
bullets: [
"Unlimited scripts",
"100 audio generations / month",
"Up to 45-minute episodes",
"AI co-host mode",
"Series & season generator",
"API access",
"Priority generation queue",
],
},
agency: {
key: "agency",
name: "Agency",
tagline: "For teams and white-label studios.",
priceMonthly: 7900,
priceYearly: 79000,
limits: { script: UNLIMITED, audio: UNLIMITED, art: UNLIMITED, repurpose: UNLIMITED, seats: 5, maxEpisodeMinutes: 90 },
features: [
"multi_voice",
"cover_art",
"content_repurposing",
"all_languages",
"ai_cohost",
"series_generator",
"api_access",
"priority_generation",
"team_workspace",
"white_label",
"custom_branding",
],
bullets: [
"Everything in Pro, unlimited",
"5-seat team workspace",
"White-label mode",
"Custom branding",
"Up to 90-minute episodes",
"Priority support",
],
},
};
export const PLAN_ORDER: PlanKey[] = ["free", "creator", "pro", "agency"];
export function getPlan(key: string | null | undefined): Plan {
if (key && key in PLANS) return PLANS[key as PlanKey];
return PLANS.free;
}
export function planHasFeature(key: PlanKey, feature: FeatureKey): boolean {
return PLANS[key].features.includes(feature);
}
/** True when `count` is within the plan's cap for `metric` (UNLIMITED always passes). */
export function withinLimit(key: PlanKey, metric: UsageMetric, count: number): boolean {
const limit = PLANS[key].limits[metric];
return limit === UNLIMITED || count < limit;
}