The unit count feeds Stripe — sized, not built
Lane I, part 3 of the onboarding wave 2 (flow map, chapter 7 and ledger row 4). Founders' standup 2026-09-17 — Sean: “the system should be feeding into Stripe — take the highest number of units for the month.” Parts 1 (#9320, merged) and 2 (#9333) are the checklist rows; this is what part 3 would be, read off the code on 2026-09-18.
What exists — read, not assumed
| Fact | Where |
|---|---|
The price model is per-unit quantity. Checkout creates the subscription as line_items: [{ price: STRIPE_PRO_PRICE_ID, quantity: totalUnits }] at $10/unit; staff get STRIPE_FOUNDER_PRICE_ID with quantity forced to 1. MRR on the staff billing page is unit_amount × quantity. | src/app/api/billing/checkout/route.ts, billing/stats/route.ts |
Nothing updates a quantity after checkout. Zero callers of subscriptions.update or subscriptionItems.update anywhere in src/ or scripts/. The Settings Billing card even promises it — “Unit count changed (N → M). Subscription will update at next renewal.” — and nothing makes that true. | git grep, OrganizationSettingsShell.tsx |
Nothing links a company to its Stripe customer. Organization.subscription (with stripeCustomerId) is declared and never written; the webhook writes the platform-wide AppSettings.subscription singleton (the leak /api/settings strips). Every billing route finds the customer by the signed-in user's email. Part 2's org-stripe-customer.ts reads the org row first and falls back to the admins' emails, and says so. | types.ts, billing/webhook/route.ts, stripe/client.ts |
Two unit counts disagree. Checkout counts getUnits() rows under the caller's property scope (non-test); the import writes Property.totalUnits from AppFolio's directory and never overwrites an existing value. Unit rows arrive through the rent-roll sync, not the picker's commit. | onboarding/pms/appfolio/import/route.ts:220, unit-count-gate.ts |
| No monthly high-water mark exists — no field, no job, no event. | — |
Stripe is live-mode everywhere. vercel env pull for development, preview and production: STRIPE_SECRET_KEY=sk_live… in all three; the stage mirror .env.local carries the same key. No sk_test key exists in the repo, in Vercel, or on the build machine. | vercel env pull, 2026-09-18 |
What it would take — one concern, four vertical slices
- A durable company → customer link. Checkout sets
metadata.organizationIdandclient_reference_id; the webhook writesOrganization.subscription(a new targeted writer,setOrganizationSubscription) instead of the singleton. ~120 lines. Part 2's reader already prefers this field, so nothing else changes. - One unit count per company.
countBillableUnits(orgId)=getUnitsrows over the company's non-test properties — the number checkout already bills on, so the two counts stop disagreeing. - The high-water mark.
Organization.billing.monthHighWater = { month: 'YYYY-MM', units }, raised (never lowered within a month) at the seams where the count changes: the onboarding import commit and the rent-roll sync. - The Stripe write. When the mark rises:
subscriptionItems.update(item, { quantity, proration_behavior: 'none' }), so the month's invoice bills the peak and the next month starts from the current count. Founder-price subscriptions excluded. Per-company flag, idempotent on the mark.
Roughly 300 lines across the four, each shippable alone and proved on a test-mode subscription before the flag is turned on for a company.
Why it is not built in this wave
- The required proof cannot be produced. Slice 4 is a money write on a third party. With only live keys, “proving” it means changing quantities on real customers' live subscriptions and watching the invoice. House rule: name the risk and wait.
- Slices 1–3 are safe and additive but inert without 4 — and slice 1 needs the webhook's Stripe event wiring to test, which is also live-only today.
- The founders' rule also needs one answer this page does not give: when the count drops mid-month, does the next month start from the new count immediately, or from the last invoice? The slice above assumes “current count at month start”; it is a sentence for Sean, not a design choice for a lane.
.env.local — Gera or Fede, from the Stripe dashboard. With it, part 3 is the four slices above, provable end to end on a test subscription, with the live flag off until a company is opted in.Sources: src/app/api/billing/{checkout,webhook,status,stats}/route.ts, src/lib/integrations/stripe/client.ts, src/lib/domain/billing/org-stripe-customer.ts (#9333), src/app/api/onboarding/pms/appfolio/import/route.ts, src/lib/domain/pms/unit-count-gate.ts; vercel env pull on 2026-09-18. Lane I ledger: #9320 (customer's checklist, merged), #9333 (billing row).