← back to Dear Bubbe Nextjs
lib/bubbe-pricing.ts
309 lines
/**
* Bubbe Pricing + Feature Flags System
*
* SAFE: Read-only helper for plans and features.
* Does NOT modify existing Bubbe chat logic.
* You can version this as v2 without changing any current behavior.
*/
export type PlanId = "basic" | "plus" | "max" | "supermax";
export type FeatureKey =
| "advice_guilt"
| "yiddish_sayings"
| "daily_reminders"
| "voice_notes"
| "eat_skinny_mode"
| "cooking_tips"
| "family_gossip"
| "oy_vey_responses"
| "storytelling"
| "yiddish_dictionary_basic"
| "yiddish_dictionary_full"
| "holiday_mode"
| "local_news"
| "local_sports"
| "local_weather"
| "personalized_insights";
export const PLAN_FEATURES: Record<PlanId, FeatureKey[]> = {
basic: [
"advice_guilt",
"yiddish_sayings",
"oy_vey_responses",
"storytelling",
"yiddish_dictionary_basic"
],
plus: [
"advice_guilt",
"yiddish_sayings",
"oy_vey_responses",
"storytelling",
"yiddish_dictionary_basic",
"daily_reminders",
"eat_skinny_mode",
"cooking_tips",
"family_gossip"
],
max: [
"advice_guilt",
"yiddish_sayings",
"oy_vey_responses",
"storytelling",
"yiddish_dictionary_basic",
"yiddish_dictionary_full",
"daily_reminders",
"eat_skinny_mode",
"cooking_tips",
"family_gossip",
"voice_notes",
"holiday_mode"
],
supermax: [
"advice_guilt",
"yiddish_sayings",
"oy_vey_responses",
"storytelling",
"yiddish_dictionary_basic",
"yiddish_dictionary_full",
"daily_reminders",
"eat_skinny_mode",
"cooking_tips",
"family_gossip",
"voice_notes",
"holiday_mode",
"local_news",
"local_sports",
"local_weather",
"personalized_insights"
]
};
export const PLAN_PRICES: Record<PlanId, number> = {
basic: 0,
plus: 18,
max: 36,
supermax: 54
};
export const PLAN_NAMES: Record<PlanId, string> = {
basic: "Basic – Free",
plus: "Bubbe Plus",
max: "Bubbe Max",
supermax: "Super Max"
};
export const PLAN_DESCRIPTIONS: Record<PlanId, string> = {
basic: "A taste of Bubbe's wisdom",
plus: "The full Bubbe experience",
max: "Bubbe on steroids – all the guilt, all the time",
supermax: "Bubbe knows EVERYTHING about your neighborhood – oy gevalt!"
};
export const FEATURE_DESCRIPTIONS: Record<FeatureKey, string> = {
advice_guilt: "Classic Bubbe advice & guilt trips",
yiddish_sayings: "Basic Yiddish sayings",
daily_reminders: "Daily reminders to eat, call your mother, etc.",
voice_notes: "Voice notes that sound like a real Bubbe",
eat_skinny_mode: "Proactive 'you look skinny' prompts",
cooking_tips: "Classic Bubbe-style recipes and cooking advice",
family_gossip: "Playful family gossip mode",
oy_vey_responses: "'Oy vey, what are you doing?' responses",
storytelling: "Storytelling mode with Bubbe tales",
yiddish_dictionary_basic: "Basic Yiddish dictionary",
yiddish_dictionary_full: "Full Yiddish dictionary with slang & insults",
holiday_mode: "Full Jewish holiday reminders & blessings",
local_news: "Local news from YOUR neighborhood with Bubbe's hot takes",
local_sports: "Your local teams' scores with Bubbe's commentary",
local_weather: "Hyper-local weather with guilt about not dressing warm",
personalized_insights: "Bubbe knows your city like the back of her hand"
};
const PLAN_ORDER: PlanId[] = ["basic", "plus", "max", "supermax"];
/**
* Check if a plan includes a specific feature
*/
export function planIncludesFeature(plan: PlanId, feature: FeatureKey): boolean {
return PLAN_FEATURES[plan].includes(feature);
}
/**
* Given a list of desired features from signup,
* return the minimum plan that satisfies them all.
*/
export function getRequiredPlanForFeatures(features: FeatureKey[]): PlanId {
for (const plan of PLAN_ORDER) {
const ok = features.every((f) => planIncludesFeature(plan, f));
if (ok) return plan;
}
// If something somehow falls through, default to max
return "max";
}
/**
* Given a plan, return human-readable info for UI.
*/
export function getPlanSummary(plan: PlanId) {
return {
id: plan,
name: PLAN_NAMES[plan],
description: PLAN_DESCRIPTIONS[plan],
pricePerMonth: PLAN_PRICES[plan],
features: PLAN_FEATURES[plan],
featureDetails: PLAN_FEATURES[plan].map(f => ({
key: f,
description: FEATURE_DESCRIPTIONS[f]
}))
};
}
/**
* Get all plans with their details (for pricing page)
*/
export function getAllPlansWithComparison() {
const allFeatures = Object.keys(FEATURE_DESCRIPTIONS) as FeatureKey[];
return PLAN_ORDER.map(plan => ({
...getPlanSummary(plan),
featureComparison: allFeatures.map(feature => ({
key: feature,
description: FEATURE_DESCRIPTIONS[feature],
included: planIncludesFeature(plan, feature)
}))
}));
}
/**
* Check if a user can access a feature based on their plan
* Returns true if allowed, or a Bubbe-style rejection message if not
*/
export function checkFeatureAccess(
userPlan: PlanId,
requestedFeature: FeatureKey
): { allowed: boolean; bubbeResponse?: string } {
const allowed = planIncludesFeature(userPlan, requestedFeature);
if (!allowed) {
// Find the minimum plan that includes this feature
const requiredPlan = getRequiredPlanForFeatures([requestedFeature]);
const price = PLAN_PRICES[requiredPlan];
// Bubbe-style rejection messages based on the feature
const rejections: Record<FeatureKey, string> = {
voice_notes: `Oy, you want to hear my voice? For ${price} dollars a month you can hear me kvetch all day! Such a bargain!`,
holiday_mode: `What, you need ME to remind you about the holidays? Your mother didn't teach you? Upgrade to ${PLAN_NAMES[requiredPlan]} already!`,
daily_reminders: `You need me to remind you to eat? Maybe if you paid ${price} dollars I'd care enough to nag you properly!`,
eat_skinny_mode: `You look skinny but your wallet looks skinnier! ${price} dollars gets you all my food guilt!`,
cooking_tips: `My secret recipes aren't FREE, bubbeleh! Fork over ${price} dollars or eat takeout forever!`,
family_gossip: `Gossip costs money, sweetie. ${price} dollars and I'll tell you what your cousin REALLY did!`,
yiddish_dictionary_full: `The good Yiddish words cost extra! ${price} dollars or you're stuck with 'oy vey'!`,
local_news: `You want LOCAL news? What, you think I'm your personal newspaper? ${price} dollars for Super Max and I'll tell you EVERYTHING happening in your schmutz neighborhood!`,
local_sports: `Your teams are all terrible anyway! But for ${price} dollars I'll tell you EXACTLY how terrible they are, with LOCAL scores!`,
local_weather: `You can't look outside? For ${price} dollars I'll tell you the weather on YOUR street, not just the city. And I'll remind you to wear a jacket!`,
personalized_insights: `Oh NOW you want me to know everything about your area? ${price} dollars for Super Max and I'll be your neighborhood yenta!`,
advice_guilt: "Even I'm not THAT cruel to charge for guilt! Oh wait, this is free already!",
yiddish_sayings: "Basic Yiddish is free, like my love - comes with strings attached!",
oy_vey_responses: "Oy vey is free, just like your cheapskate attitude!",
storytelling: "Stories are free, but the GOOD ones... that's another story!",
yiddish_dictionary_basic: "You get some Yiddish for free. Be grateful, ungrateful child!"
};
return {
allowed: false,
bubbeResponse: rejections[requestedFeature] ||
`Such expensive tastes on a ${PLAN_NAMES[userPlan]} plan! Maybe you upgrade to ${PLAN_NAMES[requiredPlan]} for ${price} dollars, nu?`
};
}
return { allowed: true };
}
/**
* Parse feature requirements from user message
* This is a helper for detecting when users ask for premium features
*/
export function detectRequestedFeatures(message: string): FeatureKey[] {
const features: FeatureKey[] = [];
const lowerMessage = message.toLowerCase();
// Voice-related
if (lowerMessage.match(/\b(voice|speak|talk|hear|sound|audio)\b/)) {
features.push("voice_notes");
}
// Holiday-related
if (lowerMessage.match(/\b(shabbat|sabbath|pesach|passover|chanukah|hanukkah|rosh|yom kippur|sukkot|holiday)\b/)) {
features.push("holiday_mode");
}
// Reminder-related
if (lowerMessage.match(/\b(remind|reminder|daily|every day|notification)\b/)) {
features.push("daily_reminders");
}
// Cooking-related
if (lowerMessage.match(/\b(recipe|cook|cooking|bake|baking|kitchen|food prep)\b/)) {
features.push("cooking_tips");
}
// Gossip-related
if (lowerMessage.match(/\b(gossip|family news|family drama|cousin|relative)\b/)) {
features.push("family_gossip");
}
// Yiddish dictionary
if (lowerMessage.match(/\b(yiddish dictionary|translate|yiddish word|what does.*mean|yiddish slang)\b/)) {
if (lowerMessage.match(/\b(full|complete|all|comprehensive|slang|insult)\b/)) {
features.push("yiddish_dictionary_full");
} else {
features.push("yiddish_dictionary_basic");
}
}
// Local content - news, sports, weather
if (lowerMessage.match(/\b(local news|news in my area|what's happening in|neighborhood news|city news)\b/)) {
features.push("local_news");
}
if (lowerMessage.match(/\b(local sports|my team|home team|city team|neighborhood team|lakers|dodgers|yankees|mets|bulls|bears)\b/)) {
features.push("local_sports");
}
if (lowerMessage.match(/\b(local weather|weather in my area|weather near me|my neighborhood weather|weather on my street)\b/)) {
features.push("local_weather");
}
// Personalized insights (catches general local requests)
if (lowerMessage.match(/\b(my neighborhood|my area|my city|around here|near me|locally)\b/) &&
!features.includes("local_news") && !features.includes("local_sports") && !features.includes("local_weather")) {
features.push("personalized_insights");
}
return features;
}
/**
* Generate a Bubbe-style upsell message
*/
export function generateUpsellMessage(
currentPlan: PlanId,
suggestedPlan: PlanId
): string {
if (currentPlan === suggestedPlan) {
return "You already have this plan, what more do you want from me?";
}
const price = PLAN_PRICES[suggestedPlan];
const planName = PLAN_NAMES[suggestedPlan];
const upsells = [
`Listen bubbeleh, for just ${price} dollars a month you could have ${planName}. That's less than your fancy coffee!`,
`Oy, still on the ${PLAN_NAMES[currentPlan]} plan? Your cousin upgraded to ${planName} already!`,
`${price} dollars? That's nothing! You spend more on those video games you never play!`,
`For ${price} dollars you get ${planName}. Such a deal! I should charge double!`,
`You want the good stuff but won't pay ${price} dollars? What am I, a charity?`
];
return upsells[Math.floor(Math.random() * upsells.length)];
}