e970ab1ef3
No build, no migration, no Stripe object touched. Awaiting Kev on Q1-Q3. TWO EXPECTATIONS IN THE ORDER ARE WRONG: 1. G5 — users.founder_status is LIVE, not dead. Written by the webhook (stripeService.js:163), read and served by routes/stripe.js:95 as is_founder, and present in middleware/auth.js:24 PROFILE_COLUMNS so it loads on EVERY authenticated request. The guardrail says don't write it unless G5 proves it live — G5 proves it live, so A5 must NOT drop it. 2. THE TWO FOUNDER FLAGS ALREADY DISAGREE IN PROD: user_profiles.founder_pricing is true on 1 of 3 profiles while users.founder_status is true on 0 of 3. The webhook writes both from the same isFounder, so this is a dual-write that has already drifted. The build must pick one canonical flag and derive or retire the other; two independently-writable founder flags is how a founder loses their rate on one code path. GREPS: G1 founder_pricing has exactly one writer (the webhook mirror) and four readers (partners MRR attribution, the profile API, the profile badge). G2 the promo-code bypass is the ONLY founder gate today — getPriceId(tier, founderCode) against VALID_FOUNDER_CODES, stamped into metadata.is_founder, which the webhook then trusts, so a code alone mints a founder at any seat number. G3 the webhook DOES set tier + subscription_status=active + founder_pricing (closing an earlier CANNOT DETERMINE: a paid sub does flip the Build-1 gate) but stores NO stripe_subscription_id, confirming A1. G4 nexapay has ZERO code references and the column is empty, so A5's drop is evidence-supported as its own migration. G6 price selection is getPriceId -> line_items. DB VERIFIED: user_profiles has nexapay_customer_id and NO stripe_customer_id / stripe_subscription_id (A1 needed); users already carries stripe_customer_id; founder_pricing_seats is a VIEW; 3 profiles, 1 flagged founder. CANNOT DETERMINE: the four Stripe price IDs — no STRIPE_SECRET_KEY or STRIPE_PRICE_* in this environment, so I could not independently re-verify that the IDs in the order are what prod will charge. Since A3 would hardcode them, a typo becomes a permanent mis-charge; recommend reading them from env (already the pattern) with a boot assertion that all four resolve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
90 lines
5.7 KiB
Markdown
90 lines
5.7 KiB
Markdown
# BUILD 2 — REVIEW ZERO REPORT (no build; awaiting Kev on Q1-Q3)
|
|
2026-07-31. Nothing built, no migration applied, no Stripe object touched.
|
|
|
|
## 🔴 TWO ORDER EXPECTATIONS ARE WRONG — read before deciding Q1-Q3
|
|
|
|
### G5 — `users.founder_status` is **LIVE, NOT DEAD**. Do not drop it.
|
|
| where | what |
|
|
|---|---|
|
|
| `src/services/stripeService.js:163` | **WRITTEN** by the webhook: `founder_status: isFounder` |
|
|
| `src/routes/stripe.js:95` | **READ** and served: `is_founder: req.user.founder_status` |
|
|
| `src/middleware/auth.js:24` | in `PROFILE_COLUMNS` — **loaded on every authenticated request** |
|
|
| `src/middleware/auth.js:72` | defaulted to `false` on the fallback profile |
|
|
|
|
The guardrail says *"Don't write `users.founder_status` unless G5 proves it live."* **G5 proves it
|
|
live.** A5's "drop on evidence" must NOT run for this column.
|
|
|
|
### 🔴 AND THE TWO FOUNDER FLAGS ALREADY DISAGREE IN PROD
|
|
- `user_profiles.founder_pricing = true` on **1 of 3** profiles
|
|
- `users.founder_status = true` on **0 of 3**
|
|
|
|
The webhook writes BOTH from the same `isFounder` — so they are a **dual-write that has already
|
|
drifted**. Whatever is decided on Q3, the build must pick ONE canonical flag (the order says
|
|
`user_profiles.founder_pricing`) and make the other a derived read or explicitly retire it. Leaving
|
|
two independently-writable founder flags is how a founder loses their rate on one code path.
|
|
|
|
## THE GREPS
|
|
|
|
**G1 — `user_profiles.founder_pricing`**
|
|
- WRITE: `stripeService.js:173` (webhook mirror) — the only writer.
|
|
- READ: `routes/partners.js:68,92` (MRR attribution picks founder vs standard price);
|
|
`web/src/app/api/user/profile/route.ts:16`; `web/src/app/profile/page.tsx:17,121` (the badge).
|
|
- `routes/founders.js:6,9` documents that the counter deliberately does **not** trust this flag
|
|
(it counts Stripe subs instead) — *"a tier/founder_pricing field a profile can set"*.
|
|
|
|
**G2 — THE PROMO-CODE BYPASS IS THE ONLY FOUNDER GATE TODAY.**
|
|
`createCheckoutSession(userId, email, tier, founderCode)` (`stripeService.js:68`) →
|
|
`getPriceId(tier, founderCode)` → `isFounderCodeValid()` against `VALID_FOUNDER_CODES`
|
|
(`FOUNDER2026, VYNDR, BETONBLK, EARLYBIRD`) + `FOUNDER_EXPIRY 2026-12-31`. The result is stamped
|
|
into **`metadata: { user_id, tier, is_founder: String(isFounder) }`** (`:119`), and the webhook
|
|
**trusts that metadata** (`:155`). **So today a code alone mints a founder at any seat number, and
|
|
carries itself into the DB flag.** This is the bypass B1 must delete.
|
|
|
|
**G3 — the webhook DOES set the entitlement** (`stripeService.js:151-176`). It writes `users`
|
|
(`tier`, `stripe_customer_id`, `founder_status`) **and** mirrors to `user_profiles`
|
|
(`tier`, `subscription_status: 'active'`, `founder_pricing`).
|
|
**This closes an earlier CANNOT DETERMINE: a paid sub DOES flip the Build-1 gate.**
|
|
**But it stores NO `stripe_subscription_id` anywhere** — confirming A1 is required, since
|
|
`finalize_founder_slot` and grandfather reconciliation both key off it.
|
|
|
|
**G4 — `nexapay_customer_id`: ZERO code references** in `src/` or `web/src/`. The column exists on
|
|
`user_profiles` and is empty (0/3). **A5's drop is evidence-supported** — as its own migration.
|
|
|
|
**G6 — price selection**: `getPriceId` returns the founder PRICE_MAP entry when the code is valid,
|
|
else standing (falling back to the `PRICE_UNCONFIGURED` sentinel if unset); used at
|
|
`line_items: [{ price: priceId, quantity: 1 }]` (`:115`).
|
|
|
|
## DB FACTS (VERIFIED against zmdnczhtdxcddsxzttub)
|
|
- `user_profiles` columns: `id, email, tier, scan_count, scan_reset_date, subscription_start,
|
|
subscription_end, subscription_status, cancel_at_period_end, founder_pricing, age_verified,
|
|
**nexapay_customer_id**, created_at, updated_at, mfa_setup_prompted, grace_period_until,
|
|
partner_ref`. **Confirmed: NO `stripe_customer_id`, NO `stripe_subscription_id`** → A1 needed.
|
|
(Note `users` DOES already carry `stripe_customer_id` — `middleware/auth.js:24`.)
|
|
- `founder_pricing_seats` = **VIEW** ✓ decorative, as the order states.
|
|
- 3 profiles; 1 with `founder_pricing=true`.
|
|
|
|
## CANNOT DETERMINE
|
|
**The four Stripe price IDs.** `PRICE_MAP` is env-driven and there is **no `STRIPE_SECRET_KEY` and
|
|
no `STRIPE_PRICE_*` in this environment**, so I cannot confirm the four IDs in the order are the
|
|
ones prod will actually charge. The order says they were verified live this session — I am flagging
|
|
that I could not independently re-verify them, and A3 hardcodes them, so **a typo becomes a
|
|
permanent mis-charge**. Recommend the build read them from env (already the pattern) and assert at
|
|
boot that all four resolve, rather than hardcoding in SQL.
|
|
|
|
## THE THREE DECISIONS — WAITING ON KEV (no silent defaults)
|
|
**Q1 POOL** — global 100 vs 100-per-tier. *Bearing on the build:* it changes the UNIQUE index
|
|
(`UNIQUE(slot_number)` vs `UNIQUE(tier, slot_number)`) and whether founder-follows-upgrade needs a
|
|
second claim at all (global = the slot travels with the user; per-tier = a Desk slot must be claimed
|
|
separately and may be full).
|
|
**Q2 REOPEN** — reopen on cancel vs 100 lifetime seats. *Bearing:* decides whether
|
|
`release_expired_slots` also handles cancellation, and whether the public counter can ever go down.
|
|
**Q3 TEST RECORD** — the 1 `founder_pricing=true` desk profile with **0 Stripe subs**. Note it also
|
|
has **`users.founder_status = false`**, i.e. it is already inconsistent. My read: it is a test
|
|
artifact, not a subscriber. *Bearing:* seed slot #1 vs start clean and clear the flag.
|
|
|
|
## TAGS
|
|
VERIFIED: G1-G6 with file:line; `user_profiles`/`users` schema; the view; the flag divergence
|
|
(1 vs 0); the webhook does set the entitlement but stores no subscription id.
|
|
**CANNOT DETERMINE: the four Stripe price IDs (no key/env here).**
|
|
**BLOCKED: all of Phase A/B — awaiting Q1, Q2, Q3.**
|