Skip to content

release: sell online-payments, with the provisioning landmine disarmed (#117 + #118) - #119

Merged
mahmutkaya merged 2 commits into
mainfrom
develop
Aug 11, 2026
Merged

mahmutkaya merged 2 commits into
mainfrom
develop

Conversation

@mahmutkaya

Copy link
Copy Markdown
Contributor

Release — the module goes on sale, and the thing that would have broken it is fixed

Two commits, and they must ship together:

#117 Removes sellable: false from online-payments — it appears on the signup configurator and the founder's provision picker
#118 Stops a purchase of that module killing the whole provision

#117 alone re-arms a landmine. Before #118, buildTenantRegistryEntry emitted modules verbatim and never emitted stripe_account, so a self-serve buyer of online-payments generated exactly the entry provision-tenant.sh:94 refuses:

if [[ " ${REG_MODULES//,/ } " == *" online-payments "* && -z "$REG_STRIPE_ACCOUNT" ]]; then exit 1; fi

That exit 1 lands before the database, the compose project and the image — so the customer did not get a restaurant lacking card payment, they got no restaurant at all, while the founder found out from an auto-opened deploy issue. It stayed un-fired only because #117 was on develop and this site serves main. Merging #117 without #118 would fire it on the first sale.

After #118 the module and the account ship as a pair or not at all: the founder supplies the acct_ on /admin/provision (runbook §2b creates it first) and provisions in one shot; a self-serve buyer has none and cannot be given one, so the module is withheld, the tenant goes live on everything else, and the generated PR body states the exact two-field follow-up. deferred is recorded on both audit entries, so "who is owed a Stripe follow-up PR" is answerable later.

What actually changes for a visitor

The online-payments option becomes selectable on the public configurator and in the provision picker, at €19/mo. Nothing else — no schema change, no migration, no billing change.

What this does NOT unblock

Selling the module and being able to onboard a tenant onto Stripe are separate gates, and the second is still shut. POST /v1/accounts on the live platform returns 400 "You must complete your platform profile to use Connect and create live connected accounts" — a dashboard questionnaire no API key of any scope gets past (SOFRA-PAYMENTS-PLAN §7.12, measured 2026-08-11). Until that is answered, an acct_ cannot be created for anyone. #118 is what makes that a delay rather than a dead provision: a buyer still gets their restaurant.

Verification after merge

Merging builds the image; rollout is manual and this release contains no schema change, so no migrate one-off is needed:

docker compose -f docker-compose.prod.yml pull sofra && docker compose -f docker-compose.prod.yml up -d sofra

Note the box is currently two releases behind/api/health reports c3b6403 (2026-07-31) while main is 5977657 (2026-08-10) — so this rollout also lands #116.

After rolling:

🤖 Generated with Claude Code

mahmutkaya and others added 2 commits August 10, 2026 21:58
`sellable: false` kept online-payments out of the public SignupConfigurator
and the founder's ProvisionPicker. Its original reason (an unbuilt surface)
expired when S11 merged; what held it to the end was SOFRA-PAYMENTS-PLAN
§7.6 — Connect unconfirmed on the LIVE Stripe platform, where
`POST /v1/accounts` is provisioning's first call and refuses outright
without it. Confirmed enabled on acct_1TpwTDCHzplJfkIy, 2026-08-10.

Removing the flag broke two things outside the one-line diff:

- The strings did not exist. The module had never rendered, so
  `signup.configurator.module.online-payments` and its hint were missing
  from all six locales and /signup showed the raw keys. The parity check
  diffs the other five AGAINST en.json, so a key absent from all six reads
  as "in parity"; SignupConfigurator builds both keys by template literal,
  so TypeScript cannot see them either. A unit test now asserts every
  sellable module has a label and a hint in en.json — parity carries the
  other five. Verified it fails when the hint is deleted.
- The marketing site contradicted the price list. faq.items.payments and
  compare.table.rows.payments.sofra both said online payments were "on the
  roadmap — today you charge at the counter", in six locales, while the
  configurator offered the feature at EUR 19. The comparison page ran that
  against GloriaFood's "US$29/month" — a competitive claim wrong in our own
  disfavour. Rewritten; the FAQ now also states the Connect KYC
  precondition, which nothing had told a customer.

The hint says "local methods like TWINT or iDEAL" rather than TWINT alone:
methods are chosen dynamically per connected account, and TWINT does not
exist for a Dutch tenant.

The bundle-exclusion test above is now vacuous (no module carries the flag)
and says so — it is kept for the next module that arrives id-first.

Verified on a dev server: /signup lists the module at EUR 19 and prices a
selection at EUR 38, /en#faq and /en/compare/gloriafood render the new copy,
and /ar/signup renders it RTL with no raw keys.

Refs SOFRA-PAYMENTS-PLAN §7.6
…118)

A tenant who buys `online-payments` generated a registry entry that
`provision-tenant.sh` refuses: the guard requires the module and a
`stripe_account:` together, and it exits 1 BEFORE the database, the compose
project and the image. So the customer did not get a restaurant without card
payment — they got no restaurant at all, and the founder learned from the
auto-opened deploy issue while a paid customer waited.

The module and the account now ship as a pair or not at all:

- Founder path — `/admin/provision` gains an optional Stripe account field.
  The runbook (§2b) already has them create the connected account BEFORE
  proposing, precisely because of this guard, so they arrive holding the
  `acct_` and the entry carries both halves in one shot.
- Self-serve path — the buyer has no account and cannot be given one (only
  the restaurant can create it, through Stripe's hosted onboarding, which
  cannot be pre-filled). Their module is withheld, they go live on everything
  else, and the PR body names what was bought, why it is absent, and the
  exact two-field follow-up PR.

`deferred` is returned from `openProvisioningPr` and recorded on both audit
entries: a prose section in one PR is not a record anyone can query later, and
a deferral means a customer is being billed for a module their tenant does not
yet have.

`buildProvisioningPrBody` moves to `lib/provisioning-pr-body.ts` — the pair
outgrew the 200 LOC limit — and is added to the coverage `include` so the move
does not drop already-covered code out of the floor's scope.

The guard's own shell condition is evaluated by a real bash in the unit suite
against both shapes, so the test pins the generator to the deploy repo's text
rather than to a paraphrase of it.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@mahmutkaya
mahmutkaya merged commit 64f41ad into main Aug 11, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant