feat(payments): the platform mints the connected account, so one registry PR carries both halves (E3) - #229
Merged
Conversation
…stry PR carries both halves (E3)
|
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
stripe_account:stops being something a founder types and becomes something the control plane MINTS, so the registry PR carriesonline-paymentsandstripe_account:in ONE commit andprovision-tenant.sh:117never fires.openProvisioningPr, besidetenantPartnerBrandand for the same reason: it is the one place that writes a registry entry, so no caller can forget it and no caller can inject one. Both paths (self-serve webhook, founder form) inherit it.stripeAccountinput is gone from the form, the schema and the FormData mapping. Its grammar check survives, applied to what Stripe returned.Issue / plan link
Type
featThe premise this deletes
Five files stated it: "only the restaurant can create it, through Stripe's hosted onboarding, which cannot be pre-filled (
oauth_not_supported)". Measured 2026-09-05:oauth_not_supportedis the answer to an UPDATE, not to a CREATE. Prefill at create time works (#228). So the premise is false in the direction that matters, and the whole deferred-module machinery stops being the normal path.It is not deleted, and that is deliberate:
splitDeferredModulesstays as the last-resort rule.provision-tenant.sh:117refusesonline-paymentswithout an account before the database, and that guard must stay satisfiable. When a mint fails, withholding the module gives the restaurant everything else and a working cash tenant; proposing the unpaired module would give them no tenant at all.stripeAccountNote) and tells the founder to fix the cause or add both fields by hand — never "wait for the restaurant".provision-tenant.sh's own guard logic still passes, which is what "still satisfiable" means concretely.The one thing the plan did not anticipate: a country
Stripe fixes an account's country permanently at creation. sofra holds no country for a tenant — not on
SignupRequest, not in the registry (provision-tenant.sh's field whitelist has nocountry). Solib/connect-account-country.tsderives it from the one fact we do hold, the currency, and refuses to guess:CH/GB/US/AEA refusal costs the founder one hand-edit before merging (the PR body says exactly what to add). A guess costs a live, uncorrectable Stripe account in the wrong country for a real restaurant. CH is the first market and RUMI is a CH tenant, so the path that matters works today. Asking for a country properly (a field on the lead / the form) is recorded in BACKLOG rather than smuggled in here — it is a form change with six locales attached, and this slice is already the wide one.
NFR triage
provision-tenant.shkeeps reading the registry (ADR-003/007) and gains no capability, and the box key must never getConnect → write. It runs after the "slug already merged" refusal, so a slug that cannot be proposed cannot cost a live Stripe account.mintForProposalnever throws (its caller chain ends at the Mollie webhook, where a throw means ~26h of redeliveries for a paying customer) and logs no PII — a slug and Stripe's own message.scripts/check-message-parity.mjs→ "All 5 locales in parity (1239 keys each)".acct_(founder path) or the reason there is none (mintNote, both paths). Anacct_is an identifier and not a secret —/admin/tenantsalready renders one — and this is the only durable record that a LIVE Stripe account was created while someone filled in a form.stripeAccount-typed-on-the-form regression test is inverted (the field must now be ignored) rather than deleted, and a new one asserts both halves land in one entry once the mint has attached an account.acct_nobody typed, and that is where they learn where it came from.Test plan
typecheck·lint·test:coverage(1110 tests, floor green, 100% lines) ·build·check-single-file --all·check-message-parityall green locallygrep -F) and COMPILED (tscexit 0) before any test result was read:FR→ 2 RED (the unit and the mint seam)buysPairedforced true (mint for a tenant that never bought it) → 1 REDreadProvisionFormreadsstripeAccountfrom the FormData again → 1 REDstripe_acount:→ 4 REDDeploy notes
STRIPE_API_KEYneedsConnect → write(owner-side prerequisite P2) before a mint can succeed; without it the code degrades exactly as designed — the module is withheld, the PR body says why, and the tenant still ships.Connect → write, EUR, Stripe down) lands on the pre-existing deferral path, which is what the fleet does today for every tenant.