feat(payments): learn from Stripe when a restaurant finishes onboarding (E5) - #232
Merged
Merged
Conversation
|
This was referenced Sep 5, 2026
Merged
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.



Summary
account.updatedbranch (ADR-011 amendment, slice E5) — how this platform learns that a restaurant finished Stripe onboarding, now that we are the one who created the account.StripeAccountStatus(one additive migration) recordingcharges_enabled,payouts_enabled,details_submitted,requirements.currently_due.lengthandcapabilities.twint_payments.event.accountguard. The tenant-facing read model (StripeAccountClient, 5-minute cache) is unchanged and stays where it is.Issue / plan link
Type
featPosition, and why it is not free
event.accountguardapplication_fee.createdevent.account(measured 2026-09-04, comment at:131-134) — the guard would discard itaccount.updatedMoved above, this branch would run for platform events that name no account. Removed, nothing would ever learn a restaurant finished onboarding. Both are mutants below, and both go red.
There is one residual risk and it is handled rather than assumed away: if
account.updatedever arrived withoutevent.account, the branch would never fire and the only symptom would be an empty table — the exact shape of silence that hidapplication_fee.created. So the!accountpath now logs a warning naming that event type. A tell, not a guess.Polling was the alternative, and it is worse
A fleet-wide
GET /v1/accountsloop is the rate-limit shape the backend's own account cache comment warns about, and it would still only be a snapshot. One event type on an endpoint we already run, with a verified secret, is a branch — not an endpoint.Why this table upserts and OVERWRITES, unlike the two fee tables
feeEarnedUpsert'supdateis deliberately empty: a fee is an immutable event, so a redelivery has nothing to say. A status is a snapshot, so a second delivery has everything to say. Overwriting is safe here for one specific reason: the write path takes only the id from the event and re-reads the account from Stripe (CLAUDE.md §5.3, fetch-and-verify — the same disciplinestripe-fee-earned.tsfollows). So a redelivery of a stale event writes today's truth, and no event ordering has to be reasoned about anywhere. Trusting the event body instead would let a redelivered "not yet enabled" silently un-do a tenant that is live.Other decisions, each with a reason in the migration header:
chargesEnabled/payoutsEnabled/detailsSubmittedare three columns, not one. They move independently — a restaurant taking cards whose payouts are blocked is a support call nobody sees coming — anddetails_submittedis the field that decides which link/onboarding/paymentsmints, because Stripe refuses a login link before onboarding completes (400, measured).twintCapabilitykeepsinactivedistinct from absent. TWINT has a Stripe-side approval queue behind it, so "the tenant is live but TWINT is not" is a real state nothing else in the fleet reports.StripeConnectAccount. That one records accounts we minted; an account we did not mint can still send this event (the hand-runcurlin runbook §2b.1 made some), and dropping its status because we cannot name its tenant would be the mistakeStripeApplicationFee's header refuses to make about money.observedAtis OUR clock — the opposite of the fee tables, and deliberately: they record events that happened at a Stripe timestamp; this records a snapshot, and what matters about a snapshot is how old it is.NFR triage
acknowledge()taxonomy is reused unchanged, so a 404 is acknowledged and anything else is a 5xx Stripe retries.account.updated, replacing a poll that does not exist. No loop, no fan-out.Database / migrations
prisma/migrations/20260905230000_stripe_account_status/— no applied migration editedmigrate deploy+migrate diff --exit-code→ 0, "No difference detected" against a throwawaypostgres:16-alpineTest plan
typecheck·lint·test:coverage(1129 tests) ·build·check-single-file --allgrep -F) and COMPILED (tsc0) before any test result was read:event.accountguard (reading the id fromevent.data.object.idso it still compiles) → 2 REDrequirementsDueCounthardcoded to0→ 1 REDupdateemptied, i.e. a redelivery can no longer restate the row → 1 REDacct_1UCPNg…, since deleted;GETon it now returns 403), not the documentation.Deploy notes
ghcr.io/piwas-21/sofra:migratebefore rolling the app.connect: trueendpoint must haveaccount.updatedticked — otherwise this branch is correct and never fires, and the tell is an empty table plus noaccount statusline in the logs.