feat(retailer): self-signup as first-class organizations - #235
Merged
Conversation
Adds a `stores` directory table + `store_users` so retailers can sign up unaided and land on the existing buyer portal. Rejects `org_type='store'` to avoid a third federation direction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 tasks, TDD. Corrects three spec errors found while reading the real files: /shop already has its empty state, /dashboard has none plus two dead links, and there is no automated RLS test harness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t loop Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nsight Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reference-identity + local-override + drift-stewardship model. Pin on edit, warn only the diverging brand on the diverged field, reconcile wholesale or per-field, surfaced at point-of-use incl. the order. Substrate (retailers table vs org_type='retailer') left explicitly open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retailers become org_type='retailer' with organization_members; hook forks the membership branch on org_type so retailer-org members reach the buyer portal. Supersedes the v1 retailers/retailer_users tables. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reworks the unmerged v1 retailers/retailer_users tables into first-class organizations. A retailer is now an organizations row (org_type='retailer') whose founder is an organization_members admin, reaching the buyer portal via isBuyer resolved from org_type in hooks.server.ts. - migration: drop retailers/retailer_users + helpers; widen org_type CHECK to include 'retailer'; accounts.retailer_id -> accounts.retailer_org_id (SP4 seam) - createRetailer: insert organizations + organization_members (admin), idempotent on an existing retailer-org membership, slug + 409 collision like create-org; no self-brand/seasons/shipping seeding - hooks: fork the membership branch on org_type (retailer -> empty buyer context, /dashboard); fork the /login redirect; drop locals.retailer - buyer-context: query only account_users (retailer handled in the hook) - auth/callback + onboarding: route retailer-org members to /dashboard - types: OrgType += 'retailer'; drop Retailer/RetailerUser; app.d.ts drops retailer Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rd, routing helper Addresses adversarial review of the SP1 retailer-org rework: - createRetailer idempotency now filters org_type='retailer' server-side via an inner join, so a user who admins both a rep/brand org and a retailer org can't fall through and mint a duplicate retailer - roll back (delete) the just-created org if the membership insert fails — otherwise an ownerless org holds the slug forever and 409s every retry - set onboarding_completed_at atomically in the org INSERT; drop the unchecked client-side completion write in the onboarding wizard - block buyers (retailer-org members AND legacy account_users buyers) from the org AI endpoint via `|| locals.isBuyer` in the 401 guard - move the profiles.display_name write to after successful org+member creation so a 409/500 no longer mutates the profile - retailer hook branch sets brandScope/scopedBrandNames = null explicitly - extract landingPathForOrgType() helper, used by both the /login fork and auth/callback so they can't drift; add landing.test.ts (3 org types) - migration: DROP CONSTRAINT IF EXISTS for fresh-DB robustness Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend get_buyer_account_ids() — the single choke point every buyer RLS policy resolves through — to also return accounts linked to a retailer org the caller belongs to, via accounts.retailer_org_id. One function; every downstream buyer policy (brands, products, variants, images, accounts, orders, order_lines, seasons) inherits retailer scope. No new policies. Security: link is necessary but not sufficient — a retailer sees a brand only when a linked account ALSO has an account_brand_access grant (the brand stays in control). Scoped to the caller's own memberships and org_type='retailer'; legacy account_users buyers untouched (UNION only adds). Proven end-to-end against real local RLS (11/11): link+grant sees brand/product, link-without-grant sees neither, cross-tenant sees nothing. hooks.server.ts retailer branch now resolves buyerAccounts/buyerBrandIds from linked accounts via new resolveRetailerBuyerContext(), mirroring the RLS helper in the app layer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
dev extracted hooks.server.ts context-loading into src/lib/server/auth.ts (loadUserContext/applyUserContext, a discriminated union of identity kinds). Re-home the retailer identity into that structure rather than the old inline hook: - auth.ts: add a fifth 'retailer' kind — a hybrid that carries org-member locals (membership, allMemberships, orgType='retailer') AND buyer locals (isBuyer, buyerAccounts, buyerBrandIds). loadUserContext resolves it via resolveRetailerBuyerContext (accounts linked by retailer_org_id); the branch precedes brand-scope/SSO, which retailers skip. - hooks.server.ts: take dev's switch orchestration; 'retailer' (like 'buyer') falls through with no redirect → lands on /dashboard. - auth/callback: keep both dev's isSsoSession (gated domain auto-join) and SP1's landingPathForOrgType (retailer → /dashboard, rep/brand → /insight). - auth.test.ts: add loadUserContext + applyUserContext coverage for the retailer branch, matching dev's per-branch test pattern. Verified: 761 tests pass, bun run check 0 errors, retailer-shopping RLS harness 11/11 against real local DB. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…outing Address PR #235 review: - Remove resolveBuyerContext + its tests. dev's loadUserContext inlines the invited-buyer resolution, so this export was dead in production and its tests gave false confidence (they exercised a copy, not the live path). The two could drift silently. resolveRetailerBuyerContext (the live SP3 path) stays. - /login fork: a membership-less legacy account_users buyer went to /insight and was bounced to /dashboard by the insight guard (two hops). Now they route to /dashboard directly, and a not-yet-onboarded user to /onboarding, in one hop — mirroring auth/callback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
What
Adds retailer self-signup and models retailers as first-class organizations (
org_type='retailer'), replacing the initial bespoke-table approach. A retailer signs up, becomes anorganizationsrow whose founder is an admin member, and lands on the buyer portal.The arc
org_typewidens to('rep','brand','retailer'). A retailer founder is anorganization_membersadmin (no self-brand/seasons/shipping). The buyer portal is reached byorg_type, not by absence-of-membership — the hook forks the membership branch on org type so a retailer member isisBuyer=trueand routes to/dashboard. Resolves the redirect loop where a self-signup retailer had noaccount_usersrow.create-retailerendpoint.get_buyer_account_ids()— the single choke point every buyer RLS policy resolves through — to also return accounts linked to a retailer org viaaccounts.retailer_org_id. Every downstream buyer policy inherits retailer scope; no new policies. Currently inert: nothing setsretailer_org_idyet (that's SP4 discovery/linking), so live access is unchanged until then./shop/[productId]to buyers with an empty brand-access list (empty scope was widening instead of denying).Security
org_type='retailer'. Link is necessary but not sufficient — a retailer sees a brand only when a linked account also has anaccount_brand_accessgrant, so the brand stays in control.account_usersbuyers are untouched (the extension is aUNIONthat only adds rows).Verification
bun run check— 0 errors.bun run test:run— 734 passing (incl. new buyer-context + landing tests).Not in this PR (deferred)
accounts.retailer_org_id, account unification, and drift reconciliation (phase-2 spec committed).Migrations
20260709000001_retailer_org.sql— widenorg_type; addaccounts.retailer_org_id.20260709000002_retailer_buyer_account_ids.sql— extendget_buyer_account_ids().🤖 Generated with Claude Code