Revive the dead Stripe billing handlers, and settle the partner price - #306
Merged
Conversation
Two silent-failure classes, found by tracing the money path end to end rather than trusting the green suite. STRIPE — handlers that could never fire in production - invoice.paid / invoice.payment_failed read `invoice.subscription`, a field Stripe REMOVED in API version 2025-04-30.basil. We pin 2026-07-29.dahlia and the live endpoint runs 2026-02-25.clover, both past basil, so the field is absent on every delivery: a failed payment never moved a subscription to past_due and a recovered one never moved it back. Nothing failed loudly because both handlers cast the invoice to Record<string, unknown>, silencing tsc, and their tests hand-built the pre-basil shape. One shared `invoiceSubscriptionId()` now reads parent.subscription_details.subscription with a legacy fallback. The rewritten tests were verified to FAIL against the old code. - checkout.session.completed recorded every order as 'paid' without checking payment_status. Delayed-notification methods (ACH, Bacs, SEPA, Klarna) fire it on AUTHORISATION with funds days out, so an unfunded order entered the fulfillment queue and the revenue count. Now records pending_payment, notifies nobody, and promotes on async_payment_succeeded — which was not handled at all before, along with async_payment_failed. Status can no longer walk backwards, so a late retry cannot reset report_delivered or refunded to paid. - No refund or dispute handling existed: a refunded $499 order sat at 'paid' forever while the admin rollup counted it as revenue and as a paying customer — the exact figure the Sep 1 kill-criteria review reads. charge.refunded and charge.dispute.created now settle the row; a PARTIAL refund is logged and left counted rather than erasing a sale over a goodwill credit. - /api/stripe/portal built its return_url from `NEXT_PUBLIC_APP_URL || 'http://localhost:3000'`. That variable is unset in production (measured 2026-08-14, see lib/site-url.ts), so a paying customer finished managing billing and landed on their own machine. Uses SITE_URL. - Every live runbook told the founder to point the webhook at the apex host, which 308-redirects; Stripe counts a non-2xx as a failed delivery and eventually disables the endpoint. All instruction surfaces now say www, with the reason inline so it is not "tidied" back. Dated audit archives left as written history. - /report/thank-you carried a hardcoded apex canonical, on the page a buyer lands on immediately after paying. PARTNER PRICE — three public numbers for one offer /partners advertised "20% revenue share" and "$299 wholesale" on a $499 report (a 40% cut) in adjacent cards, while the brain directed 40-50%. MSP-CHANNEL-RESEARCH.md flagged it as blocking and left the call open. Settled: retail $499, partners $399 — a flat $100 discount. Modelled in DOLLARS, not percent, because a percentage forces a rounding decision ($499 x 0.80 = $399.20) and whichever way it rounds the realised cut misses the published figure. It is a discount, not a payout: no money ever leaves, so there is no commission tracking to build. Retail stays $499 and not $500 — the sub-$500 figure keeps the purchase under the common procurement-approval threshold the product is positioned on, and keeps the live Stripe price (unit_amount 49900) matching the site, so nothing has to be recreated in Stripe. Wholesale is computed from the discount and the Stripe charge amounts are imported rather than typed, which is how $299 and a published "20%" coexisted. A new coherence guard fails if a retired figure reappears, if the price stops being derived, or if retail leaves $499. Gates: tsc 0 · eslint 0 errors (31 pre-existing warnings) · 2963/2963 tests pass in 209 files (was 5 failing) · production build clean. Not included: the idle-session module is parked until its middleware wiring lands — an unenforced security control is worse than none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Ugr8hJemeERsbKeTryM5Q
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
thecelestialmismatch
marked this pull request as ready for review
August 19, 2026 02:22
thecelestialmismatch
pushed a commit
that referenced
this pull request
Aug 19, 2026
Brings #300..#306 onto this branch and resolves the two conflicts, both on the homepage stat row. Both sides rewrote the SAME tile: this branch replaced the Netskope figure with "2 / deployment paths", while main (#301) kept the figure and corrected its denominator. The grid is a hard `repeat(4, 1fr)` (app/hermes.css:263), so a fifth tile orphans on its own row — it is genuinely one or the other, not a merge both sides can win. Resolved to main's tile, because keeping it loses nothing. <ModeBNotice> renders IMMEDIATELY below this row and already states both deployment paths with the honesty the NEVER-DO list requires: "CUI-safe = Mode B (Docker on your infrastructure); the hosted trial runs on Vercel and is not FedRAMP-authorized." The deployment distinction therefore survives in a richer form, and it is still asserted by the Mode-B notice test. The Netskope figure has no second home on the page, and it is the market proof for the fastest-closing buyer. The matching test conflict is resolved the same way: the tile assertions follow the source, and the branch's `not.toContain('~80,000')` guard against unverifiable statistics is kept. Gates on the merged tree: tsc 0 · eslint 0 errors (35 pre-existing warnings) · 2970/2970 tests pass in 211 files · production build clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Ugr8hJemeERsbKeTryM5Q
This was referenced Aug 19, 2026
thecelestialmismatch
added a commit
that referenced
this pull request
Aug 20, 2026
thecelestialmismatch
pushed a commit
that referenced
this pull request
Aug 20, 2026
Brings #300..#306 onto this branch and resolves the two conflicts, both on the homepage stat row. Both sides rewrote the SAME tile: this branch replaced the Netskope figure with "2 / deployment paths", while main (#301) kept the figure and corrected its denominator. The grid is a hard `repeat(4, 1fr)` (app/hermes.css:263), so a fifth tile orphans on its own row — it is genuinely one or the other, not a merge both sides can win. Resolved to main's tile, because keeping it loses nothing. <ModeBNotice> renders IMMEDIATELY below this row and already states both deployment paths with the honesty the NEVER-DO list requires: "CUI-safe = Mode B (Docker on your infrastructure); the hosted trial runs on Vercel and is not FedRAMP-authorized." The deployment distinction therefore survives in a richer form, and it is still asserted by the Mode-B notice test. The Netskope figure has no second home on the page, and it is the market proof for the fastest-closing buyer. The matching test conflict is resolved the same way: the tile assertions follow the source, and the branch's `not.toContain('~80,000')` guard against unverifiable statistics is kept. Gates on the merged tree: tsc 0 · eslint 0 errors (35 pre-existing warnings) · 2970/2970 tests pass in 211 files · production build clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Ugr8hJemeERsbKeTryM5Q
thecelestialmismatch
pushed a commit
that referenced
this pull request
Aug 20, 2026
A live read of every public URL on www.houndshield.com found four stale files served from the marketing domain, between them publishing a fabricated customer count, a second subscription pricing grid, and a C3PAO referral program. public/stats.json "orgs_protected": 143, with a comment naming these as the landing-page hero figures. Zero customers. public/hermes-demo.html Full stale site prototype: the retired $199/$499/ $999/$2,499 grid, "C3PAO Referral Partner - 30% recurring", invented tenant telemetry, and a BAA commitment /trust contradicts. Also reachable at /hermes and /hermes-redesign via next.config rewrites, now removed with it. public/_bootstrap.html 885 KB duplicate homepage saved from an artifact preview shell, badged "49/49 tests passing", leaking an internal prototype UUID. Its four orphaned dependencies go with it. public/llms.txt Rewritten. This is the file AI answer engines read, last updated 2026-05-08. It led with the $199/month tier, pitched C3PAOs as a partner channel twice, carried an unsourced "vs Nightfall ($75K/yr)", and claimed CUI-safety without the Mode B caveat. Now quotes the settled partner price from #306: $399, a flat $100 discount, not a revenue share. C3PAOs are barred by 32 CFR Part 170 and ISO 17020 cooling-off rules from recommending tools to organizations they assess. The existing guard in app/partner/__tests__/channel-framing.test.ts was scoped to app/partner/**, so none of these files were covered. Why the pricing grid survived every prior correction: plans.test.ts asserted that llms.txt MUST list every monthly tier. The guard made publishing it mandatory. PRICING_PLANS is dormant Stage-2 data with no rendering consumer (verified: no non-test importer of PRICING_PLANS/getPlan/ANNUAL_DISCOUNT), so the assertion is inverted - llms.txt may not advertise a subscription that cannot be bought. Probe-tested both directions: injecting "$199/month" turns it red, reverting turns it green. Same failure mode as the Netskope "43%" assertion recorded in tasks/lessons.md. Also removed: houndshield-logo1.png, byte-identical to houndshield-logo.png (same MD5); index-DW_q7_qd.js and index-CO1gdLWv.css, referenced by nothing. houndshield-logo.png is KEPT - 12 live references. Every deletion was reference-checked before removal. The only file with real couplings was hermes-demo.html, whose byte-fidelity test and two next.config rewrites are removed alongside it. README: corrected six stale measured numbers, replaced a regulatory paragraph that contradicted the live /cmmc-phase-2 page, and rewrote a status section that reported checkout as broken when it sells today while omitting that a completed purchase is recorded nowhere. Dropped three hand-typed stat badges - all three were stale, and p99 varies 0.65-0.89 ms between runs, so pinning it guarantees drift. Gates on this branch, rebased onto main: app 2,993 passed / 212 files - proxy 92 passed / 4 files - bench p99 within the 10 ms budget - next build exit 0. Net ~42,000 lines and ~7.3 MB removed, none of it referenced by the app. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UjEyfCdsoUjYXo6aAaQ4D
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Two classes of silent failure, found by tracing the money path end to end rather than trusting the green suite.
Stripe — handlers that could never fire in production
invoice.paid/invoice.payment_failedinvoice.subscription, a field Stripe removed in API version2025-04-30.basil. We pin2026-07-29.dahlia; the live endpoint runs2026-02-25.clover. Both are past basil, so the field is absent on every delivery. A failed payment never moved a subscription topast_due; a recovered one never moved it back.invoiceSubscriptionId()readingparent.subscription_details.subscription, with a legacy fallback and support for the expanded-object form.checkout.session.completedrecordedstatus: 'paid'without checkingpayment_status. ACH/Bacs/SEPA/Klarna fire it on authorisation, funds days out.pending_payment, notifies nobody, promotes onasync_payment_succeeded— an event that was not handled at all, along withasync_payment_failed.paidforever while the admin rollup counted it as revenue and as a paying customer — the exact figure the Sep 1 kill-criteria review reads.charge.refunded+charge.dispute.createdsettle the row. A partial refund is logged and left counted rather than erasing a sale over a goodwill credit.return_urlbuilt fromNEXT_PUBLIC_APP_URL || 'http://localhost:3000'. That variable is unset in production (measured 2026-08-14, seelib/site-url.ts), so a paying customer finished managing billing and landed on their own machine.SITE_URL.www, with the reason inline so it isn't "tidied" back. Dated audit archives left as written history./report/thank-youcanonicalsiteUrl().Status can no longer walk backwards: a late Stripe retry cannot reset
report_deliveredorrefundedback topaid.Why the tests never caught the invoice bug: both handlers cast the invoice to
Record<string, unknown>, which silencestsc, and the tests hand-built the pre-basil shape{ subscription: "sub_123" }— a payload Stripe no longer sends. Green tests over dead code. The rewritten tests were verified to fail against the old implementation (3 failures) before the fix was restored.Partner price — three public numbers for one offer
/partnersadvertised "20% revenue share" and "$299 wholesale" on a $499 report (a 40% cut) in adjacent cards, whileCLAUDE.mddirected 40–50%.docs/gtm/MSP-CHANNEL-RESEARCH.mdflagged this as a blocking inconsistency and deliberately left the margin call to the founder.Settled: retail $499, partners $399 — a flat $100 discount.
price_1Tge3aQK7cyCnCHkfIfVDAGt,unit_amount: 49900) matching the site — so nothing has to be recreated in Stripe and the Payment Link fallback rail keeps working.Accepted risk, stated in the code: the Kitecyber source in that research doc puts MSP expectations at a 60–70% gross margin. At $399 a partner clears 20% reselling at $499 and 60% at $999 — the bottom of the band, reached only at the top of the resale range.
PARTNER_DISCOUNT_USDis the one constant to revisit if partner sign-ups stall.Fixes # (n/a — no tracking issue)
Type of change
Note: the partner wholesale price moves $299 → $399. Public-facing and worth a look before merge.
Jordan's Test Plan (CMMC Buyer — required before merge)
npm run buildpasses incompliance-firewall-agent/proxy/,patterns/, orclassifier/is in this diffreport_ordersholds order metadata (email, amount, Stripe ids) — no CUI, no prompt contentVerification
npm run buildsucceeds locallynpm run lint: 0 errors, 31 warnings (all pre-existing, none in changed files)New tests proven to discriminate, not decorate:
invoice.*tests fail against the old handler (verified: 3 failures, then restored to green)lib/pricing/__tests__/partner-offer-coherence.test.tsfails if a retired figure reappears, if wholesale stops being derived from the discount, or if retail leaves $499The coherence guard caught two real defects during this work: a rounding that would have shipped a 20.04% cut under a "20%" claim, and a blind price sweep that corrupted an unrelated
$400K breach coststatistic inNavbar.tsx.Checklist
CLAUDE.md,MSP-CHANNEL-RESEARCH.md(resolution recorded against the section that raised it), and every live Stripe runbookanytypes added in compliance-critical codeNot in this PR
TURNSTILE_SECRET_KEYlesson).Founder action still required (cannot be done from code)
STRIPE_WEBHOOK_SECRETwas set and deployed during this session, and the live account now shows exactly one enabled endpoint onhttps://www.houndshield.com/api/stripe/webhook. Two gaps remain:api_versionreads2026-02-25.clover, not the2026-07-29.dahliaselected in the wizard. Low impact today — the Checkout Session fields this code reads are identical across both — but worth correcting.charge.refundedandcharge.dispute.createdare not yet subscribed. The handlers in this PR are live code with no events reaching them until those two are ticked.Generated by Claude Code