diff --git a/backend/src/app.ts b/backend/src/app.ts index f35b680..68c11bb 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -262,6 +262,25 @@ const stripePriceByBillingPlanAndInterval: Partial, Record> = { + household_basic: { + monthly: ['STRIPE_PRICE_HOUSEHOLD_CONURE_MONTHLY', 'STRIPE_PRICE_HOUSEHOLD_CONURE'], + yearly: ['STRIPE_PRICE_HOUSEHOLD_CONURE_YEARLY'], + }, + household_plus: { + monthly: ['STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_MONTHLY', 'STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK'], + yearly: ['STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_YEARLY'], + }, + household_macaw: { + monthly: ['STRIPE_PRICE_HOUSEHOLD_MACAW_MONTHLY', 'STRIPE_PRICE_HOUSEHOLD_MACAW'], + yearly: ['STRIPE_PRICE_HOUSEHOLD_MACAW_YEARLY'], + }, +}; +const stripePricePlanLabels: Record, string> = { + household_basic: 'Conure', + household_plus: 'Indian Ringneck', + household_macaw: 'Macaw', +}; const stripe = stripeSecretKey ? new Stripe(stripeSecretKey) : null; const adminEmails = new Set( (process.env.ADMIN_EMAILS ?? '') @@ -573,7 +592,11 @@ const getStripePriceIdForBillingPlan = (billingPlan: BillingPlan, billingInterva const priceId = stripePriceByBillingPlanAndInterval[billingPlan]?.[billingInterval]?.trim() ?? ''; if (!priceId) { - throw new Error(`Stripe price is not configured for ${billingPlan} (${billingInterval}).`); + const planLabel = stripePricePlanLabels[billingPlan] ?? billingPlan; + const envNames = stripePriceEnvNamesByBillingPlanAndInterval[billingPlan]?.[billingInterval] ?? []; + const envHint = envNames.length > 0 ? ` Set ${envNames.join(' or ')} in the backend environment.` : ''; + + throw new Error(`Stripe price is not configured for ${planLabel} ${billingInterval}.${envHint}`); } return priceId; diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index c3110e0..4a5becd 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -19,7 +19,7 @@ export const ensureSchema = async (database: DatabaseClient = db) => { billing_email VARCHAR(255), billing_plan VARCHAR(32) NOT NULL DEFAULT 'household_basic', billing_interval VARCHAR(16) NOT NULL DEFAULT 'monthly', - subscription_status VARCHAR(32) NOT NULL DEFAULT 'active', + subscription_status VARCHAR(32) NOT NULL DEFAULT 'none', stripe_customer_id VARCHAR(255), stripe_subscription_id VARCHAR(255), rescue_verification_status VARCHAR(32) NOT NULL DEFAULT 'not_required', @@ -34,7 +34,7 @@ export const ensureSchema = async (database: DatabaseClient = db) => { ADD COLUMN IF NOT EXISTS billing_email VARCHAR(255), ADD COLUMN IF NOT EXISTS billing_plan VARCHAR(32) NOT NULL DEFAULT 'household_basic', ADD COLUMN IF NOT EXISTS billing_interval VARCHAR(16) NOT NULL DEFAULT 'monthly', - ADD COLUMN IF NOT EXISTS subscription_status VARCHAR(32) NOT NULL DEFAULT 'active', + ADD COLUMN IF NOT EXISTS subscription_status VARCHAR(32) NOT NULL DEFAULT 'none', ADD COLUMN IF NOT EXISTS stripe_customer_id VARCHAR(255), ADD COLUMN IF NOT EXISTS stripe_subscription_id VARCHAR(255), ADD COLUMN IF NOT EXISTS rescue_verification_status VARCHAR(32) NOT NULL DEFAULT 'not_required'; @@ -47,6 +47,12 @@ export const ensureSchema = async (database: DatabaseClient = db) => { ON workspaces (stripe_customer_id) WHERE stripe_customer_id IS NOT NULL; + UPDATE workspaces + SET subscription_status = 'none' + WHERE workspace_type = 'standard' + AND stripe_subscription_id IS NULL + AND subscription_status = 'active'; + UPDATE workspaces SET rescue_verification_status = 'pending' WHERE workspace_type = 'rescue' diff --git a/backend/src/repositories/workspaceRepository.ts b/backend/src/repositories/workspaceRepository.ts index 5739d33..2c904b5 100644 --- a/backend/src/repositories/workspaceRepository.ts +++ b/backend/src/repositories/workspaceRepository.ts @@ -106,7 +106,7 @@ export const ensurePersonalWorkspaceForUser = async (user: UserRow) => { if (!unclaimed.rowCount) { await db.query( `INSERT INTO workspaces (id, name, workspace_type, billing_plan, billing_interval, billing_email, subscription_status, rescue_verification_status) - VALUES ($1, $2, 'standard', 'household_basic', 'monthly', $3, 'active', 'not_required')`, + VALUES ($1, $2, 'standard', 'household_basic', 'monthly', $3, 'none', 'not_required')`, [workspaceId, `${user.name}'s Flock`, user.email], ); } else { @@ -117,7 +117,7 @@ export const ensurePersonalWorkspaceForUser = async (user: UserRow) => { billing_plan = 'household_basic', billing_interval = 'monthly', billing_email = $3, - subscription_status = 'active', + subscription_status = 'none', rescue_verification_status = 'not_required', updated_at = CURRENT_TIMESTAMP WHERE id = $1`, @@ -178,7 +178,7 @@ export const createWorkspace = async ({ billingEmail, billingPlan, billingInterval, - workspaceType === 'rescue' ? 'active' : 'active', + workspaceType === 'rescue' ? 'active' : 'none', workspaceType === 'rescue' ? 'pending' : 'not_required', ], ); @@ -412,6 +412,10 @@ export const updateRescueVerificationStatus = async (workspaceId: number, status WHEN $2 = 'rejected' THEN 'monthly' ELSE billing_interval END, + subscription_status = CASE + WHEN $2 = 'rejected' THEN 'none' + ELSE subscription_status + END, rescue_verification_status = CASE WHEN $2 = 'rejected' THEN 'not_required' ELSE $2 @@ -432,6 +436,7 @@ export const cancelRescueVerificationRequest = async (workspaceId: number) => { SET workspace_type = 'standard', billing_plan = 'household_basic', billing_interval = 'monthly', + subscription_status = 'none', rescue_verification_status = 'not_required', updated_at = CURRENT_TIMESTAMP WHERE id = $1 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 42b3b23..5c1f6d9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -521,6 +521,36 @@ const formatBillingPlanCapacity = (billingPlan: BillingPlan) => { return 'Permits 11 or more birds in the flock.'; }; +const formatBillingPlanDropdownLabel = (billingPlan: HouseholdBillingPlan) => { + if (billingPlan === 'household_basic') { + return 'Conure (4 birds)'; + } + + if (billingPlan === 'household_plus') { + return 'Indian Ringneck (10 birds)'; + } + + return 'Macaw (11+)'; +}; + +const householdPlanPrices: Record> = { + household_basic: { + monthly: '$4.99/month', + yearly: '$50/year', + }, + household_plus: { + monthly: '$8.99/month', + yearly: '$90/year', + }, + household_macaw: { + monthly: '$15.99/month', + yearly: '$160/year', + }, +}; + +const formatBillingIntervalDropdownLabel = (billingPlan: HouseholdBillingPlan, billingInterval: BillingInterval) => + `${formatBillingIntervalName(billingInterval)} (${householdPlanPrices[billingPlan][billingInterval]})`; + const formatBillingPlanBirdLimit = (billingPlan: BillingPlan) => { if (billingPlan === 'household_basic') { return '4'; @@ -3392,9 +3422,9 @@ function App() { }) } > - - - + + +