17 Commits

Author SHA1 Message Date
blaisadmin c6dc5b22b8 Merge dev into main 2026-05-30 15:29:48 -04:00
blaisadmin d2763744eb Fixed build test
Deploy / deploy-dev (push) Successful in 2m35s
Deploy / deploy-prod (push) Has been skipped
2026-05-30 15:22:43 -04:00
blaisadmin 841d0a9669 Updated subscriptions
Deploy / deploy-dev (push) Failing after 4s
Deploy / deploy-prod (push) Has been skipped
2026-05-30 15:19:47 -04:00
blaisadmin b4f6193395 add codegraph
Deploy / deploy-dev (push) Successful in 1m23s
Deploy / deploy-prod (push) Has been skipped
2026-05-30 12:28:16 -04:00
blaisadmin 9ee46e53e0 Validate builds before dev deploy
Deploy / deploy-dev (push) Successful in 1m29s
Deploy / deploy-prod (push) Has been skipped
2026-05-24 21:56:56 -04:00
blaisadmin f16e88e2f0 Validate builds before deploy 2026-05-24 21:48:42 -04:00
blaisadmin 016bc187d4 Run deploy compose from host repo paths 2026-05-24 11:33:58 -04:00
blaisadmin 104f01f75d Use local mounts for production deploy workflow 2026-05-23 18:38:49 -04:00
blaisadmin 613b2c941c Use standalone deploy compose files
Deploy / deploy-dev (push) Successful in 10s
Deploy / deploy-prod (push) Has been skipped
2026-05-23 18:34:06 -04:00
blaisadmin 3ab3f48f19 Use environment compose files in deploy workflow
Deploy / deploy-dev (push) Successful in 1m25s
Deploy / deploy-prod (push) Has been skipped
2026-05-23 18:31:00 -04:00
blaisadmin d9822e6626 Use host deploy paths for job container mounts
Deploy / deploy-dev (push) Successful in 40s
Deploy / deploy-prod (push) Has been skipped
2026-05-23 18:27:00 -04:00
blaisadmin 9e92e1212a Avoid duplicate Docker socket mount in deploy workflow
Deploy / deploy-prod (push) Has been skipped
Deploy / deploy-dev (push) Failing after 13m0s
2026-05-23 18:00:30 -04:00
blaisadmin 96bc76ef0d Mount deploy paths in Gitea job containers
Deploy / deploy-dev (push) Failing after 1s
Deploy / deploy-prod (push) Has been skipped
2026-05-23 17:57:44 -04:00
blaisadmin fe4e69ceb5 Use local mounts for Gitea deploy workflow
Deploy / deploy-dev (push) Failing after 3s
Deploy / deploy-prod (push) Has been skipped
2026-05-23 17:51:33 -04:00
blaisadmin c09e7f63ce Force deploy workflow to use configured SSH key
Deploy / deploy-prod (push) Has been skipped
Deploy / deploy-dev (push) Failing after 3s
2026-05-23 16:47:42 -04:00
blaisadmin 306e3a8c85 Add Gitea deploy workflow
Deploy / deploy-dev (push) Failing after 1m6s
Deploy / deploy-prod (push) Has been skipped
2026-05-23 16:35:11 -04:00
blaisadmin 568aee3e70 Adding gitea action 2026-05-23 16:32:59 -04:00
10 changed files with 189 additions and 34 deletions
+16
View File
@@ -0,0 +1,16 @@
# CodeGraph data files
# These are local to each machine and should not be committed
# Database
*.db
*.db-wal
*.db-shm
# Cache
cache/
# Logs
*.log
# Hook markers
.dirty
+6 -3
View File
@@ -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
+87
View File
@@ -0,0 +1,87 @@
name: Deploy
on:
push:
branches:
- dev
- develop
workflow_dispatch:
jobs:
deploy-dev:
if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
container:
volumes:
- /docker/FlockPal-dev:/docker/FlockPal-dev
steps:
- name: Update dev checkout
run: |
set -e
cd /docker/FlockPal-dev
git fetch --all --prune
git reset --hard "origin/${{ github.ref_name }}"
git clean -fd
- name: Validate backend
run: |
set -e
cd /docker/FlockPal-dev
docker run --rm -v "$PWD/backend:/src:ro" -w /tmp/app node:22-alpine sh -lc "cp -a /src/. /tmp/app && npm ci && npm run build && npm test"
- name: Validate frontend
run: |
set -e
cd /docker/FlockPal-dev
docker run --rm -v "$PWD/frontend:/src:ro" -w /tmp/app node:22-alpine sh -lc "cp -a /src/. /tmp/app && npm ci && npm run build"
- name: Validate dev compose config
run: |
set -e
cd /docker/FlockPal-dev
docker compose -f docker-compose.dev.yaml config --quiet
- name: Deploy dev
run: |
set -e
cd /docker/FlockPal-dev
docker compose -f docker-compose.dev.yaml up -d --build
deploy-prod:
if: ${{ github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
container:
volumes:
- /docker/FlockPal:/docker/FlockPal
steps:
- name: Update prod checkout
run: |
set -e
cd /docker/FlockPal
git fetch --all --prune
git reset --hard "origin/${{ github.ref_name }}"
git clean -fd
- name: Validate backend
run: |
set -e
cd /docker/FlockPal
docker run --rm -v "$PWD/backend:/src:ro" -w /tmp/app node:22-alpine sh -lc "cp -a /src/. /tmp/app && npm ci && npm run build && npm test"
- name: Validate frontend
run: |
set -e
cd /docker/FlockPal
docker run --rm -v "$PWD/frontend:/src:ro" -w /tmp/app node:22-alpine sh -lc "cp -a /src/. /tmp/app && npm ci && npm run build"
- name: Validate prod compose config
run: |
set -e
cd /docker/FlockPal
docker compose -f docker-compose.prod.yml config --quiet
- name: Deploy prod
run: |
set -e
cd /docker/FlockPal
docker compose -f docker-compose.prod.yml up -d --build
+5 -3
View File
@@ -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:
+26 -7
View File
@@ -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<Exclude<BillingPlan, '
yearly: process.env.STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_YEARLY?.trim() ?? '',
},
household_macaw: {
monthly: process.env.STRIPE_PRICE_HOUSEHOLD_MACAW_MONTHLY?.trim() || process.env.STRIPE_PRICE_HOUSEHOLD_MACAW?.trim() || '',
yearly: process.env.STRIPE_PRICE_HOUSEHOLD_MACAW_YEARLY?.trim() ?? '',
monthly:
process.env.STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_MONTHLY?.trim() ||
process.env.STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY?.trim() ||
'',
yearly: process.env.STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_YEARLY?.trim() ?? '',
},
household_hyacinth_macaw: {
monthly:
process.env.STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_MONTHLY?.trim() ||
process.env.STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW?.trim() ||
'',
yearly: process.env.STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_YEARLY?.trim() ?? '',
},
};
const stripePriceEnvNamesByBillingPlanAndInterval: Record<Exclude<BillingPlan, 'rescue_free'>, Record<BillingInterval, string[]>> = {
@@ -454,14 +468,19 @@ const stripePriceEnvNamesByBillingPlanAndInterval: Record<Exclude<BillingPlan, '
yearly: ['STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK_YEARLY'],
},
household_macaw: {
monthly: ['STRIPE_PRICE_HOUSEHOLD_MACAW_MONTHLY', 'STRIPE_PRICE_HOUSEHOLD_MACAW'],
yearly: ['STRIPE_PRICE_HOUSEHOLD_MACAW_YEARLY'],
monthly: ['STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_MONTHLY', 'STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY'],
yearly: ['STRIPE_PRICE_HOUSEHOLD_AFRICAN_GREY_YEARLY'],
},
household_hyacinth_macaw: {
monthly: ['STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_MONTHLY', 'STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW'],
yearly: ['STRIPE_PRICE_HOUSEHOLD_HYACINTH_MACAW_YEARLY'],
},
};
const stripePricePlanLabels: Record<Exclude<BillingPlan, 'rescue_free'>, 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(
+1 -1
View File
@@ -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';
+6 -3
View File
@@ -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}
+6 -3
View File
@@ -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}
+1 -1
View File
@@ -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`:
+35 -13
View File
@@ -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<BillingPlan, 'rescue_free'>;
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<HouseholdBillingPlan, Record<BillingInterval, string>> = {
@@ -1039,6 +1051,10 @@ const householdPlanPrices: Record<HouseholdBillingPlan, Record<BillingInterval,
monthly: '$15.99/month',
yearly: '$160/year',
},
household_hyacinth_macaw: {
monthly: '$49.99/month',
yearly: '$500/year',
},
};
const formatBillingIntervalDropdownLabel = (billingPlan: HouseholdBillingPlan, billingInterval: BillingInterval) =>
@@ -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() {
}
>
<option value="household_basic">Conure (4 birds)</option>
<option value="household_plus">Indian Ringneck (10 birds)</option>
<option value="household_macaw">Macaw (11+)</option>
<option value="household_plus">Indian Ringneck (5-9 birds)</option>
<option value="household_macaw">African Grey (11-16 birds)</option>
<option value="household_hyacinth_macaw">Hyacinth Macaw (17+)</option>
</select>
</label>
<label>
@@ -6570,8 +6591,9 @@ function App() {
}
>
<option value="household_basic">Conure (4 birds)</option>
<option value="household_plus">Indian Ringneck (10 birds)</option>
<option value="household_macaw">Macaw (11+)</option>
<option value="household_plus">Indian Ringneck (5-9 birds)</option>
<option value="household_macaw">African Grey (11-16 birds)</option>
<option value="household_hyacinth_macaw">Hyacinth Macaw (17+)</option>
</select>
</label>
<label>