From 841d0a966918ac3decf4c00ae6b944be211af547 Mon Sep 17 00:00:00 2001 From: blaisadmin Date: Sat, 30 May 2026 15:19:47 -0400 Subject: [PATCH] Updated subscriptions --- .env.example | 9 +++++--- README.md | 8 ++++--- backend/src/app.ts | 33 ++++++++++++++++++++++------ backend/src/types.ts | 2 +- docker-compose.prod.yml | 9 +++++--- docker-compose.yml | 9 +++++--- docs/API_REFERENCE.md | 2 +- frontend/src/App.tsx | 48 ++++++++++++++++++++++++++++++----------- 8 files changed, 86 insertions(+), 34 deletions(-) diff --git a/.env.example b/.env.example index c12db60..1ed0bfe 100644 --- a/.env.example +++ b/.env.example @@ -29,9 +29,12 @@ STRIPE_PRICE_HOUSEHOLD_CONURE_YEARLY= STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK= STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_MONTHLY= STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_YEARLY= -STRIPE_PRICE_HOUSEHOLD_MACAW= -STRIPE_PRICE_HOUSEHOLD_MACAW_MONTHLY= -STRIPE_PRICE_HOUSEHOLD_MACAW_YEARLY= +STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY= +STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_MONTHLY= +STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_YEARLY= +STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW= +STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_MONTHLY= +STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_YEARLY= STRIPE_CHECKOUT_SUCCESS_URL=http://localhost:3000/?billing=success STRIPE_CHECKOUT_CANCEL_URL=http://localhost:3000/?billing=cancelled STRIPE_PORTAL_RETURN_URL=http://localhost:3000/?billing=portal diff --git a/README.md b/README.md index 9cb26ff..1fdd36e 100644 --- a/README.md +++ b/README.md @@ -203,8 +203,10 @@ Set these when the Stripe Checkout, Customer Portal, and webhook flow is enabled - `STRIPE_PRICE_HOUSEHOLD_CONURE_YEARLY` - `STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_MONTHLY` or `STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK` - `STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_YEARLY` -- `STRIPE_PRICE_HOUSEHOLD_MACAW_MONTHLY` or `STRIPE_PRICE_HOUSEHOLD_MACAW` -- `STRIPE_PRICE_HOUSEHOLD_MACAW_YEARLY` +- `STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_MONTHLY` or `STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY` +- `STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_YEARLY` +- `STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_MONTHLY` or `STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW` +- `STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_YEARLY` - `STRIPE_CHECKOUT_SUCCESS_URL` - `STRIPE_CHECKOUT_CANCEL_URL` - `STRIPE_PORTAL_RETURN_URL` @@ -221,7 +223,7 @@ Recommended defaults: - Enable the proration behavior you want in the Customer Portal configuration. FlockPal treats Stripe as the source of truth for upgrade timing and proration outcomes. - Point the Stripe webhook endpoint at `https://your-backend-host/api/billing/stripe/webhook`. - Subscribe the webhook endpoint to at least `checkout.session.completed`, `customer.subscription.created`, `customer.subscription.updated`, and `customer.subscription.deleted`. -- Use one Stripe Price per plan and billing interval. FlockPal maps Stripe price IDs back to `household_basic`, `household_plus`, and `household_macaw`. +- Use one Stripe Price per plan and billing interval. FlockPal maps Stripe price IDs back to `household_basic`, `household_plus`, `household_macaw`, and `household_hyacinth_macaw`. - After Stripe redirects back to the app, FlockPal now performs a direct billing sync against Stripe and then refreshes the active session. Webhooks are still required so asynchronous subscription changes stay in sync later. For local development with the Stripe CLI: diff --git a/backend/src/app.ts b/backend/src/app.ts index a8df99d..0b8afa7 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -199,7 +199,7 @@ const switchWorkspaceSchema = z.object({ const workspaceTypeSchema = z.enum(['standard', 'rescue']); const workspaceRoleSchema = z.enum(['owner', 'assistant', 'caregiver', 'viewer']); -const billingPlanSchema = z.enum(['household_basic', 'household_plus', 'household_macaw']); +const billingPlanSchema = z.enum(['household_basic', 'household_plus', 'household_macaw', 'household_hyacinth_macaw']); const billingIntervalSchema = z.enum(['monthly', 'yearly']); const integrationTokenScopeSchema = z.enum(['read_only', 'read_write']); const birdGenderSchema = z.enum(['unknown', 'male', 'female']); @@ -383,12 +383,16 @@ const createCodeChallenge = (verifier: string) => crypto.createHash('sha256').up const resolveBillingPlan = ( workspaceType: WorkspaceType, - requestedPlan?: BillingPlan | 'household_basic' | 'household_plus' | 'household_macaw', + requestedPlan?: BillingPlan | 'household_basic' | 'household_plus' | 'household_macaw' | 'household_hyacinth_macaw', ) => { if (workspaceType === 'rescue') { return 'rescue_free' as const; } + if (requestedPlan === 'household_hyacinth_macaw') { + return 'household_hyacinth_macaw'; + } + if (requestedPlan === 'household_macaw') { return 'household_macaw'; } @@ -440,8 +444,18 @@ const stripePriceByBillingPlanAndInterval: Partial, Record> = { @@ -454,14 +468,19 @@ const stripePriceEnvNamesByBillingPlanAndInterval: Record, string> = { household_basic: 'Conure', household_plus: 'Indian Ringneck', - household_macaw: 'Macaw', + household_macaw: 'African Grey', + household_hyacinth_macaw: 'Hyacinth Macaw', }; const stripe = stripeSecretKey ? new Stripe(stripeSecretKey) : null; const adminEmails = new Set( diff --git a/backend/src/types.ts b/backend/src/types.ts index 390ee05..97c2e22 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -1,6 +1,6 @@ export type WorkspaceType = 'standard' | 'rescue'; export type WorkspaceRole = 'owner' | 'assistant' | 'caregiver' | 'viewer'; -export type BillingPlan = 'rescue_free' | 'household_basic' | 'household_plus' | 'household_macaw'; +export type BillingPlan = 'rescue_free' | 'household_basic' | 'household_plus' | 'household_macaw' | 'household_hyacinth_macaw'; export type BillingInterval = 'monthly' | 'yearly'; export type SubscriptionStatus = 'active' | 'trialing' | 'past_due' | 'canceled' | 'unpaid' | 'none'; export type RescueVerificationStatus = 'not_required' | 'pending' | 'approved' | 'rejected'; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 167a94b..78a6a71 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -73,9 +73,12 @@ services: STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK: ${STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK:-} STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_MONTHLY: ${STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_MONTHLY:-} STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_YEARLY: ${STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_YEARLY:-} - STRIPE_PRICE_HOUSEHOLD_MACAW: ${STRIPE_PRICE_HOUSEHOLD_MACAW:-} - STRIPE_PRICE_HOUSEHOLD_MACAW_MONTHLY: ${STRIPE_PRICE_HOUSEHOLD_MACAW_MONTHLY:-} - STRIPE_PRICE_HOUSEHOLD_MACAW_YEARLY: ${STRIPE_PRICE_HOUSEHOLD_MACAW_YEARLY:-} + STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY: ${STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY:-} + STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_MONTHLY: ${STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_MONTHLY:-} + STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_YEARLY: ${STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_YEARLY:-} + STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW: ${STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW:-} + STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_MONTHLY: ${STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_MONTHLY:-} + STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_YEARLY: ${STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_YEARLY:-} STRIPE_CHECKOUT_SUCCESS_URL: ${STRIPE_CHECKOUT_SUCCESS_URL:-${FRONTEND_URL}/?billing=success} STRIPE_CHECKOUT_CANCEL_URL: ${STRIPE_CHECKOUT_CANCEL_URL:-${FRONTEND_URL}/?billing=cancelled} STRIPE_PORTAL_RETURN_URL: ${STRIPE_PORTAL_RETURN_URL:-${FRONTEND_URL}/?billing=portal} diff --git a/docker-compose.yml b/docker-compose.yml index b7cd0cc..dc25cec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -71,9 +71,12 @@ services: STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK: ${STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK:-} STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_MONTHLY: ${STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_MONTHLY:-} STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_YEARLY: ${STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_YEARLY:-} - STRIPE_PRICE_HOUSEHOLD_MACAW: ${STRIPE_PRICE_HOUSEHOLD_MACAW:-} - STRIPE_PRICE_HOUSEHOLD_MACAW_MONTHLY: ${STRIPE_PRICE_HOUSEHOLD_MACAW_MONTHLY:-} - STRIPE_PRICE_HOUSEHOLD_MACAW_YEARLY: ${STRIPE_PRICE_HOUSEHOLD_MACAW_YEARLY:-} + STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY: ${STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY:-} + STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_MONTHLY: ${STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_MONTHLY:-} + STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_YEARLY: ${STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_YEARLY:-} + STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW: ${STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW:-} + STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_MONTHLY: ${STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_MONTHLY:-} + STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_YEARLY: ${STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_YEARLY:-} STRIPE_CHECKOUT_SUCCESS_URL: ${STRIPE_CHECKOUT_SUCCESS_URL:-http://localhost:3000/?billing=success} STRIPE_CHECKOUT_CANCEL_URL: ${STRIPE_CHECKOUT_CANCEL_URL:-http://localhost:3000/?billing=cancelled} STRIPE_PORTAL_RETURN_URL: ${STRIPE_PORTAL_RETURN_URL:-http://localhost:3000/?billing=portal} diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 14e18fd..a1736a0 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -653,7 +653,7 @@ Request body: Notes: - `workspaceType` must be `standard` or `rescue` -- `billingPlan` may be `household_basic`, `household_plus`, or `household_macaw` +- `billingPlan` may be `household_basic`, `household_plus`, `household_macaw`, or `household_hyacinth_macaw` - rescue workspaces are forced to `rescue_free` Response `201`: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3d3b96c..ec1a70e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,7 +5,7 @@ import defaultBirdPhoto from './assets/yoda-default.png'; import { findParrotWeightReference, parrotSpeciesOptions, type ParrotWeightReference } from './parrotWeightReference'; import QRCode from 'qrcode'; -type BillingPlan = 'rescue_free' | 'household_basic' | 'household_plus' | 'household_macaw'; +type BillingPlan = 'rescue_free' | 'household_basic' | 'household_plus' | 'household_macaw' | 'household_hyacinth_macaw'; type HouseholdBillingPlan = Exclude; type BillingInterval = 'monthly' | 'yearly'; type WorkspaceType = 'standard' | 'rescue'; @@ -978,7 +978,7 @@ const oauthStartUrl = (providerKey: AuthProvider['providerKey']) => { }; const isHouseholdPlan = (billingPlan: BillingPlan): billingPlan is HouseholdBillingPlan => - billingPlan === 'household_basic' || billingPlan === 'household_plus' || billingPlan === 'household_macaw'; + billingPlan === 'household_basic' || billingPlan === 'household_plus' || billingPlan === 'household_macaw' || billingPlan === 'household_hyacinth_macaw'; const formatBillingIntervalName = (billingInterval: BillingInterval) => (billingInterval === 'yearly' ? 'Annual' : 'Monthly'); @@ -995,7 +995,11 @@ const formatBillingPlanName = (billingPlan: BillingPlan) => { return 'Indian Ringneck'; } - return 'Macaw'; + if (billingPlan === 'household_macaw') { + return 'African Grey'; + } + + return 'Hyacinth Macaw'; }; const formatBillingPlanCapacity = (billingPlan: BillingPlan) => { @@ -1008,10 +1012,14 @@ const formatBillingPlanCapacity = (billingPlan: BillingPlan) => { } if (billingPlan === 'household_plus') { - return 'Permits 5 to 10 birds in the flock.'; + return 'Permits 5 to 9 birds in the flock.'; } - return 'Permits 11 or more birds in the flock.'; + if (billingPlan === 'household_macaw') { + return 'Permits 11 to 16 birds in the flock.'; + } + + return 'Permits 17 or more birds in the flock.'; }; const formatBillingPlanDropdownLabel = (billingPlan: HouseholdBillingPlan) => { @@ -1020,10 +1028,14 @@ const formatBillingPlanDropdownLabel = (billingPlan: HouseholdBillingPlan) => { } if (billingPlan === 'household_plus') { - return 'Indian Ringneck (10 birds)'; + return 'Indian Ringneck (5-9 birds)'; } - return 'Macaw (11+)'; + if (billingPlan === 'household_macaw') { + return 'African Grey (11-16 birds)'; + } + + return 'Hyacinth Macaw (17+)'; }; const householdPlanPrices: Record> = { @@ -1039,6 +1051,10 @@ const householdPlanPrices: Record @@ -1050,11 +1066,15 @@ const formatBillingPlanBirdLimit = (billingPlan: BillingPlan) => { } if (billingPlan === 'household_plus') { - return '10'; + return '9'; } if (billingPlan === 'household_macaw') { - return '11+'; + return '16'; + } + + if (billingPlan === 'household_hyacinth_macaw') { + return '17+'; } return null; @@ -6432,8 +6452,9 @@ function App() { } > - - + + +