release(sofra): develop → main — onboard tenant picker (#49) + build-image CI (#48) - #50
Conversation
* docs(claude): rewrite §8 to GitFlow (develop = integration, main = releases) Also add a no-commit-to-branch pre-commit guard mirroring the server-side main-develop ruleset (commit-stage only; keeps the review-gate pre-push symlink). * ci: run CI on PRs to develop (GitFlow) — unblocks develop-based PRs
* docs(claude): rewrite §8 to GitFlow (develop = integration, main = releases) Also add a no-commit-to-branch pre-commit guard mirroring the server-side main-develop ruleset (commit-stage only; keeps the review-gate pre-push symlink). * ci: run CI on PRs to develop (GitFlow) — unblocks develop-based PRs
…mparison (sofra #6) (#42) AEO plan §2 slice: three localized public marketing pages under [locale], in all six locales, with per-page hreflang/canonical metadata + JSON-LD. - /changelog — dated what's-new entries (data-driven via components/changelog-data.ts + changelog.* messages; WebPage JSON-LD with dateModified = newest entry) - /case/rumi — RUMI Geneva case study, verifiable facts only (live since 2026-06-29, 10 guest languages, EU infra); the quantitative stats section is deliberately omitted (owner-owed), with an honest "where are the numbers?" note instead. Article JSON-LD. - /compare/gloriafood — honest, table-driven Sofra-vs-GloriaFood page with a visible "last checked" date; every GloriaFood claim verified 2026-07-10 against their public pages (URLs in components/compare-gloriafood-data.ts and rendered as a visible Sources section). Conservative wording for anything unverified (website/app add-on prices described qualitatively only). Cross-cutting: - lib/seo.ts: shared SITE_URL + pageAlternates/marketingPageMetadata (layout refactored to use it); unit-tested and added to the coverage floor scope - components/JsonLdScript.tsx: shared hardened JSON-LD renderer (JsonLd.tsx refactored onto it) - Header/Footer anchors switched to locale-aware /#anchor links so the nav works from subpages; footer links the three new pages - sitemap.xml now lists all 4 routes x 6 locales; llms.txt mentions the new pages - messages: +71 keys per locale, parity green across en/fr/de/nl/tr/ar Validation: typecheck, eslint, next build (all routes SSG x6 locales), vitest 89 passed + coverage floor, i18n parity, check-single-file --all, runtime smoke (200 + JSON-LD + hreflang + RTL verified on next start). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…sonLdScript cleanups, Readonly props (#43) Post-merge triage of the Gemini review + open Sonar issues on #42 (the PR merged before the async bot review landed — process gap fixed separately): - app/sitemap.ts: one new Date() hoisted out of the flatMap/map (Gemini MEDIUM) — also keeps lastModified consistent across all 24 entries. - components/JsonLdScript.tsx: stable keys (the serialized JSON-LD string, not the array index — S6479), replaceAll + String.raw for the "<" escape (S7781/S7780), Readonly props (S6759). Escaping behavior unchanged. - Readonly props on the three content pages (S6759). Declined with rationale (replied on the PR): switching the header /login link to LocaleLink — the control plane (login/dashboard/admin) is deliberately NOT locale-prefixed (sofra CLAUDE.md); LocaleLink would mis-route it. The independent pre-push review verified this distinction. typecheck clean · eslint 0 warnings · next build 43/43 SSG · vitest 89/89 · message parity 6 locales · file checker green. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…not main) (#44) The Standard checklist still said 'Branch off main, PR to main' — stale since the 2026-07-10 GitFlow reinstatement (CLAUDE.md §8: feature/fix/ chore/docs PRs target develop; only the develop → main release PR is a release). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
) * feat(billing): partner onboarding + self-serve monthly subscription Let an admin onboard a referred partner as the reseller payer for a tenant and hand them a pre-approval link; the partner sets a password, sees a welcome panel (go-live date + monthly amount), and starts an auto-monthly Mollie subscription themselves. First concrete case: RUMI (tenant #1, brought in via a partner). Reuses the existing invite -> set-password -> ACTIVE flow and the Mollie subscription engine (customer -> PENDING plan -> first-payment checkout -> webhook activation, all unchanged). The gap this fills: - Admin "Onboard a partner" action (requireAdmin): creates/reuses the PARTNER user + the tenant Client (linked by tenantSlug) + a PENDING plan with NO Mollie customer yet, mints the invite link, returns it for manual sharing. - Partner welcome hero on /dashboard + a /dashboard/billing page; nav branches by derived state (has-billing -> Plan; commissions/no-billing -> Ledger) so a reseller and a commission partner each see the right surface. - Partner-scoped startPaymentAction (requirePartner + client-ownership check + rate-limit) creates the Mollie customer + first-payment checkout on demand and redirects to Mollie server-side (progressively enhanced). Schema: TenantBilling.mollieCustomerId is now nullable (a plan can be defined before its first payment; kept @unique) + a display-only liveSince. Handwritten migration; the existing nullable call sites are adjusted. Money-safety (live Mollie key): - Refuse a second first payment once one is paid (the mandate-lag window where the sub still reads PENDING) -- server guard + the UI shows "processing", never a second pay button. - Idempotent mollieCustomerId claim so concurrent starts can't create two customers and strand a paid payment. EUR throughout; the commission ledger is untouched (it belongs to commission-type partners -- a separate, valid partner type). Out of scope (follow-ups): public direct-owner self-serve signup (OWNER role + payerUserId); commission automation. Verify: tsc, eslint, 103 vitest, i18n parity (500 x6), file-length, next build, and scripts/e2e-local.mjs end-to-end against a throwaway postgres (migration applies clean; onboard -> invite -> welcome -> billing all pass). Billing QA is on the test key only; the first live charge stays the owner's deliberate action. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test: put lib/billing-display.ts under the coverage floor The plan-state helper carries the double-charge guard (paid first payment -> 'processing', never a second pay button); pin it at the floor like the other pure lib modules. 100% stmts / 98% branches locally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(billing): bound the partner billing-payment reads (review gate) The dashboard welcome hero + startFirstPayment pulled a tenant's full BillingPayment history unpaginated in request paths. planState + the paid/reuse guards only need FIRST payments, so scope the include to sequenceType:first + take:20 — bounded and semantically exact (recurring history never enters these paths). Billing page keeps take:10 for its history display. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(billing): address Gemini + Sonar review on #45 Gemini: - onboarding: reject an impossible liveSince (e.g. 2026-02-31 passes the format regex but is not a real day) BEFORE any DB write — was a Prisma 500 + orphan user/client risk. - start-payment: rate-limit by user.id, not IP (authenticated action → no NAT collisions, no proxy-header dependency, nothing to spoof). Sonar: - billing page: extract the plan-status nested ternary into an if/else helper (S3358 x2). - StartPaymentButton: Readonly props (S6759). - start-payment: type-guard formData billingId instead of String(... ?? "") (S6551). typecheck/eslint/103 vitest+coverage/file-length/build green; e2e-local all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(onboarding): extract helpers to cut cognitive complexity (Sonar S3776) onboardPartnerAction tipped over Sonar's cognitive-complexity threshold after the liveSince guard. Extract resolvePartnerUser / resolveTenantClient / emailOnboardInvite — the main action is now mostly linear guard-and-go, same behavior. typecheck/eslint/build green; e2e-local all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…46 review) (#47) * fix(onboarding): robust liveSince date validation + orphan-user pre-check (#46 Gemini) Two medium comments from the release-PR review: - liveSince: centralize calendar validation in onboardSchema via .refine(). Note: Gemini's suggested isNaN check does NOT catch the bug it cites — new Date('2026-02-31T00:00:00Z') doesn't return Invalid Date, it silently ROLLS OVER to Mar 3. Round-trip instead (reconstruct + compare), with a NaN guard because the refine still runs when the regex failed (e.g. ''). - onboarding: pre-check tenant-slug ownership BEFORE resolvePartnerUser, so a slug held by a different partner can't leave a freshly-created user orphaned. Unit tests pin both impossible-date forms (Feb-31 rollover + month-13 NaN). tsc/eslint/104 vitest+coverage(100/98)/build green; e2e-local 24/24 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(onboarding): case-insensitive email compare in the slug-owner pre-check (#47 Gemini) Emails are case-insensitive; compare accordingly. All current paths store lowercased email, but don't rely on that in the ownership guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…fault_branch) (#48) build-image.yml gated the mutable :latest (app) and :migrate (DB-tooling) GHCR tags on {{is_default_branch}}, but the workflow only runs on pushes to main. GitFlow made `develop` the repo default branch (2026-07-10), so a release push to main has is_default_branch=false and both tags were silently dropped from the metadata-action tag list — while the immutable :sha-<sha>/:migrate-<sha> tags (no enable gate) still pushed. Result: GHCR :latest/:migrate froze at the last pre-flip build (release #46 / 4167a72 shipped :sha-4167a72 but :latest still pointed at the 2026-07-10 build), so a naive `pull :latest` rollout ships stale code and skips migrations. Gate both mutable tags on `github.ref == 'refs/heads/main'` instead — correct regardless of which branch is the repo default, and preserves current behaviour for v* tag pushes (they don't move :latest). Also add retag-mutable-tags.yml: a workflow_dispatch break-glass that re-points :latest/:migrate at an already-published :sha-<sha>/:migrate-<sha> via a server-side `imagetools` copy (used to correct the 4167a72 release now, and to roll :latest back to a known-good sha in future). Operator-supplied sha is env-passed + regex-validated per build-image.yml's injection posture. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#49) * feat(onboard): registry-driven tenant picker with partner-free filter Replace the free-text tenant slug + restaurant name on /admin/onboard with a registry-driven picker. The page joins the tenant registry with TenantBilling by slug (same pattern as /admin/tenants) to compute partner-free tenants; selecting one pre-fills the restaurant name + go-live date and surfaces the city/currency context. A "show all" toggle reveals onboarded/retired tenants (disabled). Falls back to the original free-text form + an inline banner when the registry can't be read. - lib/tenant-registry.ts: optional live_since (YYYY-MM-DD) field - lib/onboard-tenants.ts: pure registry-billing join helper (unit-tested) - server action + onboardSchema unchanged (slug regex still validates the value) - e2e-local.mjs: drives the picker + asserts the partner-free filter - 7 new i18n keys x 6 locales (parity green) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(onboard): reject impossible live_since dates; drop non-null assertion Gemini review triage: - lib/tenant-registry.ts: add a round-trip refine to live_since so a format-valid-but-impossible date (e.g. 2026-02-31) fails the registry load loudly instead of silently rolling over + pre-filling a phantom day. Mirrors onboardSchema.liveSince. Test fixture updated to the impossible-date case. - OnboardPartnerForm.tsx: replace `tenants!` with `tenants ?? []` in the show-all path (defensive; no behaviour change — the toggle only renders in registry mode). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(onboard): clear SonarCloud smells on the picker - Readonly props (S6759) - flatten the optionLabel nested ternary (S3358) + nested template literal (S4624) into a cityPart var + if/else badge - String.raw for the e2e selectOptions regex (S7780) All behaviour-preserving (regex + label output proven identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces partner onboarding and self-serve billing features, allowing admins to define pending plans for tenants and partners to complete payments later. It also adds new marketing pages, including a RUMI case study, a GloriaFood comparison, and a changelog, along with corresponding tests and layout updates. The review feedback suggests updating existing CRM clients' status to 'LIVE' during onboarding to maintain consistency, and adding a time-based expiration check (e.g., 1 hour) before reusing open Mollie checkout URLs to prevent errors from expired sessions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Code Review
This pull request implements partner onboarding and self-serve billing (the reseller flow), allowing admins to define pending plans for referred partners who can then complete payments via Mollie from their dashboards. It also introduces new marketing pages (RUMI case study, GloriaFood comparison, and a changelog) with localized SEO metadata, and updates the Git workflow to GitFlow. Feedback on the changes suggests wrapping the onboarding email dispatch in a try-catch block to prevent action failures during mail provider outages, utilizing case-insensitive lookups for existing users to avoid duplicate accounts, and safely handling potentially null customer IDs during payment recording.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
chore(gitflow): back-merge main → develop (reconcile history, 0 content change)
|



Release:
develop→main(sofra)Content delta since the last release (
4167a72):/admin/onboardtenant picker (partner-free filter,live_sincepre-fill, free-text fallback). Newlib/onboard-tenants.ts, optionallive_sinceregistry field, 7 i18n keys × 6 locales, extendede2e-local.mjs, unit tests.build-image.ymladvances:latest/:migrateon main releases (notis_default_branch) +retag-mutable-tags.yml.Safety / scope:
prisma/schema.prismaorprisma/migrations/(the partner-onboarding migration shipped in the previous release4167a72and is already applied on the box).TENANT_REGISTRY_PATHis already set on the box for/admin/tenants.4167a72and is already live.develop(CI + code-reviewer + Gemini + Sonar).Deploy notes (merge ≠ deploy):
ghcr.io/piwas-21/sofra:latest. Rollout is the manual staging-box step:docker compose -f docker-compose.prod.yml pull sofra && up -d sofra.live_sincepre-fill lights up once the companion deploy release (registry.yml) syncs to the box.