From a4c0058f8c3d9612f1d30508626fa5be00431fa7 Mon Sep 17 00:00:00 2001 From: ced4568 Date: Fri, 28 Aug 2026 09:47:11 -0500 Subject: [PATCH] docs(stripe): document real Stripe TEST-mode validation results PHASE9_1_STRIPE_TEST_VALIDATION.md rewritten from BLOCKED to the real, proven results: a real Stripe TEST Checkout, a real TEST payment at the founder-approved $399 Founding Cohort price, a genuine signed webhook, the existing registration->enrollment conversion, a real duplicate-delivery proof, a real automated abandoned-checkout proof, a real refund with the non-cascade rule reconfirmed, full Stripe<->RootNode reconciliation IDs, and the one real defect found and fixed. STRIPE_INTEGRATION_RUNBOOK.md updated (real-Stripe local testing is no longer 'not yet exercised'; notes the no-sudo Stripe CLI install path and --api-key/--print-secret usage). WINTER_BREAK_LAUNCH_CHECKLIST.md: Stripe TEST validation checked, price checked ($399 launch / $449 retail per Chase directly), live-mode items correctly still unchecked. PHASE9_COMMERCE_AND_LAUNCH.md's Phase 9.1 addendum updated to reflect completion. --- docs/PHASE9_1_STRIPE_TEST_VALIDATION.md | 461 +++++++++++++++++------- docs/PHASE9_COMMERCE_AND_LAUNCH.md | 15 +- docs/STRIPE_INTEGRATION_RUNBOOK.md | 34 +- docs/WINTER_BREAK_LAUNCH_CHECKLIST.md | 22 +- 4 files changed, 366 insertions(+), 166 deletions(-) diff --git a/docs/PHASE9_1_STRIPE_TEST_VALIDATION.md b/docs/PHASE9_1_STRIPE_TEST_VALIDATION.md index 081509e..cf7f94d 100644 --- a/docs/PHASE9_1_STRIPE_TEST_VALIDATION.md +++ b/docs/PHASE9_1_STRIPE_TEST_VALIDATION.md @@ -1,180 +1,363 @@ # Phase 9.1 — Real Stripe TEST-Mode Integration Validation -Status: **BLOCKED — no Stripe TEST credentials are available to this session.** This document -records exactly what was verified without them, exactly what Chase needs to provide, and the -permanent, opt-in validation script that will run the real proof the moment those credentials -exist. No live money was risked; no Stripe network call of any kind was made during this phase. +Status: **VALIDATED.** A real Stripe TEST Checkout Session, a real Stripe TEST payment ($399.00 +— the founder-approved Winter Break Founding Cohort tuition), a genuine Stripe-signed webhook, +and the existing registration→enrollment conversion path were all proven end to end against +Stripe's real TEST-mode API — no live money, no live-mode credential, nothing simulated. This +document also records the first, credential-less attempt at this phase (which correctly stopped +rather than fake anything) and a real defect the live traffic exposed and fixed. ## 1. Baseline verification -- Branch: `main`. Working tree: clean at start. -- `git fetch origin` — HEAD and `origin/main` both `80a5f6e4576cb9b83a60c9e9b57e4ebc52b62f07`, - exactly the Phase 9 milestone commit. -- Phase 9 payment code and documentation inspected in full: `payment-provider.js`, - `commerce.js`, `webhook-routes.js`, `offerings.js`'s registration/conversion path, - `admin-offerings-routes.js`'s payment routes, `integration-routes.js`'s checkout/agreement/ - status routes, `platform/test/e2e-phase9.js`, `infra/scripts/drills/ - test-phase9-winter-camp-commerce.js`, `docs/STRIPE_INTEGRATION_RUNBOOK.md`, `docs/ - WINTER_BREAK_LAUNCH_CHECKLIST.md`. Nothing was changed as a result of this review — no defect - was found, and none was invented to justify a change (§1/§22's own instruction). +- First attempt (credential-less): branch `main`, clean tree, HEAD = `origin/main` = + `80a5f6e4576cb9b83a60c9e9b57e4ebc52b62f07` — the Phase 9 milestone. +- Resume attempt (this run): `git fetch origin` confirmed HEAD = `origin/main` = + `77fd1869dbe694a7ff323a6fac07382e1f55b225` — the prior, credential-less Phase 9.1 commit. +- No `docs/ROOTNODE_PRICING_AND_OFFERS_V1.md` or any "Phase 9.5" commit exists anywhere in this + repository's history, branches, or tags (verified via `git log --all`) — the pricing decision + below came directly from Chase in this session, not from a document that turned out not to + have reached this canonical repo yet (it was apparently done on a different machine and not + yet pushed). Used directly; no invented business decision was needed to proceed. +- Phase 9 payment code and documentation re-inspected: `payment-provider.js`, `commerce.js`, + `webhook-routes.js`, `offerings.js`'s conversion path, `admin-offerings-routes.js`'s payment + routes, `integration-routes.js`'s checkout/agreement/status routes, `platform/test/ + e2e-phase9.js`, `infra/scripts/drills/test-phase9-winter-camp-commerce.js`. -## 2. Stripe TEST credential search — none found +## 2. Stripe TEST credential status -Searched, in order: process environment (`env | grep -i stripe`), every `.env`/`.env.example` -file reachable from this repository and the operator's home directory, this repository's own -Infisical project wiring (none exists — no `.infisical.json` in this repo), and this session's -own memory of prior credential-provisioning work (none references Stripe or this project). No -`STRIPE_SECRET_KEY`, no `STRIPE_WEBHOOK_SECRET`, in any test or live form, exists anywhere this -session can reach. +**First attempt**: none found anywhere reachable (process env, every `.env` file, this repo's +Infisical wiring, session memory) — correctly stopped and documented rather than faked (see git +history for that version of this document). -**This is exactly STOP condition 1/2 from this phase's own authorization.** Per §2/§27: do not -fake validation, stop after documenting exactly what's needed. +**This run**: Chase provided a real `sk_test_...` secret key directly. Format-verified as TEST +mode (never printed in full anywhere in this document, any commit, or any persisted log — only +an 8-character prefix `sk_test_` ever appears). Stored in `platform/.env` (gitignored, never +committed — confirmed via `git status` throughout this phase). A push to Infisical for durable +storage was attempted (per Chase's request) but the existing machine identity credential at +`~/.config/infisical/identity.env` returned a consistent `401 Invalid credentials` from +`app.infisical.com` across three attempts — a pre-existing, unrelated credential-store problem, +not something fixable from this session. The Stripe key remains safely local in the meantime. -### What Chase needs to provide or configure - -1. A Stripe account (test mode is available on any account, including one with no live - business verification completed yet). -2. From the Stripe Dashboard, in **Test mode**: the test **Secret key** (`sk_test_...`) → - `STRIPE_SECRET_KEY`. -3. A webhook signing secret for a test endpoint — either a permanent test-mode webhook endpoint - configured in the dashboard, or the ephemeral one the Stripe CLI prints when running - `stripe listen --forward-to /api/webhooks/payment` → `STRIPE_WEBHOOK_SECRET`. Full - setup steps already documented in `docs/STRIPE_INTEGRATION_RUNBOOK.md`'s "Local testing - against real Stripe TEST mode" section (written during Phase 9, unchanged and still accurate). -4. Both values set as environment configuration on the running Platform API process - (`PAYMENT_PROVIDER=stripe` alongside them) — never committed to git, never placed in any - browser-reachable configuration. **No credential value appears anywhere in this document, any - commit, or any log produced during this phase.** - -Once those exist, run the permanent validation script this phase adds (§4 below) to produce the -actual proof this phase was asked to deliver. +The webhook signing secret was obtained via `stripe listen --api-key ... --forward-to +http://localhost:4200/api/webhooks/payment --print-secret` (the Stripe CLI, installed to +`~/.local/bin/stripe` this session for exactly this purpose — no Dashboard endpoint needed for +local TEST-mode validation). ## 3. Stripe adapter architecture — confirmed, unchanged -Inspected `payment-provider.js`'s `StripeProvider.createCheckoutSession()`: it uses **dynamic -Checkout line-item `price_data`** (`unit_amount`/`currency`/`product_data.name` supplied inline -per session), **not** pre-created Stripe Price/Product IDs. RootNode's `offerings` table remains -the sole source of truth for identity, price, and currency; Stripe never becomes a second -business-domain model — no Stripe object id is ever treated as anything other than an opaque -`provider_reference`. This matches §3's explicit constraint and required no change. +`StripeProvider.createCheckoutSession()` uses **dynamic Checkout line-item `price_data`**, never +pre-created Stripe Price/Product IDs. RootNode's `offerings` table remains the sole source of +truth for identity, price, and currency. Confirmed still true after this phase's fix (below) — +the fix only changes how a LATER webhook event is correlated back to a transaction, not how the +Checkout Session itself is created. -## 4. Permanent opt-in Stripe integration test — added +## 4. Disposable TEST offering used -`platform/test/stripe-integration-test.js` (new). Default behavior (no environment variable -set): prints a skip message and exits `0` with **zero Stripe network traffic** — confirmed by -running it with no configuration at all. **Verified this run**: +A clearly-labeled, disposable offering — `[TEST] Stripe Integration Check ` — +`delivery_mode: cohort`, price **39900 (cents) / usd = $399.00**, the founder-approved Winter +Break "Founding Cohort" launch tuition Chase gave directly in this session (standard/reference +retail $449, paid in full, no deposit, no payment plan — none of that billing structure was +built, only the flat price was used for this validation). **Not** the real Winter Break offering +— a separate, disposable fixture, transitioned to `cancelled` at the end of the run (confirmed). + +## 5. Registration result + +A real **guardian + minor** registration (the more complex path, preferred per the mission spec +since the Winter Break audience may include minors) via +`POST /api/integrations/registrations` — guardian `display_name`/email/phone real-shaped test +data, registrant `is_minor: true`, no real child PII. Registration created, seat reserved, +payment status tracked separately from registration status, no enrollment existed before payment +— all as designed. + +## 6. Agreement gate result + +Checkout attempted before agreement acceptance → `409 AGREEMENTS_INCOMPLETE`. Agreement then +accepted via `POST /api/integrations/registrations/:id/agreements` → `201`, `accepted_version: +1` persisted. Checkout attempted again → succeeded. The real, live enforcement path, not +bypassed. + +## 7. Real Stripe TEST Checkout Session result + +`POST /api/integrations/registrations/:id/checkout` returned a real +`https://checkout.stripe.com/...` URL. Independently retrieved via **Stripe's own** +`checkout.sessions.retrieve` API (never trusting RootNode's own echo): ``` -$ node test/stripe-integration-test.js -Stripe integration test skipped (RUN_STRIPE_INTEGRATION_TEST is not "true") — no Stripe network traffic, no credentials required. +Stripe session cs_test_a1VBoHHATcDr3DRTnZixQX8E0E5Ulk3H1UAKwTYVEOYVboOcaKCuZEMuff: + amount_total = 39900, currency = usd, mode = payment, livemode = false ``` -Enabled via `RUN_STRIPE_INTEGRATION_TEST=true`, reusing the exact `STRIPE_SECRET_KEY`/ -`STRIPE_WEBHOOK_SECRET`-adjacent naming Phase 9 already established (only `STRIPE_SECRET_KEY` is -actually needed by the script itself, to independently query Stripe's own API — the webhook -secret only needs to be configured on the server process, not read by this script). Guard -clauses verified directly, without needing real credentials: +## 8. Server-authoritative amount verification -| Condition | Verified behavior | +The checkout request was sent with a tampered body (`amount: 1, currency: "eur"` — neither field +exists on the real contract, sent anyway to prove they're inert). Stripe's own recorded session +still shows `amount_total: 39900, currency: usd` — the real, configured offering price, not the +caller-supplied value. **The caller cannot choose the price or currency.** + +## 9. Real TEST payment result + +Chase completed the real Stripe-hosted TEST Checkout in a browser with Stripe's official test +card (4242 4242 4242 4242). Independently confirmed via Stripe's own API (not just the +redirect): + +``` +GET /v1/checkout/sessions/cs_test_a1VB... → status: complete, payment_status: paid, + payment_intent: pi_3U9QuqPl0OwZKsjY0lprOPWi +``` + +## 10. Real signed webhook result + +`stripe listen` relayed a genuine Stripe-signed `checkout.session.completed` event +(`evt_1U9QutPl0OwZKsjYSkW8Le84`) to `POST /api/webhooks/payment`, which accepted it (`200`, +`{"received":true,"result":"processed"}`). This is Stripe's own real signature over Stripe's own +real event body — not a locally-fabricated one. + +## 11. Invalid-signature result + +A garbage `Stripe-Signature` header against a fabricated body → `400 invalid_signature`. The +registration's status was re-read before and after and confirmed unchanged — zero domain side +effect from a rejected signature. + +## 12. Payment transaction result + +Exactly one `payment_transactions` row for this registration, `status: paid`, +`provider_reference` matching the real Checkout Session id. No `card_number`/`cvv`/`card_last4`/ +`payment_method_details` field exists anywhere on the record (none is ever stored, by design). + +## 13. Registration confirmation result + +`offering_registrations.status = enrollment_activated`, server-derived entirely from the real +payment webhook — never a client action. + +## 14. Enrollment conversion result + +The **existing, unmodified** `convertRegistrationToEnrollment()` path ran — no second +implementation was created. Verified directly against the database: exactly one row in +`enrollments` for this student/program. + +## 15. Cohort/capacity result + +Exactly one row in `cohort_enrollments` for this student/cohort — the offering's `cohort_id` +correctly produced real classroom membership on conversion, unchanged from Phase 8/9's design. + +## 16. Guardian preservation result + +Exactly one row in `student_guardians` for this student (the Phase 9 guardian-linking fix from +the original Phase 9 phase held under real Stripe traffic too). `students.email` for the minor +remains the empty string — the guardian's email was never written into the student's own +identity record. + +## 17. Activation/notification result + +The real activation notification was queued and delivered through the existing dev-preview +mechanism (the same one every prior phase's testing has used — `EMAIL_PROVIDER` was left at its +dev default this run; Google Workspace live delivery was not exercised, since nothing about +Stripe validation requires it and the mission explicitly said not to make GWS delivery a Stripe +blocker). No second, Stripe-specific notification path exists — this reuses the exact +`notifyForEnrollment` flow every other enrollment trigger already uses. + +## 18. Student access result + +The real one-time activation token worked: `POST /api/student/activate` → `200`, and the newly +activated student's `GET /api/student/programs` includes the enrolled program. One access proof, +as instructed — no full curriculum rehearsal was repeated (Phase 8/9's own rehearsals already +cover that in depth). + +## 19. Duplicate-event/idempotency result + +The **real** Stripe event was fetched fresh via `stripe.events.retrieve(eventId)` and re-signed +locally with the real webhook secret (`stripe.webhooks.generateTestHeaderString`) — never a +fabricated "payment succeeded" event for an unpaid session, but a genuine replay of the exact +event Stripe itself recorded, mirroring what `stripe events resend` does server-side (not usable +here directly — an ephemeral `stripe listen` session has no persistent dashboard endpoint id to +target). Result: `200`, `{"result":"duplicate"}`. Confirmed via direct query: still exactly one +enrollment, still exactly one activation/confirmation notification — no double conversion, no +double seat consumption, no double email. + +## 20. Declined-payment result + +**Not exercised via an actual real declined Stripe TEST payment this session** — doing so would +require a second full browser-driven Checkout completion (Stripe Checkout Sessions have no +supported pure-API "decline" trigger; PaymentIntents-direct-confirm would mean not using +Checkout Sessions at all, which this phase was told not to redesign toward). Phase 9's +deterministic suite already proves "failed payment retains the seat" directly, and this phase's +own new regression test (§28) proves the exact real-Stripe reference-correlation shape a genuine +`payment_intent.payment_failed` event would use. Available on request if Chase wants one more +manual decline-card checkout to close this specific gap with fully real traffic. + +## 21. Abandoned-checkout result + +Fully automated, no browser needed: a second, separate registration's Checkout Session was +created normally, then expired via Stripe's own `sessions.expire()` API (a real, supported, +pure-API operation — no fabricated event). Stripe itself then fired a genuine +`checkout.session.expired` webhook, relayed by `stripe listen` and accepted (`200`). Confirmed: +the registration's status became `cancelled` and its seat was released exactly once, within the +polling window. + +## 22. Refund result + +`stripe.refunds.create({ payment_intent })` against the real PaymentIntent → `succeeded` +(`re_3U9QuqPl0OwZKsjY0IYvfOsM`, `39900 usd`). RootNode's own refund state was confirmed +(`status: refunded`). The non-cascade rule was reconfirmed directly against the database: the +registration stayed `enrollment_activated` and the enrollment stayed `active` — a refund does +**not** automatically revoke registration or academic access. Whether it *should*, for this +specific offering/policy, remains an explicit business-policy question this phase does not +decide in code (per Phase 9's own design and this phase's own instruction). + +## 23. Stripe ↔ RootNode reconciliation + +| | | |---|---| -| `OWNER_EMAIL`/`OWNER_PASSWORD` unset | Exits `1` with a clear message. | -| `STRIPE_SECRET_KEY` unset | Exits `1`, names the exact variable needed. | -| `STRIPE_SECRET_KEY` starts with `sk_live_` | **Refuses to run** — "TEST MODE ONLY" — exits `1`. | -| `STRIPE_SECRET_KEY` any other non-test shape | Refuses to run out of caution — exits `1`. | -| A syntactically valid `sk_test_...` value, no real stack running | Reaches `main()` cleanly, fails only on the expected connection error — confirms no syntax defect in the enabled code path. | +| RootNode registration | `04772e72-4a09-4afd-8032-d6ef4d132375` | +| RootNode enrollment | `10e2724a-43bb-4d35-8bea-98f600cbd8e9` | +| RootNode student | `student-001` | +| Stripe Checkout Session | `cs_test_a1VBoHHATcDr3DRTnZixQX8E0E5Ulk3H1UAKwTYVEOYVboOcaKCuZEMuff` | +| Stripe PaymentIntent | `pi_3U9QuqPl0OwZKsjY0lprOPWi` | +| Stripe webhook event | `evt_1U9QutPl0OwZKsjYSkW8Le84` | +| Stripe refund | `re_3U9QuqPl0OwZKsjY0IYvfOsM` | +| Amount / currency | `39900` / `usd` ($399.00) | -When real test credentials exist and the dev stack is running with `PAYMENT_PROVIDER=stripe`, -this script will automatically and honestly prove, via pure API calls (no browser needed): +No secret key, webhook secret, card number, or CVV appears above or anywhere in this document. -- RootNode creates a **real** Stripe TEST Checkout Session (not RootNode's own claim — verified - by retrieving the session directly from Stripe's own `checkout.sessions.retrieve` API). -- The amount/currency Stripe actually recorded matches the offering's server-side price exactly, - even when the request is sent with a tampered `amount`/`currency` in the body — proving - server-authoritative pricing against **Stripe's own object**, the strongest form of that proof. -- `session.livemode === false` is asserted directly — the script itself refuses to consider its - own run valid against anything but a real TEST-mode session. -- RootNode's webhook route rejects an invalid/garbage signature cleanly, with **zero domain side - effect** (the registration's status is re-read and asserted unchanged). +## 24. Stripe Dashboard reconciliation -What it deliberately does **not** automate: actually completing Stripe's hosted Checkout page. -There is no supported pure-API way to "pay" a Checkout Session, and fabricating a signed -"payment succeeded" webhook for a session nobody actually paid would itself be exactly the kind -of faked validation this phase was told never to produce. Instead, the script prints the real -checkout URL and polls RootNode's own status endpoint for a bounded window (default 90s, -`STRIPE_INTEGRATION_TEST_WAIT_SECONDS`) while a human completes payment in a browser (Stripe's -test card `4242 4242 4242 4242`) or a parallel `stripe listen --forward-to` relays Stripe's real -event — see `docs/STRIPE_INTEGRATION_RUNBOOK.md`. If nothing arrives in that window (expected in -an unattended run), it says so plainly rather than reporting a false pass. +For a real transaction, Chase can locate it in the Stripe Dashboard under **Test mode → +Payments** (search by the PaymentIntent id `pi_...` or the Checkout Session id `cs_test_...` +above) or **Developers → Events** (search by the event id `evt_...`). The RootNode admin UI's +own Payments card (on the registration detail view) shows the matching `provider_reference` +side by side with RootNode's own status/timestamps — reconciliation is possible without either +system exposing credentials or card data to the other. -Never runs as part of normal regression suites or CI — no other test file requires or imports it, -and its own default is a guaranteed no-op. +## 25. Security revalidation (with real Stripe enabled) -## 5. What was actually proven this phase - -| Step from the mission spec | Result | +| Check | Result | |---|---| -| 1. Baseline verification | Done — see §1. | -| 2. Stripe TEST-mode safety | No live-mode action was possible or attempted; no credentials of any kind exist. | -| 3. Stripe architecture review | Done — dynamic `price_data`, confirmed, unchanged (§3). | -| 4-20 (disposable offering through security revalidation, all requiring a real Stripe TEST payment/webhook round trip) | **Not performed — blocked on missing credentials.** Faking any of these was explicitly disallowed. | -| 21. Permanent opt-in integration test | **Built** (§4) — the automatable portion is real and will run the moment credentials exist; the human/CLI-dependent portion is honestly deferred, not faked. | -| 22. Application changes | None made — no genuine defect was exposed (none could be, without real Stripe traffic), so §22's "smallest correct fix" rule had nothing to apply to. | -| 23. Regression | No registration/agreement/enrollment/notification/frontend code changed this phase, so per §23's own conditional rule, no regression rerun was required. The new test file's default no-op path was verified directly (§4) instead. | -| 24. Documentation | This document, plus the update noted below. | -| 25. Live-mode readiness checklist | See §6. | +| Stripe secret key never reaches browser | Server-only env var; not referenced anywhere in `frontend/`. | +| Webhook signing secret never reaches browser | Same. | +| Caller cannot choose price | Proven directly (§8) against Stripe's own recorded amount. | +| Caller cannot choose currency | Proven directly (§8), same test. | +| Caller cannot bypass required agreements | Proven directly (§6). | +| Caller cannot bypass capacity | Unchanged architecture; existing `e2e-phase9.js` last-seat race regression rerun clean (§27). | +| Success redirect cannot forge payment | The script never trusted the redirect — every "paid" claim in this document is independently verified via Stripe's own API or the real webhook, not the URL. | +| Invalid webhook signature rejected | Proven directly (§11). | +| Duplicate webhook safe | Proven directly with a REAL re-signed event (§19). | +| No card number enters RootNode DB | Proven directly (§12) — no such field exists. | +| No card number enters RootNode logs | Confirmed — grepped this session's own logs/scratch files for the real secret values and any card-shaped pattern; none found. | +| No Stripe secret enters audit log | Existing `e2e-phase9.js` hygiene test rerun clean (§27). | +| No Stripe secret enters metrics | Same. | +| Stripe provider IDs not high-cardinality metric labels | Unchanged architecture — metrics still use only fixed-enum labels. | +| One student cannot inspect another's payment | Structural — payment routes are admin/owner-only; students have no route to any payment data at all. | +| Integration shared secret remains server-to-server only | Unchanged — never read from any browser-facing code. | -## 6. Live-mode readiness checklist +## 26. Bugs found -Everything Stripe-side is still pending exactly as `docs/STRIPE_INTEGRATION_RUNBOOK.md`'s "Going -live" section already lays out, restated here as a checklist per §25: +**One real defect**, found only because real Stripe traffic was exercised: `payment_intent. +payment_failed` and `charge.refunded` webhook events reference Stripe's PaymentIntent/Charge +object, never the Checkout Session — but `payment_transactions.provider_reference` only ever +stored the Session id. A real Stripe-issued failure or refund event would have silently failed +to match any transaction (`result: 'error', reason: 'UNKNOWN_TRANSACTION'`). Phase 9's +deterministic TestProvider never exposed this because its fabricated events always reuse +whatever reference the calling test explicitly supplies, never distinguishing real Stripe object +id shapes. -**Stripe:** -- [ ] Business/account verification completed in the Stripe dashboard. -- [ ] Live API secret key (`sk_live_...`) generated. -- [ ] A permanent live-mode webhook endpoint created, pointed at the production API's - `/api/webhooks/payment` URL, and its signing secret (`whsec_...`) captured. -- [ ] Payout/bank details configured in Stripe. -- [ ] Business/support information (statement descriptor, support contact) configured in Stripe. +## 27. Bugs fixed -**RootNode:** -- [ ] A real, HTTPS production URL for the Platform API (Stripe requires HTTPS for live webhooks). -- [ ] `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` (live values) provisioned into production - secret configuration only. -- [ ] Production deployment itself (explicitly out of scope for this phase and Phase 9 — - unchanged status). -- [ ] Backup/monitoring in place for the production database holding `payment_transactions`. -- [ ] The real Winter Break offering configured with its real price, once decided. +Smallest correct fix, matching Stripe's own documented pattern for this exact problem: -**Business** (Chase's decisions, not invented here): final price, refund/cancellation policy, -camp dates, venue, capacity — all still open, tracked in `docs/ -WINTER_BREAK_LAUNCH_CHECKLIST.md`. +- `payment-provider.js`: `StripeProvider.createCheckoutSession()` now also sets + `payment_intent_data: { metadata }` at session creation, so the SAME metadata (which already + carries our own `transaction_id`) propagates onto the PaymentIntent Stripe creates — not just + the Session. +- `normalizeEvent()` (now `async`, one extra API call only on the rare refund path): for + `payment_intent.payment_failed`, reads `obj.metadata.transaction_id` directly (the event + object IS the PaymentIntent). For `charge.refunded`, retrieves the PaymentIntent + (`stripe.paymentIntents.retrieve`) since Charges do not inherit PaymentIntent metadata + automatically, and reads `transaction_id` from there. +- `commerce.js`: the transaction lookup now matches `provider_reference = $1 OR + transaction_id::text = $1` — a single additional `OR` clause, no schema/migration change. +- `webhook-routes.js`: added the now-required `await` on `verifyAndParseWebhook` (the + deterministic `TestProvider`'s synchronous return is unaffected — `await` on a non-promise + value resolves immediately). -**Legal** (unchanged from every prior phase's own list): the actual agreement/waiver text, -guardian/minor consent review, privacy/retention requirements — see `docs/ -STUDENT_GUARDIAN_DATA_MODEL.md`. +Verified fixed: the real Stripe refund in this session (§22) correctly resolved via this new +path (the refund's `charge.refunded` webhook references the PaymentIntent, exactly the +previously-broken case), and a new permanent deterministic regression test (§28) proves the +mechanism directly. -Nothing above is marked complete — none of it has been verified, and this phase did not attempt -to invent or shortcut any of it. +## 28. Permanent regression coverage added -## 7. Bugs found +`platform/test/e2e-phase9.js` gained one new test: *"a webhook event referencing OUR OWN +transaction_id (not provider_reference) — the real Stripe payment_intent/charge shape — is +still matched"* — builds a test event whose `providerReference` is the transaction's own UUID +(exactly what the fixed `normalizeEvent()` now produces for real Stripe failure/refund events) +and confirms it's found and processed. This runs on every future deterministic `e2e-phase9.js` +pass, with no Stripe credentials required. -None. No genuine defect was exposed because no real Stripe traffic was possible this phase. +`platform/test/stripe-integration-test.js` (the opt-in real-Stripe check from the first Phase +9.1 attempt) was substantially extended: configurable price/currency +(`STRIPE_TEST_PRICE_CENTS`/`STRIPE_TEST_CURRENCY`, defaults stay cheap/generic), a +`STRIPE_TEST_MINOR=true` guardian+minor path, and — once a real payment lands — fully automated +deep verification (payment record, enrollment/cohort/guardian reconciliation, the real +re-signed-event duplicate proof, student access, a real refund) plus a fully automated, +browser-free abandoned/expired-checkout proof via `sessions.expire()`. Still opt-in, still a +guaranteed no-op by default, still refuses live-mode keys. -## 8. Security posture — reasoned about, not re-verified against real Stripe +## 29. Regression results -Every item in the mission's §20 checklist was already true and tested against the deterministic -provider in Phase 9 (`e2e-phase9.js`), and nothing about switching providers changes the code -path that enforces them (the same `commerce.js`/`webhook-routes.js` code runs regardless of -which provider is active — see §3). This phase did not and could not re-verify them against -REAL Stripe traffic specifically; that re-verification is exactly what `stripe-integration-test.js` -(§4) will perform automatically the moment credentials exist. +- `platform/test/e2e-phase9.js`: **20/20** (fresh stack, `PAYMENT_PROVIDER=test`) — includes the + one new regression test from §28. +- `infra/scripts/drills/test-phase9-winter-camp-commerce.js`: **13/13** (fresh stack) — unaffected + by the fix, rerun to confirm. +- No registration/agreement/enrollment/notification-template code changed beyond the webhook + correlation fix itself, so Phase 8/5B/5C were not rerun (per the mission's own conditional + rule) — the fix is entirely inside `commerce.js`/`payment-provider.js`/`webhook-routes.js`, + already covered by the Phase 9 suite. +- No frontend code changed — no frontend build was required. + +## 30. Documentation + +Rewrote `PHASE9_1_STRIPE_TEST_VALIDATION.md` (this document) from BLOCKED to the real, proven +results. Updated `docs/STRIPE_INTEGRATION_RUNBOOK.md` (real-Stripe local testing is no longer +"not yet exercised") and `docs/WINTER_BREAK_LAUNCH_CHECKLIST.md` (the Stripe TEST technical +validation item is now checked; live-mode items remain unchecked, correctly — this phase never +touched live mode). `docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md` needed no change — nothing about +the website-facing contract itself changed this phase, only the server-side webhook correlation +internals. + +## 31. Remaining Stripe TEST gaps + +An actual real declined-payment checkout was not exercised (§20) — available on request, needs +one more manual browser step. Everything else the mission asked for was proven with real Stripe +traffic. + +## 32. Remaining LIVE Stripe gaps + +Unchanged from Phase 9 — see `docs/STRIPE_INTEGRATION_RUNBOOK.md`'s "Going live" section and +`docs/WINTER_BREAK_LAUNCH_CHECKLIST.md`: business/account verification, a live secret key, a +permanent live webhook endpoint, payout/bank configuration, a real HTTPS production URL, and +production deployment itself. None of this phase's work performed or authorized any live-mode +action. + +## 33. Remaining Winter Break business decisions + +Real dates, venue, and final capacity remain undecided (price is now decided — $399 launch / +$449 standard retail, per Chase directly this session; not yet reflected in a committed +`ROOTNODE_PRICING_AND_OFFERS_V1.md` in this canonical repo — see §1). Refund/cancellation +*policy* (as opposed to the mechanical non-cascade behavior proven in §22) remains an open +business decision. + +## 34. Remaining legal requirements + +Unchanged from every prior phase: the actual agreement/waiver legal text, jurisdiction-specific +minor-consent review, data-retention/deletion policy — see `docs/STUDENT_GUARDIAN_DATA_MODEL.md`. ## Final status -**PHASE 9.1 BLOCKED — NO STRIPE TEST CREDENTIALS AVAILABLE.** +**PHASE 9.1 COMPLETE / STRIPE TEST-MODE VALIDATED** -No real Stripe TEST Checkout, real TEST payment, or real signed Stripe webhook was created or -observed this phase — none of the actions requiring them were attempted, per this phase's own -explicit instruction never to fake validation. The permanent, opt-in `stripe-integration-test.js` -is ready and waiting; provide `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` (test mode) and set -`RUN_STRIPE_INTEGRATION_TEST=true` to run the real proof. +A real Stripe TEST Checkout Session, a real Stripe TEST payment, a genuine Stripe-signed +webhook, and the existing registration→enrollment conversion were all proven end to end, plus a +real duplicate-delivery proof, a real automated abandoned-checkout proof, and a real refund with +the non-cascade rule reconfirmed. One genuine defect (webhook-event correlation for +PaymentIntent/Charge-referencing events) was found only by this real traffic, fixed with the +smallest correct change, and covered by a new permanent deterministic regression test. diff --git a/docs/PHASE9_COMMERCE_AND_LAUNCH.md b/docs/PHASE9_COMMERCE_AND_LAUNCH.md index 5c3ffa0..8b1bb71 100644 --- a/docs/PHASE9_COMMERCE_AND_LAUNCH.md +++ b/docs/PHASE9_COMMERCE_AND_LAUNCH.md @@ -271,11 +271,16 @@ Real Stripe live-mode validation once credentials exist (`docs/STRIPE_INTEGRATIO text once reviewed; and, informed by real Winter Break Camp commercial operation, whether a waitlist is actually worth building. -**Phase 9.1 addendum**: real Stripe TEST-mode validation was attempted immediately after this -phase and remained blocked on the exact same missing credentials — see `docs/ -PHASE9_1_STRIPE_TEST_VALIDATION.md`. That phase added a permanent, opt-in integration test -(`platform/test/stripe-integration-test.js`, default no-op, refuses live-mode keys) ready to run -the real proof the moment `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` (test mode) exist. +**Phase 9.1 addendum**: real Stripe TEST-mode validation was completed — a real Stripe TEST +Checkout, a real TEST payment (at the founder-approved $399 Founding Cohort price), a genuine +signed webhook, and the existing registration→enrollment conversion were all proven end to end, +plus real duplicate-delivery, abandoned-checkout, and refund proofs. One real defect (webhook +event correlation for PaymentIntent/Charge-referencing events, invisible to the deterministic +TestProvider) was found and fixed with a permanent regression test added. See `docs/ +PHASE9_1_STRIPE_TEST_VALIDATION.md` for the full evidence. `PAYMENT_PROVIDER=stripe`, +`STRIPE_SECRET_KEY`, and `STRIPE_WEBHOOK_SECRET` are now wired through +`platform/docker-compose.dev.yml` (empty/unset by default — the deterministic TestProvider +remains the default for routine dev/test). --- diff --git a/docs/STRIPE_INTEGRATION_RUNBOOK.md b/docs/STRIPE_INTEGRATION_RUNBOOK.md index 2a0613d..49e0ed8 100644 --- a/docs/STRIPE_INTEGRATION_RUNBOOK.md +++ b/docs/STRIPE_INTEGRATION_RUNBOOK.md @@ -72,27 +72,31 @@ To simulate a **failed payment** or **abandoned checkout**, use `eventType: at all; wait past `REGISTRATION_RESERVATION_TIMEOUT_MINUTES`, or lower it for a faster local test). -## Local testing against real Stripe TEST mode (once credentials exist) +## Local testing against real Stripe TEST mode -Not yet exercised in this repo — no Stripe account/credentials have been provided as of Phase -9.1 either (see `docs/PHASE9_1_STRIPE_TEST_VALIDATION.md` for the exact search performed and -what's needed). Once they exist, `platform/test/stripe-integration-test.js` (added in Phase 9.1) -automates most of the steps below — set `RUN_STRIPE_INTEGRATION_TEST=true` plus -`STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET`/`OWNER_EMAIL`/`OWNER_PASSWORD` and run it; it refuses -to run against a live-mode key and makes zero Stripe network calls unless explicitly enabled. -Manually, or to understand what that script does: +**Exercised for real in Phase 9.1** — a real Stripe TEST Checkout, a real TEST payment, a real +signed webhook, and the full registration→enrollment conversion were all proven this way; see +`docs/PHASE9_1_STRIPE_TEST_VALIDATION.md` for the full result and the one real defect it +exposed and fixed. `platform/test/stripe-integration-test.js` automates most of the steps below +— set `RUN_STRIPE_INTEGRATION_TEST=true` plus `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET`/ +`OWNER_EMAIL`/`OWNER_PASSWORD` and run it; it refuses to run against a live-mode key and makes +zero Stripe network calls unless explicitly enabled. Manually, or to understand what that script +does: 1. Create (or use an existing) Stripe account, switch to **Test mode** in the dashboard. 2. Copy the test-mode **Secret key** (`sk_test_...`) into `STRIPE_SECRET_KEY`. -3. Install the [Stripe CLI](https://stripe.com/docs/stripe-cli) and run: +3. Install the [Stripe CLI](https://stripe.com/docs/stripe-cli) (a prebuilt binary from + https://github.com/stripe/stripe-cli/releases works with no `apt`/sudo needed — this is how + Phase 9.1 installed it, to `~/.local/bin/stripe`) and run: ```bash - stripe login - stripe listen --forward-to localhost:4200/api/webhooks/payment + stripe listen --api-key sk_test_... --forward-to localhost:4200/api/webhooks/payment ``` - The CLI prints a webhook signing secret (`whsec_...`) — set that as `STRIPE_WEBHOOK_SECRET` - for the duration of the local session (it's ephemeral per `stripe listen` run; a real - deployment instead creates a permanent webhook endpoint in the Stripe dashboard and uses ITS - signing secret). + No `stripe login` needed when passing `--api-key` directly (useful in a headless/agent + session with no browser to complete an OAuth flow). The CLI prints a webhook signing secret + (`whsec_...`) — set that as `STRIPE_WEBHOOK_SECRET` for the duration of the local session + (it's ephemeral per `stripe listen` run; a real deployment instead creates a permanent + webhook endpoint in the Stripe dashboard and uses ITS signing secret). Add `--print-secret` + to retrieve just the secret without leaving the listener running. 4. Set `PAYMENT_PROVIDER=stripe` and restart the API. 5. Drive the checkout flow through steps 2-4 of `docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md` — you now get back a REAL `checkout_url`. Open it in a browser and pay with a Stripe test card diff --git a/docs/WINTER_BREAK_LAUNCH_CHECKLIST.md b/docs/WINTER_BREAK_LAUNCH_CHECKLIST.md index e1c0e15..5bc34d4 100644 --- a/docs/WINTER_BREAK_LAUNCH_CHECKLIST.md +++ b/docs/WINTER_BREAK_LAUNCH_CHECKLIST.md @@ -29,7 +29,12 @@ legal reviewer, or Stripe, not from more code. - [ ] Real camp dates and duration. - [ ] Real venue/location (or confirm fully online/remote). - [ ] Real capacity (seat count). -- [ ] Real price and currency. +- [x] Price and currency — **$399.00 USD launch tuition ("Founding Cohort"), $449.00 standard + retail, paid in full, no deposit/payment plan** — founder-approved per Chase directly + (Phase 9.1). Validated against real Stripe TEST mode at this exact amount. Not yet + reflected in a committed pricing document in this canonical repo (referenced but not found + — see `docs/PHASE9_1_STRIPE_TEST_VALIDATION.md` §1/§33); set directly on the real Winter + Break offering via `PATCH /api/admin/offerings/:id` when ready to open registration. - [ ] Whether registration opens ahead of, or alongside, a specific marketing push. - [ ] Refund policy in plain terms (the platform can RECORD any refund decision; it does not decide policy — see `docs/OFFERING_AND_REGISTRATION_MODEL.md`'s "Payment and the commerce @@ -44,13 +49,16 @@ legal reviewer, or Stripe, not from more code. - [ ] Data-retention/deletion policy decided and, if needed, reflected in the agreement text. - [ ] A decision on whether emergency-contact fields need additional encryption at rest. -## Payments — pending real Stripe credentials +## Payments -Phase 9.1 searched for Stripe TEST credentials specifically to run real validation and found -none available anywhere this session could reach — see `docs/ -PHASE9_1_STRIPE_TEST_VALIDATION.md` for the exact search and what Chase needs to provide. A -permanent, opt-in test (`platform/test/stripe-integration-test.js`) is ready to run the real -TEST-mode proof the moment `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` exist. +- [x] **Stripe TEST-mode validation** — a real Stripe TEST Checkout Session, a real TEST payment + at the actual $399.00 founding-cohort price, a genuine signed webhook, the existing + registration→enrollment conversion, a real duplicate-delivery proof, a real automated + abandoned-checkout proof, and a real refund with the non-cascade rule reconfirmed — all + proven end to end in Phase 9.1. One real defect (webhook-event correlation) was found and + fixed as a direct result. See `docs/PHASE9_1_STRIPE_TEST_VALIDATION.md` for the full + evidence and reconciliation IDs. **This is TEST mode only — it does not authorize or imply + LIVE-mode readiness**, see the unchecked items below. - [ ] Real Stripe account created/available (live mode). - [ ] `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` (live values) provisioned into production