You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The control plane's own half of E6. The frontend (#726) and backend (#493) shipped the Express copy and the paymentsLinkUrl contract; sofra was deliberately left to this repo's owner, and its strings still asserted the retired Standard premise — that the restaurant supplies their own acct_, and that a tenant is "not eligible" until they do.
The panel renders exactly when a buyer paid for online-payments and the entry does not grant it. Under Express that means our account creation did not happen. So:
https://dashboard.stripe.com pointed at a login the restaurant cannot use — an Express account has no full dashboard, and in this state they have no account at all.
Deleting the sentence would lose something useful, so it is re-stated as nextStep: what the short Stripe form will ask for (date of birth, phone, accepting Stripe's terms — the measured currently_due remainder), and why no link can be kept (one lives ~5 minutes).
body now says we open the Stripe account. "There is nothing for you to do right now" stays, and is more true than before: the outstanding work is ours, not theirs.
The §9 Q1 copy rule still holds — no env/box/deploy/registry/pull request/founder in any of the six locales — and it is enforced by the existing test, which I mutated to prove it still bites.
Contract check, against the MERGED refs rather than my worktree
Referrer-Policy — done here.next.config.ts gets a second, later-matching entry for /:locale/onboarding/payments/:token with no-referrer (last match wins in Next's merge). Verified against a running next start, with a positive control: that route answers Referrer-Policy: no-referrer, /en/legal answers the site-wide strict-origin-when-cross-origin. One header, not two.
Log hygiene — measured, and it goes in E7's BACKLOG.git grep -nE "^\s*log\s*\{|log_file|access.log" origin/develop -- Caddyfile Caddyfile.staging caddy-tenants tenants/templates in the deploy repo finds nothing, while plain log matches 7 lines in the same file (positive control). So no site explicitly configures access logging — which means Caddy's default behaviour decides, and its default emits access logs to stderr, i.e. into the container log. The token is therefore very likely in docker logs on the box. I have not read a running container's log (no improvised SSH), so that last step is UNVERIFIED and the BACKLOG line will say so.
Rotation — E7 BACKLOG. The column is nullable + unique, so rotation is possible and nothing does it. The line will state the founder's move (null the column, re-mint, re-send) and that the old URL dies the moment it is nulled, because findConnectAccountByToken treats NULL as no match.
NFR triage
D1 security: no-referrer on the credential-bearing route, verified live. No other surface changes.
D6 i18n: 3 keys × 6 locale files, one key renamed (stripeLink → nextStep); parity script → "All 5 locales in parity (1244 keys each)" (it compares the five non-English bundles against English; six files, five comparisons).
D9 testing: the copy-rule test's key-set assertion updated with the reason; the E2E spec's link assertion inverted (toHaveCount(0) plus a positive assertion on the replacement text) rather than removed — an absent link that nobody asserts is how a regression sneaks back.
D4 UX: the panel now prepares the restaurant for the short form instead of offering a login they do not have.
Rest: n/a because no schema change, no money arithmetic, no new endpoint, no PII.
Header verified by curl against next start, with a control page
Mutation: the nextStep string rewritten to name "the deploy details" → the copy-rule test goes RED (landed via grep -F, tsc 0, checked before reading the result). So the rule that forbids naming our plumbing still bites on the string I added.
npm run test:e2e:full — the unmocked suite that owns owner-payments-pending.spec.ts; result appended to this PR as a comment (it is not a CI job here, so it is not gate-visible either way).
Deploy notes
Migration one-off required: no. New env vars: none.
Rollout order / risk: copy and one response header. The no-referrer entry only matches the onboarding route.
→ throwaway postgres on :55440
→ migrate + seed
seed-e2e: created ADMIN e2e-admin@example.test
seed-e2e: created PARTNER e2e-partner@example.test
→ build (without DATABASE_URL in scope — repo rule)
→ playwright
Running 80 tests using 1 worker
✓ 1 [chromium] › tests/e2e/admin-backups.spec.ts:81:5 › the three machine endpoints are not open to an anonymous caller (527ms)
✓ 2 [chromium] › tests/e2e/admin-backups.spec.ts:101:5 › a malformed inventory is refused without echoing anything back (48ms)
✓ 3 [chromium] › tests/e2e/admin-backups.spec.ts:114:5 › the page shows what is protected and, first, what is not (1.0s)
✓ 4 [chromium] › tests/e2e/admin-backups.spec.ts:175:5 › an artifact the box stops listing is removed, not remembered (117ms)
✓ 5 [chromium] › tests/e2e/admin-backups.spec.ts:198:5 › the founder queues a backup and the box collects it on its next poll (944ms)
✓ 6 [chromium] › tests/e2e/admin-backups.spec.ts:248:5 › deleting a tenant's LAST copy is refused until it is explicitly overridden (1.0s)
✓ 7 [chromium] › tests/e2e/admin-tenants.spec.ts:14:7 › the founder can see which tenants can take a card › an entry with an account shows it, and one without the account is flagged (660ms)
✓ 8 [chromium] › tests/e2e/backup-agent-box-binding.spec.ts:24:5 › a valid agent bearer cannot push an inventory for a box it is not (50ms)
✓ 9 [chromium] › tests/e2e/backup-agent-box-binding.spec.ts:36:5 › …and cannot claim another box's jobs either (48ms)
✓ 10 [chromium] › tests/e2e/backup-agent-box-binding.spec.ts:44:5 › its OWN box still works — the mechanism is a binding, not a block (48ms)
✓ 11 [chromium] › tests/e2e/backup-agent-box-binding.spec.ts:54:5 › a bearer this control plane does not hold is refused outright (43ms)
✓ 12 [chromium] › tests/e2e/backup-alert-cron.spec.ts:23:5 › the sweep endpoint is not an open relay (46ms)
✓ 13 [chromium] › tests/e2e/backup-alert-cron.spec.ts:34:5 › a sweep that could not mail says so, and leaves no marker behind (85ms)
e2e cleanup: cancel sub_3vCBrm2uhP → 200
✓ 14 [chromium] › tests/e2e/billing-mollie.spec.ts:93:7 › Mollie first payment and activation › a self-serve owner pays, the mandate race resolves, and the plan goes ACTIVE (14.4s)
✓ 15 [chromium] › tests/e2e/contact-intake.spec.ts:35:5 › the sixth contact POST from one client is refused (61ms)
✓ 16 [chromium] › tests/e2e/contact-intake.spec.ts:47:5 › a honeypot-filled contact POST is still dropped silently (44ms)
✓ 17 [chromium] › tests/e2e/contact-intake.spec.ts:60:5 › a malformed contact POST is a 400, not a 500 (43ms)
✓ 18 [chromium] › tests/e2e/control-auth.spec.ts:45:5 › admin logs in and lands on the admin console (521ms)
✓ 19 [chromium] › tests/e2e/control-auth.spec.ts:52:5 › partner logs in and lands on their dashboard (543ms)
✓ 20 [chromium] › tests/e2e/control-auth.spec.ts:58:5 › partner is denied the admin console (617ms)
✓ 21 [chromium] › tests/e2e/health.spec.ts:18:7 › /api/health › reports liveness and identifies the service (6ms)
✓ 22 [chromium] › tests/e2e/health.spec.ts:35:7 › /api/health › is never cached (5ms)
✓ 23 [chromium] › tests/e2e/health.spec.ts:43:7 › /api/health › leaks no configuration (4ms)
✓ 24 [chromium] › tests/e2e/invite-resend.spec.ts:27:5 › an expired invite link offers a new one instead of a dead end (138ms)
✓ 25 [chromium] › tests/e2e/invite-resend.spec.ts:36:5 › a re-send answers the same to an unknown address as to a real one (604ms)
✓ 26 [chromium] › tests/e2e/login-timing.spec.ts:75:5 › an unknown address costs the same as a known one (no user enumeration) (4.2s)
✓ 27 [chromium] › tests/e2e/owner-dashboard.spec.ts:39:7 › the owner is told where their app is — and only what is true › before paying, the app panel says nothing at all (4.8s)
✓ 28 [chromium] › tests/e2e/owner-dashboard.spec.ts:50:7 › the owner is told where their app is — and only what is true › paid with nothing proposed yet says 'preparing', and hands out no link (3.8s)
✓ 29 [chromium] › tests/e2e/owner-dashboard.spec.ts:62:7 › the owner is told where their app is — and only what is true › an open proposal says 'being built', and still hands out no link (3.9s)
✓ 30 [chromium] › tests/e2e/owner-dashboard.spec.ts:72:7 › the owner is told where their app is — and only what is true › the mandate-lag window is not told the same thing twice (7.8s)
✓ 31 [chromium] › tests/e2e/owner-dashboard.spec.ts:86:7 › the owner is told where their app is — and only what is true › a MERGED registry entry is still not 'ready' — the app has to answer (1.6s)
✓ 32 [chromium] › tests/e2e/owner-dashboard.spec.ts:109:7 › an owner with an active plan is shown their plan › amount, next charge and payment history — not 'nothing to do here' (3.8s)
✓ 33 [chromium] › tests/e2e/owner-dashboard.spec.ts:145:7 › an owner with an active plan is shown their plan › no pay button once the plan is active — that is the double-charge trap (1.5s)
✓ 34 [chromium] › tests/e2e/owner-payments-pending.spec.ts:46:7 › the buyer is told about the module they cannot see yet › bought and not yet granted — the card is there, and says nothing they can't act on (2.6s)
✓ 35 [chromium] › tests/e2e/owner-payments-pending.spec.ts:78:7 › the buyer is told about the module they cannot see yet › bought and granted — the card is gone (1.6s)
✓ 36 [chromium] › tests/e2e/owner-payments-pending.spec.ts:93:7 › the buyer is told about the module they cannot see yet › never bought it — the card is gone too (1.6s)
✓ 37 [chromium] › tests/e2e/partner-base-domain.spec.ts:33:5 › a partner claims a domain and is told exactly what to publish (1.0s)
✓ 38 [chromium] › tests/e2e/partner-base-domain.spec.ts:59:5 › a check that finds no record leaves the domain unusable, and says so (1.1s)
✓ 39 [chromium] › tests/e2e/partner-base-domain.spec.ts:86:5 › our own zone is refused before any lookup happens (926ms)
✓ 40 [chromium] › tests/e2e/partner-base-domain.spec.ts:102:5 › a partner never sees another partner's zone (1.1s)
✓ 41 [chromium] › tests/e2e/partner-base-domain.spec.ts:121:5 › the founder sees a partner's zones, and which proof has gone stale (1.3s)
✓ 42 [chromium] › tests/e2e/partner-brand.spec.ts:29:5 › a partner saves their public details, and the publish switch is inert (996ms)
✓ 43 [chromium] › tests/e2e/partner-brand.spec.ts:61:5 › the prefill carries the trade name and NOT the billing address (952ms)
✓ 44 [chromium] › tests/e2e/partner-brand.spec.ts:87:5 › a partner cannot write another partner's brand, even by naming them (1.2s)
✓ 45 [chromium] › tests/e2e/partner-brand.spec.ts:119:5 › a sole trader is told why their own name cannot be published (D-B1a) (1.1s)
✓ 46 [chromium] › tests/e2e/partner-client-domain.spec.ts:21:5 › a partner proposes their own zone, and is handed the record to publish (1.1s)
✓ 47 [chromium] › tests/e2e/partner-client-domain.spec.ts:61:5 › the default costs the partner no DNS at all (1.0s)
✓ 48 [chromium] › tests/e2e/partner-client-domain.spec.ts:80:5 › a partner with no verified zone is shown the way to one, not a dead control (919ms)
✓ 49 [chromium] › tests/e2e/partner-client-domain.spec.ts:103:5 › buying a domain through us is shown as unavailable, and refused if forced (3.4s)
✓ 50 [chromium] › tests/e2e/partner-client-domain.spec.ts:133:5 › once the restaurant is set up, the chooser is gone (928ms)
✓ 51 [chromium] › tests/e2e/partner-client-domain.spec.ts:156:5 › the founder can place a tenant under a proven partner zone, and sees whose it is (1.2s)
✓ 52 [chromium] › tests/e2e/partner-tenant-dns.spec.ts:47:5 › a client under the partner's own zone is told exactly which record to publish (893ms)
✓ 53 [chromium] › tests/e2e/partner-tenant-dns.spec.ts:78:5 › a client on our own base domain is asked for nothing (866ms)
✓ 54 [chromium] › tests/e2e/partner-tenant-panel.spec.ts:19:5 › a live client shows its tenant, what it includes and what it costs (982ms)
✓ 55 [chromium] › tests/e2e/partner-tenant-panel.spec.ts:58:5 › a partner can ask for a change, and it is kept as a note (1.1s)
✓ 56 [chromium] › tests/e2e/partner-tenant-panel.spec.ts:81:5 › an onboarding client with no tenant yet says what is being waited on (898ms)
✓ 57 [chromium] › tests/e2e/partner-tenant-panel.spec.ts:99:5 › a partner cannot open another partner's client (1.2s)
✓ 58 [chromium] › tests/e2e/partner-trial.spec.ts:48:5 › a partner inside the free period is told until when, and is never asked to pay (994ms)
✓ 59 [chromium] › tests/e2e/partner-trial.spec.ts:91:5 › once the free period has passed, the ask comes back and nothing else happens (899ms)
✓ 60 [chromium] › tests/e2e/partner-trial.spec.ts:127:5 › the founder can push a trial out, with a reason, and can never pull one in (2.8s)
✓ 61 [chromium] › tests/e2e/partner-trial.spec.ts:202:5 › a partner cannot reach the control that extends their own trial (901ms)
✓ 62 [chromium] › tests/e2e/self-serve-signup.spec.ts:43:7 › the public signup creates a payable account › a free slug mints an OWNER, a PENDING plan at the catalog price, and an invite (2.8s)
✓ 63 [chromium] › tests/e2e/self-serve-signup.spec.ts:80:7 › the public signup creates a payable account › an INVITED owner cannot log in until the invite is used (2.4s)
✓ 64 [chromium] › tests/e2e/self-serve-signup.spec.ts:91:7 › the public signup creates a payable account › invite -> password -> login lands on a dashboard showing the plan and a pay button (1.6s)
✓ 65 [chromium] › tests/e2e/self-serve-signup.spec.ts:128:7 › the mandate-lag window never invites a second payment › a paid first payment with a PENDING plan shows 'activating' and no pay button (3.9s)
✓ 66 [chromium] › tests/e2e/self-serve-signup.spec.ts:168:7 › the slug is refused while the customer can still fix it › a reserved slug is refused at the keyboard, with no request at all (426ms)
✓ 67 [chromium] › tests/e2e/self-serve-signup.spec.ts:183:7 › the slug is refused while the customer can still fix it › a taken slug is refused by the server, and the form keeps every answer (2.0s)
✓ 68 [chromium] › tests/e2e/self-serve-signup.spec.ts:207:7 › the slug is refused while the customer can still fix it › fixing the address on the spot succeeds (1.8s)
✓ 69 [chromium] › tests/e2e/self-serve-signup.spec.ts:223:7 › what the customer cannot fix becomes a founder lead › a second signup on the same email creates no second plan (1.9s)
✓ 70 [chromium] › tests/e2e/self-serve-signup.spec.ts:245:7 › what the customer cannot fix becomes a founder lead › resubmitting the same email AND slug is reported as a lead, never as 'taken' (646ms)
✓ 71 [chromium] › tests/e2e/self-serve-signup.spec.ts:262:7 › what the customer cannot fix becomes a founder lead › an admin's email is never repurposed into an owner (2.7s)
✓ 72 [chromium] › tests/e2e/self-serve-signup.spec.ts:276:7 › provisioning is gated on payment › an unpaid self-serve tenant is refused before any GitHub call (2.7s)
✓ 73 [chromium] › tests/e2e/self-serve-signup.spec.ts:293:7 › provisioning is gated on payment › a tenant with no plan at all is not gated — the founder's own path is untouched (912ms)
✓ 74 [chromium] › tests/e2e/self-serve-signup.spec.ts:311:7 › the signup answer says whether the welcome mail actually went out (G5) › the suite's own signups report emailed:false, and the copy follows the answer (2.0s)
✓ 75 [chromium] › tests/e2e/self-serve-signup.spec.ts:329:7 › the signup answer says whether the welcome mail actually went out (G5) › a signup whose welcome mail DID go out is told to check their email (406ms)
✓ 76 [chromium] › tests/e2e/self-serve-signup.spec.ts:350:7 › the founder can see which mails did not get delivered (G16) › a lead whose welcome mail failed is flagged on /admin/signups (979ms)
✓ 77 [chromium] › tests/e2e/self-serve-signup.spec.ts:372:7 › the account remembers which language the customer speaks (G9) › a signup filled in French mints a French account (430ms)
✓ 78 [chromium] › tests/e2e/trial-warning-cron.spec.ts:41:5 › the sweep endpoint is not an open relay (355ms)
✓ 79 [chromium] › tests/e2e/trial-warning-cron.spec.ts:54:5 › a partner is warned once, and a second sweep says nothing more (392ms)
✓ 80 [chromium] › tests/e2e/trial-warning-cron.spec.ts:103:5 › a plan that is already paying is never warned about its free period (681ms)
80 passed (2.0m))** — not a CI job here, so recorded on the PR.
Whole suite: 80 passed (1.9m).
Re-run scoped to the spec this PR edits, so the result is decisive rather than inferred from a total: bash scripts/e2e-suite.sh owner-payments-pending -> 3 passed (9.8s), including "bought and not yet granted — the card is there, and says nothing they can't act on", which now asserts a[href="https://dashboard.stripe.com"] has count 0 and that the replacement prose is visible.
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
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
paymentsLinkUrlcontract; sofra was deliberately left to this repo's owner, and its strings still asserted the retired Standard premise — that the restaurant supplies their ownacct_, and that a tenant is "not eligible" until they do.control.paymentsPending(the buyer's panel),admin.paymentsMode.notEligibleNotPaired,clientPaymentsMode.notEligibleNotPaired— six locale files each. (tenants.stripeAccountMissing,commissionEarnings.noAccountand the provision-form label were already rewritten in feat(payments): the platform mints the connected account, so one registry PR carries both halves (E3) #229.)PaymentsPendingPanel's outboundhttps://dashboard.stripe.comlink is replaced by prose, not deleted.Referrer-Policy: no-referreron the token route.Issue / plan link
Type
featThe link that had to go, and what replaced it
The panel renders exactly when a buyer paid for
online-paymentsand the entry does not grant it. Under Express that means our account creation did not happen. So:https://dashboard.stripe.compointed at a login the restaurant cannot use — an Express account has no full dashboard, and in this state they have no account at all.nextStep: what the short Stripe form will ask for (date of birth, phone, accepting Stripe's terms — the measuredcurrently_dueremainder), and why no link can be kept (one lives ~5 minutes).bodynow says we open the Stripe account. "There is nothing for you to do right now" stays, and is more true than before: the outstanding work is ours, not theirs.env/box/deploy/registry/pull request/founder in any of the six locales — and it is enforced by the existing test, which I mutated to prove it still bites.Contract check, against the MERGED refs rather than my worktree
git grep -F paymentsLinkUrl origin/develop -- src(frontend)PaymentsTab.tsx:194,types/paymentsOnboarding.ts:42, testsgit grep -F PaymentsLinkUrl origin/develop(backend)StripeSettings.cs:49(defaultstring.Empty),GetPaymentsOnboardingQuery.cs:115(IsNullOrWhiteSpace → null)git grep -iF dashboardurl origin/developdata.TryGetProperty("dashboardUrl", out _).Should().BeFalse()) — no live fieldrequirementsDue/PaymentsOnboardingStatedashboardUrl" result is realgrep -rn "dashboardUrl|dashboard.stripe.com" .Your three review points on #230
next.config.tsgets a second, later-matching entry for/:locale/onboarding/payments/:tokenwithno-referrer(last match wins in Next's merge). Verified against a runningnext start, with a positive control: that route answersReferrer-Policy: no-referrer,/en/legalanswers the site-widestrict-origin-when-cross-origin. One header, not two.git grep -nE "^\s*log\s*\{|log_file|access.log" origin/develop -- Caddyfile Caddyfile.staging caddy-tenants tenants/templatesin the deploy repo finds nothing, while plainlogmatches 7 lines in the same file (positive control). So no site explicitly configures access logging — which means Caddy's default behaviour decides, and its default emits access logs to stderr, i.e. into the container log. The token is therefore very likely indocker logson the box. I have not read a running container's log (no improvised SSH), so that last step is UNVERIFIED and the BACKLOG line will say so.findConnectAccountByTokentreats NULL as no match.NFR triage
no-referreron the credential-bearing route, verified live. No other surface changes.stripeLink→nextStep); parity script → "All 5 locales in parity (1244 keys each)" (it compares the five non-English bundles against English; six files, five comparisons).toHaveCount(0)plus a positive assertion on the replacement text) rather than removed — an absent link that nobody asserts is how a regression sneaks back.Test plan
typecheck·lint·test:coverage(1118) ·build·check-message-parity·check-single-file --allnext start, with a control pagenextStepstring rewritten to name "the deploy details" → the copy-rule test goes RED (landed viagrep -F,tsc0, checked before reading the result). So the rule that forbids naming our plumbing still bites on the string I added.npm run test:e2e:full— the unmocked suite that ownsowner-payments-pending.spec.ts; result appended to this PR as a comment (it is not a CI job here, so it is not gate-visible either way).Deploy notes
no-referrerentry only matches the onboarding route.