Initial commit: PodcastYes — AI podcast platform
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user