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.

Verdict: report, not build. The price model is genuinely per-unit, so the ruling fits the plumbing — but nothing today changes a quantity after checkout, nothing links a company to its Stripe customer, no monthly high-water mark exists, and every environment's Stripe key is live-mode. The brief's own proof (“proven against a Stripe TEST-mode subscription”) is impossible until a test key exists, and building the write without that proof means adjusting real customers' live subscriptions blind. One unblock, named at the bottom.

What exists — read, not assumed

FactWhere
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

  1. A durable company → customer link. Checkout sets metadata.organizationId and client_reference_id; the webhook writes Organization.subscription (a new targeted writer, setOrganizationSubscription) instead of the singleton. ~120 lines. Part 2's reader already prefers this field, so nothing else changes.
  2. One unit count per company. countBillableUnits(orgId) = getUnits rows over the company's non-test properties — the number checkout already bills on, so the two counts stop disagreeing.
  3. 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.
  4. 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 one unblock. A Stripe test-mode secret key and a test price for the Pro plan, added to Vercel Development/Preview and the stage .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).

PropFlow Docs