163 lines
4.5 KiB
TypeScript
163 lines
4.5 KiB
TypeScript
|
|
/**
|
||
|
|
* 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;
|
||
|
|
}
|