From 4167a723326c6a85cee9915ff1e668703338d528 Mon Sep 17 00:00:00 2001 From: mahmutKaya <33642821+mahmutkaya@users.noreply.github.com> Date: Sun, 12 Jul 2026 01:05:22 +0200 Subject: [PATCH] =?UTF-8?q?release(sofra):=20develop=20=E2=86=92=20main=20?= =?UTF-8?q?=E2=80=94=20partner=20onboarding=20(#45)=20+=20AEO=20content=20?= =?UTF-8?q?(#42/#43)=20+=20GitFlow=20docs=20(#46)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(claude): rewrite §8 to GitFlow (#40) * 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 * ci+docs: run CI on PRs to develop (GitFlow) + branch guard + §8 (#41) * 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 * feat(aeo): content engine — changelog, RUMI case study, GloriaFood comparison (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 * fix(aeo): PR #42 Gemini/Sonar follow-ups — sitemap timestamp hoist, JsonLdScript 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 * docs: fix stale PR-template branch line for GitFlow (target develop, 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 * feat(billing): partner onboarding + self-serve monthly subscription (#45) * 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 * 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 * 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 * 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 * 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 --------- Co-authored-by: Claude Opus 4.8 * fix(onboarding): robust liveSince validation + orphan-user pre-check (#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 * 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 --------- Co-authored-by: Claude Opus 4.8 --------- Co-authored-by: Claude Fable 5 --- .github/pull_request_template.md | 2 +- .github/workflows/ci.yml | 4 +- .pre-commit-config.yaml | 20 ++ CLAUDE.md | 2 +- app/(control)/admin/billing/[id]/page.tsx | 2 +- app/(control)/admin/layout.tsx | 1 + app/(control)/admin/onboard/page.tsx | 30 ++ app/(control)/dashboard/billing/page.tsx | 116 ++++++++ app/(control)/dashboard/layout.tsx | 26 +- app/(control)/dashboard/page.tsx | 65 ++++- app/[locale]/case/rumi/page.tsx | 149 ++++++++++ app/[locale]/changelog/page.tsx | 84 ++++++ app/[locale]/compare/gloriafood/page.tsx | 156 ++++++++++ app/[locale]/layout.tsx | 12 +- app/sitemap.ts | 22 +- components/CompareTable.tsx | 45 +++ components/Footer.tsx | 38 ++- components/Header.tsx | 29 +- components/JsonLd.tsx | 20 +- components/JsonLdScript.tsx | 21 ++ components/changelog-data.ts | 13 + components/compare-gloriafood-data.ts | 34 +++ components/control/OnboardPartnerForm.tsx | 92 ++++++ components/control/StartPaymentButton.tsx | 26 ++ lib/actions/billing-actions.ts | 6 +- lib/actions/onboarding-actions.ts | 136 +++++++++ lib/actions/partner-billing-actions.ts | 60 ++++ lib/billing-display.ts | 29 ++ lib/billing-onboarding.ts | 166 +++++++++++ lib/billing.ts | 7 +- lib/seo.ts | 43 +++ lib/validation.ts | 32 +++ messages/ar.json | 268 +++++++++++++++++- messages/de.json | 268 +++++++++++++++++- messages/en.json | 268 +++++++++++++++++- messages/fr.json | 268 +++++++++++++++++- messages/nl.json | 268 +++++++++++++++++- messages/tr.json | 268 +++++++++++++++++- .../migration.sql | 11 + prisma/schema.prisma | 8 +- public/llms.txt | 8 + scripts/e2e-local.mjs | 64 ++++- tests/unit/billing-display.test.ts | 46 +++ tests/unit/seo.test.ts | 49 ++++ tests/unit/validation.test.ts | 48 ++++ vitest.config.ts | 2 + 46 files changed, 3230 insertions(+), 102 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 app/(control)/admin/onboard/page.tsx create mode 100644 app/(control)/dashboard/billing/page.tsx create mode 100644 app/[locale]/case/rumi/page.tsx create mode 100644 app/[locale]/changelog/page.tsx create mode 100644 app/[locale]/compare/gloriafood/page.tsx create mode 100644 components/CompareTable.tsx create mode 100644 components/JsonLdScript.tsx create mode 100644 components/changelog-data.ts create mode 100644 components/compare-gloriafood-data.ts create mode 100644 components/control/OnboardPartnerForm.tsx create mode 100644 components/control/StartPaymentButton.tsx create mode 100644 lib/actions/onboarding-actions.ts create mode 100644 lib/actions/partner-billing-actions.ts create mode 100644 lib/billing-display.ts create mode 100644 lib/billing-onboarding.ts create mode 100644 lib/seo.ts create mode 100644 prisma/migrations/20260711214957_partner_onboarding/migration.sql create mode 100644 tests/unit/billing-display.test.ts create mode 100644 tests/unit/seo.test.ts diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f265157..7f175d7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -44,7 +44,7 @@ - [ ] Craft tokens only (no ad-hoc hex); dark mode via `.dark` class - [ ] Money handled as EUR integer cents - [ ] No secrets/keys in the diff -- [ ] Branch off `main`, PR to `main` +- [ ] Branch off `develop`, PR to `develop` (releases ship via a `develop` → `main` release PR — CLAUDE.md §8) ## Test plan