Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
# User dashboard feature catalog (agent 1/10)
|
||||
|
||||
Inventory of signed-in **user dashboard** surfaces: everything rendered in the dashboard shell (`+layout.svelte` branch that is not marketing, auth, or `/admin/*`). Sources: `Nav.svelte`, route `+page.svelte` files, `plan-gates.ts`, `billing-display.ts`, `pricing-data.ts`, `entitlements.go`, `AssertCanStartProcessing`, and UI upgrade banners.
|
||||
|
||||
**Plan ladder (API defaults):** Free → Starter → Plus → Growth → Business → Scale → Enterprise (`apps/api/internal/billing/service.go` `defaultPublicPlans`, `apps/web/src/lib/components/pricing/pricing-data.ts`).
|
||||
|
||||
| Plan | SKU cap | Monthly AI credits | Paid (`is_paid_plan`) |
|
||||
|------|---------|-------------------|------------------------|
|
||||
| Free | 50 | 0 | No |
|
||||
| Starter | 100 | 100 | Yes |
|
||||
| Plus | 400 | 400 | Yes |
|
||||
| Growth | 1,200 | 1,200 | Yes |
|
||||
| Business | 4,000 | 4,000 | Yes |
|
||||
| Scale | 12,000 | 12,000 | Yes |
|
||||
| Enterprise | Unlimited (`max_products` null) | 1,000,000 wallet grant | Yes (custom) |
|
||||
|
||||
**Enforced today (API):** product SKU cap (`ErrProductLimitExceeded`), AI credit wallet + `can_use_ai` for AI-only processing (`ErrAIRequiresUpgrade` / `ErrInsufficientCredits`), email live-send blocked on Free (`email/service.go` dry-run), campaign AI generate (`campaigns/generate_send.go`), brand voice injection in AI prompts (`AIBrandApplyAllowed` → paid or trial). EPREL is **not** plan-gated (`CanUseEPREL` always true).
|
||||
|
||||
**Marketing copy only (not found enforced in API):** per-plan feed-source counts, export-feed counts, storage GB, WooCommerce “test vs full sync”, API read vs full access tiers, BYOK add-on packaging. Flag these when designing admin toggles — they need new enforcement or stay documentation-only.
|
||||
|
||||
---
|
||||
|
||||
## Admin toggle groups
|
||||
|
||||
Use these sections when grouping `feature_key` toggles in an admin plan editor.
|
||||
|
||||
| Section key | Scope |
|
||||
|-------------|--------|
|
||||
| `shell` | Global chrome: nav, header, command palette, tutorial, company switcher |
|
||||
| `dashboard` | Home / overview |
|
||||
| `catalog` | Products, categories, attributes, standard fields, structured descriptions |
|
||||
| `feeds` | Import feeds, mapping, export feeds, file uploads |
|
||||
| `stores` | Store hub, WooCommerce, Shopify connectors |
|
||||
| `processing` | Background job monitor |
|
||||
| `marketing` | Campaigns, calendar, brand, SEO, reviews |
|
||||
| `integrations` | AI BYOK, email sending |
|
||||
| `billing` | Billing, plans, checkout |
|
||||
| `settings` | Account, company, alerts, API keys, team |
|
||||
| `support` | Support center |
|
||||
| `capabilities` | Cross-cutting gates (AI, SKU cap, email send, etc.) |
|
||||
|
||||
---
|
||||
|
||||
## Shell (`shell`)
|
||||
|
||||
| feature_key | UI location | User actions | Default by plan |
|
||||
|-------------|-------------|--------------|-------------------|
|
||||
| `shell.navigation` | Sidebar `Nav.svelte` — primary + More (Setup / Operate) | Navigate all dashboard routes; mobile drawer | All plans |
|
||||
| `shell.command_palette` | Header area + `CommandPalette.svelte` (⌘K) | Quick-jump to Products, Feeds, Processing, Export, Settings, Billing, Support | All plans |
|
||||
| `shell.company_switcher` | `DashboardHeader` → `CompanySwitcher.svelte` | Switch active company/workspace | All plans (multi-company users) |
|
||||
| `shell.support_notifications` | `SupportNotificationBell.svelte` | Poll staff replies; open support tickets | All plans |
|
||||
| `shell.tutorial` | Header tutorial buttons + `TutorialOverlay.svelte` | Start / resume / browse guided tour | All plans |
|
||||
| `shell.account_menu` | Header avatar → `/settings` | Open account settings | All plans |
|
||||
| `shell.billing_recovery_banner` | `BillingRecoveryBanner.svelte` (layout) | Past-due or missing-plan recovery CTAs | All plans (state-driven) |
|
||||
|
||||
---
|
||||
|
||||
## Dashboard (`dashboard`)
|
||||
|
||||
| feature_key | Route / component | User actions | Default by plan |
|
||||
|-------------|-------------------|--------------|-------------------|
|
||||
| `dashboard.overview` | `/dashboard` — `dashboard/+page.svelte` | Welcome, plan/credit summary | All |
|
||||
| `dashboard.stats` | `DashboardStats.svelte` | View SKU count, active jobs, credits used/remaining | All |
|
||||
| `dashboard.quick_links` | Dashboard shortcuts section | Jump to Products, Feeds, Processing | All |
|
||||
| `dashboard.recent_jobs` | Dashboard jobs list | View recent processing jobs, open Processing | All |
|
||||
| `dashboard.news_feed` | `NewsFeed.svelte` | Product news / announcements | All |
|
||||
| `dashboard.activation_checklist` | `ActivationChecklist.svelte` | First-run onboarding steps | All |
|
||||
| `dashboard.migrated_checklist` | `MigratedCohortChecklist.svelte` | Post-migration cutover checklist (cohort flag) | All (cohort) |
|
||||
| `dashboard.etl_gaps` | `MigratedEtlGapsPanel.svelte` | Migration gap warnings | All (cohort) |
|
||||
| `dashboard.store_reconnect` | `StoreReconnectBanner.svelte` | Reconnect broken store credentials | All |
|
||||
| `dashboard.upgrade_banners` | `UpgradeBanner.svelte` on dashboard | Free plan, out-of-credits, SKU limit, trial, low credits | **Gated messaging** — see capabilities |
|
||||
|
||||
---
|
||||
|
||||
## Catalog (`catalog`)
|
||||
|
||||
### Products — `/products`
|
||||
|
||||
| feature_key | UI location | User actions | Default by plan |
|
||||
|-------------|-------------|--------------|-------------------|
|
||||
| `catalog.products` | Nav **Products** → `products/+page.svelte` | Browse, filter, sort, select products | All |
|
||||
| `catalog.products.tab_processed` | `ProductTabs.svelte` | View completed processed products | All |
|
||||
| `catalog.products.tab_needs_review` | Product tabs | Review AI enrichment before accept | All (needs AI run) |
|
||||
| `catalog.products.tab_error` | Product tabs | View failed processing rows | All |
|
||||
| `catalog.products.tab_processing` | Product tabs | View in-flight rows | All |
|
||||
| `catalog.products.tab_unprocessed` | Product tabs | View raw / unprocessed inventory | All |
|
||||
| `catalog.products.process_categories` | `ProductProcessingActions` | Run category assignment (no AI credits) | All |
|
||||
| `catalog.products.process_attributes` | Processing actions | Normalize specs / fill attributes (no AI) | All |
|
||||
| `catalog.products.process_ai_titles` | Processing actions | AI title generation | **Starter+** or Free with credits |
|
||||
| `catalog.products.process_ai_descriptions` | Processing actions | AI description generation | **Starter+** or Free with credits |
|
||||
| `catalog.products.enrichment_review` | `ProductEditPanel.svelte` | Accept / reject / edit AI fields | All (after AI run) |
|
||||
| `catalog.products.inline_rename` | Product table | Quick rename processed titles | All |
|
||||
| `catalog.products.export_selection` | Processing actions export | Export selected products | All |
|
||||
| `catalog.products.upgrade_prompt` | Upgrade banners on products page | Free-plan and credit-limit CTAs | **Already gated** (UI) |
|
||||
|
||||
### Categories — `/categories`, `/categories/[id]/title-formula`, `/categories/[id]/description-formula`
|
||||
|
||||
| feature_key | UI location | User actions | Default by plan |
|
||||
|-------------|-------------|--------------|-------------------|
|
||||
| `catalog.categories` | Nav More → Categories | List/edit category tree | All |
|
||||
| `catalog.categories.title_formula` | Title formula page | Build title formulas with variables | Free: basic (1 cat marketing); Starter+: full |
|
||||
| `catalog.categories.description_formula` | Description formula page | Build description formulas | Same as title formulas |
|
||||
|
||||
### Attributes — `/attributes`
|
||||
|
||||
| feature_key | UI location | User actions | Default by plan |
|
||||
|-------------|-------------|--------------|-------------------|
|
||||
| `catalog.attributes` | Nav More → Attributes | Define attribute definitions | All |
|
||||
| `catalog.attributes.bulk_import` | Attributes **Bulk Import** tab | CSV bulk import attributes | All |
|
||||
|
||||
### Standard fields — `/standard-fields`
|
||||
|
||||
| feature_key | UI location | User actions | Default by plan |
|
||||
|-------------|-------------|--------------|-------------------|
|
||||
| `catalog.standard_fields` | Nav More → Standard Fields | Manage field dictionary | All |
|
||||
| `catalog.standard_fields.groups` | Standard Fields **Groups** tab | Organize fields into groups | All |
|
||||
|
||||
### Other catalog pages (no nav link)
|
||||
|
||||
| feature_key | Route | User actions | Default by plan |
|
||||
|-------------|-------|--------------|-------------------|
|
||||
| `catalog.structured_descriptions` | `/structured-descriptions` | Structured description field config | All |
|
||||
| `catalog.vector_categories` | `/vector-categories` | Dev/test vector index (Pinecone) | All if Pinecone configured |
|
||||
|
||||
---
|
||||
|
||||
## Feeds (`feeds`)
|
||||
|
||||
### Import feeds — `/feeds`
|
||||
|
||||
| feature_key | UI location | User actions | Default by plan |
|
||||
|-------------|-------------|--------------|-------------------|
|
||||
| `feeds.list` | Nav **Feeds** | List supplier feeds, search, stats | All (Free marketing: 1 source) |
|
||||
| `feeds.add_url` | Add feed dialog **Feed URL** tab | Add HTTP(S) feed URL | All |
|
||||
| `feeds.add_csv` | Add feed **CSV file** tab | Upload CSV as feed source | All |
|
||||
| `feeds.sync` | Per-feed sync actions | Trigger feed sync (FTP unsupported in UI) | All |
|
||||
| `feeds.delete` | Feed row actions | Delete feed | All |
|
||||
|
||||
### Feed mapping — `/feeds/[feedId]/mapping`
|
||||
|
||||
| feature_key | UI location | User actions | Default by plan |
|
||||
|-------------|-------------|--------------|-------------------|
|
||||
| `feeds.mapping` | From feeds list → Map | Map supplier fields to standard fields | All |
|
||||
| `feeds.mapping.select_item` | Mapping tab 1 | Pick XML/CSV item element | All |
|
||||
| `feeds.mapping.map_fields` | Mapping tab 2 | Column / path mapping, transforms | All |
|
||||
| `feeds.mapping.queue_processing` | Post-map CTA | Queue products for processing after sync | All (AI steps follow capability gates) |
|
||||
|
||||
### Export feeds — `/export-feeds`
|
||||
|
||||
| feature_key | UI location | User actions | Default by plan |
|
||||
|-------------|-------------|--------------|-------------------|
|
||||
| `feeds.export_feeds` | Nav **Export feeds** | List channel export definitions | All (Free marketing: 1 export) |
|
||||
| `feeds.export_feeds.create` | Create / edit dialog | Configure export format & filters | All |
|
||||
| `feeds.export_feeds.generate` | Generate action | Run export generation (rate-limited API) | All |
|
||||
| `feeds.export_feeds.public_url` | Public URL display | Copy signed export URL | All |
|
||||
|
||||
### Uploads — `/files`
|
||||
|
||||
| feature_key | Route | User actions | Default by plan |
|
||||
|-------------|-------|--------------|-------------------|
|
||||
| `feeds.uploads` | `/files` | List/delete CSV uploads; link to product import | All |
|
||||
|
||||
---
|
||||
|
||||
## Stores (`stores`)
|
||||
|
||||
### Hub — `/stores`
|
||||
|
||||
| feature_key | UI location | User actions | Default by plan |
|
||||
|-------------|-------------|--------------|-------------------|
|
||||
| `stores.hub` | Nav **Stores** | Overview of WooCommerce / Shopify cards | All |
|
||||
| `stores.hub.api_keys_link` | Stores page | Deep-link to Settings → API keys | All |
|
||||
|
||||
### WooCommerce — `/woocommerce`
|
||||
|
||||
| feature_key | Tab / section | User actions | Default by plan |
|
||||
|-------------|---------------|--------------|-------------------|
|
||||
| `stores.woocommerce` | Nav More → Reviews uses `?tab=reviews` | Full WooCommerce integration page | All |
|
||||
| `stores.woocommerce.connection` | Connection tab | Store URL, credentials, test connection | Free: test/limited (marketing); Starter+: full |
|
||||
| `stores.woocommerce.categories` | Categories tab | Sync categories to/from Woo | Plan-dependent (marketing) |
|
||||
| `stores.woocommerce.attributes` | Attributes tab | Sync attributes | Plan-dependent (marketing) |
|
||||
| `stores.woocommerce.orders` | Orders tab | View recent orders | All |
|
||||
| `stores.woocommerce.reviews` | Reviews tab | Manage product reviews | All |
|
||||
| `stores.woocommerce.settings` | Settings tab | Sync options, overwrite guards | All |
|
||||
| `stores.woocommerce.queue_sync` | Push products to Woo | Queue catalog sync jobs | Gated by processing/SKU caps |
|
||||
|
||||
### Shopify — `/stores/shopify` (also `/shopify` legacy)
|
||||
|
||||
| feature_key | Tab | User actions | Default by plan |
|
||||
|-------------|-----|--------------|-------------------|
|
||||
| `stores.shopify` | Stores hub → Shopify | Shopify connector | All |
|
||||
| `stores.shopify.connection` | Connection tab | OAuth / API connection | All |
|
||||
| `stores.shopify.orders` | Orders tab | View orders | All |
|
||||
| `stores.shopify.settings` | Settings tab | Sync settings | All |
|
||||
| `stores.shopify.queue_sync` | Sync actions | Push products to Shopify | Gated by processing/SKU caps |
|
||||
|
||||
---
|
||||
|
||||
## Processing (`processing`)
|
||||
|
||||
| feature_key | Route | User actions | Default by plan |
|
||||
|-------------|-------|--------------|-------------------|
|
||||
| `processing.monitor` | Nav More → **Processing** (`/processing`, alias `/tasks`) | Live job list, refresh, auto-poll | All |
|
||||
| `processing.job_cancel` | Job row actions (if present) | Cancel / retry failed jobs | All |
|
||||
| `processing.pipeline_steps` | Job detail / description | normalize → specs → fill → EPREL → AI | EPREL all; AI gated |
|
||||
|
||||
---
|
||||
|
||||
## Marketing (`marketing`)
|
||||
|
||||
### Campaigns — `/campaigns`, `/campaigns/new`, `/campaigns/[id]`
|
||||
|
||||
| feature_key | UI location | User actions | Default by plan |
|
||||
|-------------|-------------|--------------|-------------------|
|
||||
| `marketing.campaigns` | Nav More → Campaigns | List seasonal email campaigns | All |
|
||||
| `marketing.campaigns.create` | `/campaigns/new` → `CampaignWizard.svelte` | Pick season, audience, products | All |
|
||||
| `marketing.campaigns.generate_ai` | Wizard generate step | AI email draft generation | **Starter+** (`showUpgrade` on Free) |
|
||||
| `marketing.campaigns.send` | Wizard / campaign detail | Send or schedule blast | Free: dry-run only (email service) |
|
||||
| `marketing.campaigns.edit_content` | Campaign editor | Edit subject/body HTML | All |
|
||||
|
||||
### Content calendar — `/marketing/calendar`
|
||||
|
||||
| feature_key | Route | User actions | Default by plan |
|
||||
|-------------|-------|--------------|-------------------|
|
||||
| `marketing.content_calendar` | Nav More → Content calendar | Seasonal presets, link campaigns to dates, export feed prep | All |
|
||||
|
||||
### Brand — `/brand`
|
||||
|
||||
| feature_key | Route | User actions | Default by plan |
|
||||
|-------------|-------|--------------|-------------------|
|
||||
| `marketing.brand_kit` | Nav More → Brand | Edit voice, colors, logo, preferred terms | All (view); admin edit |
|
||||
| `marketing.brand_ai_apply` | Brand + formula previews | Inject brand voice into AI enhance prompts | **Starter+** (`ai_apply_allowed` API) |
|
||||
|
||||
### SEO — `/seo`
|
||||
|
||||
| feature_key | Route | User actions | Default by plan |
|
||||
|-------------|-------|--------------|-------------------|
|
||||
| `marketing.seo` | Nav More → SEO | SEO checklist scores, recommendations | All |
|
||||
| `marketing.seo.template_fill` | SEO actions | Rule-based meta template fill | All |
|
||||
| `marketing.seo.ai_rewrite` | SEO actions | AI meta rewrite | **Starter+** or credits (`can_use_ai`) |
|
||||
|
||||
### Reviews — `/woocommerce?tab=reviews`
|
||||
|
||||
| feature_key | Route | User actions | Default by plan |
|
||||
|-------------|-------|--------------|-------------------|
|
||||
| `marketing.reviews` | Nav More → Reviews | WooCommerce reviews tab | All |
|
||||
|
||||
---
|
||||
|
||||
## Integrations (`integrations`)
|
||||
|
||||
### AI — `/integrations/ai`
|
||||
|
||||
| feature_key | Route | User actions | Default by plan |
|
||||
|-------------|-------|--------------|-------------------|
|
||||
| `integrations.ai` | Nav More → AI integrations | Configure OpenAI / platform AI modes | All |
|
||||
| `integrations.ai.byok` | AI integrations page | Bring-your-own API key | Growth+ marketing (BYOK add-on); Business included |
|
||||
|
||||
### Email — `/integrations/email`
|
||||
|
||||
| feature_key | Route | User actions | Default by plan |
|
||||
|-------------|-------|--------------|-------------------|
|
||||
| `integrations.email` | Nav More → Email sending | Resend or SMTP credentials | All |
|
||||
| `integrations.email.test` | Test send | Send test email (may dry-run) | Free: dry-run |
|
||||
| `integrations.email.blast` | Blast dialog | Marketing blast to audience | Free: dry-run forced |
|
||||
|
||||
---
|
||||
|
||||
## Billing (`billing`)
|
||||
|
||||
| feature_key | Route | User actions | Default by plan |
|
||||
|-------------|-------|--------------|-------------------|
|
||||
| `billing.overview` | Nav **Billing** `/billing` | Plan name, credits, SKU usage, subscription status | All |
|
||||
| `billing.customer_portal` | Billing page | Open Stripe Customer Portal (company admin) | Paid plans with Stripe |
|
||||
| `billing.quick_upgrade` | Billing quick upgrade cards | Checkout Starter / Growth / Business | Company admin |
|
||||
| `billing.add_credits` | Billing dialog (if shown) | Purchase extra credits | Paid |
|
||||
| `billing.plans_compare` | `/plans` (linked from banners) | Compare public ladder, feature matrix | All |
|
||||
| `billing.checkout` | `/plans` + Billing | Stripe Checkout self-serve upgrade | Company admin; Starter–Business |
|
||||
|
||||
---
|
||||
|
||||
## Settings (`settings`)
|
||||
|
||||
| feature_key | Tab (`/settings?tab=`) | User actions | Default by plan |
|
||||
|-------------|------------------------|--------------|-------------------|
|
||||
| `settings.profile` | `profile` | Name, language, save profile | All |
|
||||
| `settings.company` | `company` | Company name, locale, branding prefs | Company admin |
|
||||
| `settings.alerts` | `alerts` | In-app alert preferences (`alert-prefs.ts`) | All |
|
||||
| `settings.api_keys` | `api-keys` | Create/revoke company API keys | Free marketing: no API; Starter+: read/full per marketing |
|
||||
| `settings.team` | `team` | Invite members, roles, demote/remove | Company admin |
|
||||
| `settings.team_invite` | Team tab dialog | Send teammate invite | Company admin |
|
||||
|
||||
---
|
||||
|
||||
## Support (`support`)
|
||||
|
||||
| feature_key | Route | User actions | Default by plan |
|
||||
|-------------|-------|--------------|-------------------|
|
||||
| `support.center` | Nav More → Support `/support` | List own tickets | All |
|
||||
| `support.ticket_create` | `/support/new` | Open new ticket | All |
|
||||
| `support.ticket_thread` | `/support/[ticketId]` | Reply, read staff messages | All |
|
||||
|
||||
---
|
||||
|
||||
## Capabilities (`capabilities`) — cross-cutting gates
|
||||
|
||||
These map to API enforcement and/or prominent UI locks. Prefer admin toggles here for plan matrices.
|
||||
|
||||
| feature_key | Enforced where | Behavior | Suggested defaults |
|
||||
|-------------|----------------|----------|-------------------|
|
||||
| `capability.sku_cap` | `AssertCanStartProcessing`, `at_product_limit` on `/api/auth/me` | Blocks new processing when processed SKU count ≥ plan `max_products` | Free 50 · Starter 100 · Plus 400 · Growth 1.2k · Business 4k · Scale 12k · Enterprise unlimited |
|
||||
| `capability.ai_credits` | Entitlements `can_use_ai`, credit wallet debit | AI jobs need credits or paid plan | Free 0/mo · Starter 100 · Plus 400 · Growth 1.2k · Business 4k · Scale 12k · Enterprise unlimited |
|
||||
| `capability.ai_processing` | Processing pipeline + products UI | Title/description/enhance/seo AI types | Off on Free unless credits; on paid |
|
||||
| `capability.eprel` | `CanUseEPREL` (always true) | EU energy label enrichment | **All plans** |
|
||||
| `capability.normalize_specs_fill` | Pipeline steps without AI | Category, attributes, normalize | **All plans** |
|
||||
| `capability.campaign_ai` | `campaigns/generate_send.go` | AI campaign copy generation | Starter+ |
|
||||
| `capability.email_live_send` | `email/service.go` dry-run | Real marketing email delivery | Free dry-run; paid live (unless `EMAIL_DRY_RUN`) |
|
||||
| `capability.brand_ai_apply` | `AIBrandApplyAllowed` | Brand voice in AI prompts | Paid or trial |
|
||||
| `capability.seo_ai_rewrite` | SEO API `can_use_ai` | AI meta rewrite on SEO page | Same as AI credits |
|
||||
| `capability.feed_source_limit` | **Marketing only** (`pricing-data`) | Max import feed sources | Free 1 · Starter 2 · Plus 3 · Growth 5 · Business 8 · Scale 12 · Enterprise unlimited |
|
||||
| `capability.export_feed_limit` | **Marketing only** | Max export feed definitions | Free 1 · Starter 2 · Plus 4 · Growth 8 · Business unlimited · Scale unlimited · Enterprise unlimited |
|
||||
| `capability.storage_limit` | **Marketing only** | Storage GB | Free 2 · Starter 20 · Growth 100 · Business 500 · Enterprise unlimited |
|
||||
| `capability.api_access` | **Marketing only** (keys UI exists for all) | REST / v1 API | Free off · Starter read · Growth+ full |
|
||||
| `capability.byok` | AI integrations UI | Customer OpenAI key | Growth add-on · Business included · Enterprise |
|
||||
|
||||
---
|
||||
|
||||
## Already gated in UI (summary)
|
||||
|
||||
| Surface | Gate signal | feature_key |
|
||||
|---------|-------------|-------------|
|
||||
| Products processing menu | `canUseAI` locks AI options | `catalog.products.process_ai_*` |
|
||||
| Products / Dashboard / Billing banners | `isFreePlan`, credits, SKU limit | `dashboard.upgrade_banners`, `catalog.products.upgrade_prompt` |
|
||||
| Campaign wizard | `showUpgrade` on Free | `marketing.campaigns.generate_ai` |
|
||||
| SEO page | `showUpgrade`, `report.can_use_ai` | `marketing.seo.ai_rewrite` |
|
||||
| Brand page | `ai_apply_allowed` alert | `marketing.brand_ai_apply` |
|
||||
| Email integrations | `dry_run_forced` banner | `integrations.email.blast` |
|
||||
| API processing start | HTTP 402 `plan_gate` codes | `capability.ai_processing`, `capability.sku_cap` |
|
||||
|
||||
---
|
||||
|
||||
## Routes in dashboard shell but excluded from this catalog
|
||||
|
||||
| Path | Reason |
|
||||
|------|--------|
|
||||
| `/admin/*` | Platform admin (separate shell) |
|
||||
| `/login`, `/register`, `/accept-invite` | Auth shell |
|
||||
| `/`, `/pricing`, `/features`, `/docs`, `/privacy`, `/terms`, `/unsubscribe` | Marketing shell (`MARKETING_PATHS`) |
|
||||
| `/integrations` | Redirects to `/stores` |
|
||||
|
||||
---
|
||||
|
||||
## Navigation index (primary + More)
|
||||
|
||||
**Primary (`Nav.svelte` `primaryItems`):** Dashboard, Products, Feeds, Stores, Export feeds, Billing.
|
||||
|
||||
**More → Setup:** Categories, Attributes, Standard Fields, Brand, Settings, AI integrations, Email sending, Platform admin (platform admins only).
|
||||
|
||||
**More → Operate:** Processing, Campaigns, Content calendar, SEO, Reviews, Support.
|
||||
@@ -0,0 +1,120 @@
|
||||
[
|
||||
{ "key": "shell.navigation", "section": "shell", "label": "Sidebar navigation", "route": null, "component": "apps/web/src/lib/components/Nav.svelte" },
|
||||
{ "key": "shell.command_palette", "section": "shell", "label": "Command palette", "route": null, "component": "apps/web/src/lib/components/CommandPalette.svelte" },
|
||||
{ "key": "shell.company_switcher", "section": "shell", "label": "Company switcher", "route": null, "component": "apps/web/src/lib/components/CompanySwitcher.svelte" },
|
||||
{ "key": "shell.support_notifications", "section": "shell", "label": "Support notification bell", "route": null, "component": "apps/web/src/lib/components/SupportNotificationBell.svelte" },
|
||||
{ "key": "shell.tutorial", "section": "shell", "label": "Product tutorial", "route": null, "component": "apps/web/src/lib/components/tutorial/TutorialOverlay.svelte" },
|
||||
{ "key": "shell.account_menu", "section": "shell", "label": "Account menu", "route": "/settings", "component": "apps/web/src/routes/+layout.svelte" },
|
||||
{ "key": "shell.billing_recovery_banner", "section": "shell", "label": "Billing recovery banner", "route": null, "component": "apps/web/src/lib/components/BillingRecoveryBanner.svelte" },
|
||||
|
||||
{ "key": "dashboard.overview", "section": "dashboard", "label": "Dashboard overview", "route": "/dashboard", "component": "apps/web/src/routes/dashboard/+page.svelte" },
|
||||
{ "key": "dashboard.stats", "section": "dashboard", "label": "Stats cards", "route": "/dashboard", "component": "apps/web/src/lib/components/DashboardStats.svelte" },
|
||||
{ "key": "dashboard.quick_links", "section": "dashboard", "label": "Quick links", "route": "/dashboard", "component": "apps/web/src/routes/dashboard/+page.svelte" },
|
||||
{ "key": "dashboard.recent_jobs", "section": "dashboard", "label": "Recent jobs", "route": "/dashboard", "component": "apps/web/src/routes/dashboard/+page.svelte" },
|
||||
{ "key": "dashboard.news_feed", "section": "dashboard", "label": "News feed", "route": "/dashboard", "component": "apps/web/src/lib/components/NewsFeed.svelte" },
|
||||
{ "key": "dashboard.activation_checklist", "section": "dashboard", "label": "Activation checklist", "route": "/dashboard", "component": "apps/web/src/lib/components/ActivationChecklist.svelte" },
|
||||
{ "key": "dashboard.migrated_checklist", "section": "dashboard", "label": "Migrated cohort checklist", "route": "/dashboard", "component": "apps/web/src/lib/components/MigratedCohortChecklist.svelte" },
|
||||
{ "key": "dashboard.etl_gaps", "section": "dashboard", "label": "ETL gaps panel", "route": "/dashboard", "component": "apps/web/src/lib/components/MigratedEtlGapsPanel.svelte" },
|
||||
{ "key": "dashboard.store_reconnect", "section": "dashboard", "label": "Store reconnect banner", "route": "/dashboard", "component": "apps/web/src/lib/components/stores/StoreReconnectBanner.svelte" },
|
||||
{ "key": "dashboard.upgrade_banners", "section": "dashboard", "label": "Upgrade banners", "route": "/dashboard", "component": "apps/web/src/lib/components/UpgradeBanner.svelte" },
|
||||
|
||||
{ "key": "catalog.products", "section": "catalog", "label": "Products", "route": "/products", "component": "apps/web/src/routes/products/+page.svelte" },
|
||||
{ "key": "catalog.products.tab_processed", "section": "catalog", "label": "Products — Processed tab", "route": "/products", "component": "apps/web/src/lib/components/products/ProductTabs.svelte" },
|
||||
{ "key": "catalog.products.tab_needs_review", "section": "catalog", "label": "Products — Needs review tab", "route": "/products", "component": "apps/web/src/lib/components/products/ProductTabs.svelte" },
|
||||
{ "key": "catalog.products.tab_error", "section": "catalog", "label": "Products — Error tab", "route": "/products", "component": "apps/web/src/lib/components/products/ProductTabs.svelte" },
|
||||
{ "key": "catalog.products.tab_processing", "section": "catalog", "label": "Products — Processing tab", "route": "/products", "component": "apps/web/src/lib/components/products/ProductTabs.svelte" },
|
||||
{ "key": "catalog.products.tab_unprocessed", "section": "catalog", "label": "Products — Unprocessed tab", "route": "/products", "component": "apps/web/src/lib/components/products/ProductTabs.svelte" },
|
||||
{ "key": "catalog.products.process_categories", "section": "catalog", "label": "Process categories", "route": "/products", "component": "apps/web/src/lib/components/products/ProductProcessingActions.svelte" },
|
||||
{ "key": "catalog.products.process_attributes", "section": "catalog", "label": "Process attributes / specs", "route": "/products", "component": "apps/web/src/lib/components/products/ProductProcessingActions.svelte" },
|
||||
{ "key": "catalog.products.process_ai_titles", "section": "catalog", "label": "Process AI titles", "route": "/products", "component": "apps/web/src/lib/components/products/ProductProcessingActions.svelte" },
|
||||
{ "key": "catalog.products.process_ai_descriptions", "section": "catalog", "label": "Process AI descriptions", "route": "/products", "component": "apps/web/src/lib/components/products/ProductProcessingActions.svelte" },
|
||||
{ "key": "catalog.products.enrichment_review", "section": "catalog", "label": "Enrichment review panel", "route": "/products", "component": "apps/web/src/lib/components/products/ProductEditPanel.svelte" },
|
||||
{ "key": "catalog.products.export_selection", "section": "catalog", "label": "Export selected products", "route": "/products", "component": "apps/web/src/lib/components/products/ProductProcessingActions.svelte" },
|
||||
{ "key": "catalog.products.upgrade_prompt", "section": "catalog", "label": "Products upgrade prompts", "route": "/products", "component": "apps/web/src/routes/products/+page.svelte" },
|
||||
{ "key": "catalog.categories", "section": "catalog", "label": "Categories", "route": "/categories", "component": "apps/web/src/routes/categories/+page.svelte" },
|
||||
{ "key": "catalog.categories.title_formula", "section": "catalog", "label": "Category title formula", "route": "/categories/[categoryId]/title-formula", "component": "apps/web/src/routes/categories/[categoryId]/title-formula/+page.svelte" },
|
||||
{ "key": "catalog.categories.description_formula", "section": "catalog", "label": "Category description formula", "route": "/categories/[categoryId]/description-formula", "component": "apps/web/src/routes/categories/[categoryId]/description-formula/+page.svelte" },
|
||||
{ "key": "catalog.attributes", "section": "catalog", "label": "Attributes", "route": "/attributes", "component": "apps/web/src/routes/attributes/+page.svelte" },
|
||||
{ "key": "catalog.attributes.bulk_import", "section": "catalog", "label": "Attributes bulk import", "route": "/attributes", "component": "apps/web/src/routes/attributes/+page.svelte" },
|
||||
{ "key": "catalog.standard_fields", "section": "catalog", "label": "Standard fields", "route": "/standard-fields", "component": "apps/web/src/routes/standard-fields/+page.svelte" },
|
||||
{ "key": "catalog.standard_fields.groups", "section": "catalog", "label": "Standard field groups", "route": "/standard-fields", "component": "apps/web/src/routes/standard-fields/+page.svelte" },
|
||||
{ "key": "catalog.structured_descriptions", "section": "catalog", "label": "Structured description fields", "route": "/structured-descriptions", "component": "apps/web/src/routes/structured-descriptions/+page.svelte" },
|
||||
{ "key": "catalog.vector_categories", "section": "catalog", "label": "Vector categories test", "route": "/vector-categories", "component": "apps/web/src/routes/vector-categories/+page.svelte" },
|
||||
|
||||
{ "key": "feeds.list", "section": "feeds", "label": "Import feeds", "route": "/feeds", "component": "apps/web/src/routes/feeds/+page.svelte" },
|
||||
{ "key": "feeds.add_url", "section": "feeds", "label": "Add feed URL", "route": "/feeds", "component": "apps/web/src/routes/feeds/+page.svelte" },
|
||||
{ "key": "feeds.add_csv", "section": "feeds", "label": "Add feed CSV upload", "route": "/feeds", "component": "apps/web/src/routes/feeds/+page.svelte" },
|
||||
{ "key": "feeds.sync", "section": "feeds", "label": "Sync feed", "route": "/feeds", "component": "apps/web/src/routes/feeds/+page.svelte" },
|
||||
{ "key": "feeds.mapping", "section": "feeds", "label": "Feed mapping", "route": "/feeds/[feedId]/mapping", "component": "apps/web/src/routes/feeds/[feedId]/mapping/+page.svelte" },
|
||||
{ "key": "feeds.mapping.select_item", "section": "feeds", "label": "Mapping — select item element", "route": "/feeds/[feedId]/mapping", "component": "apps/web/src/routes/feeds/[feedId]/mapping/+page.svelte" },
|
||||
{ "key": "feeds.mapping.map_fields", "section": "feeds", "label": "Mapping — map fields", "route": "/feeds/[feedId]/mapping", "component": "apps/web/src/routes/feeds/[feedId]/mapping/+page.svelte" },
|
||||
{ "key": "feeds.export_feeds", "section": "feeds", "label": "Export feeds", "route": "/export-feeds", "component": "apps/web/src/routes/export-feeds/+page.svelte" },
|
||||
{ "key": "feeds.export_feeds.create", "section": "feeds", "label": "Create export feed", "route": "/export-feeds", "component": "apps/web/src/routes/export-feeds/+page.svelte" },
|
||||
{ "key": "feeds.export_feeds.generate", "section": "feeds", "label": "Generate export feed", "route": "/export-feeds", "component": "apps/web/src/routes/export-feeds/+page.svelte" },
|
||||
{ "key": "feeds.uploads", "section": "feeds", "label": "File uploads", "route": "/files", "component": "apps/web/src/routes/files/+page.svelte" },
|
||||
|
||||
{ "key": "stores.hub", "section": "stores", "label": "Stores hub", "route": "/stores", "component": "apps/web/src/routes/stores/+page.svelte" },
|
||||
{ "key": "stores.woocommerce", "section": "stores", "label": "WooCommerce integration", "route": "/woocommerce", "component": "apps/web/src/routes/woocommerce/+page.svelte" },
|
||||
{ "key": "stores.woocommerce.connection", "section": "stores", "label": "WooCommerce connection", "route": "/woocommerce", "component": "apps/web/src/routes/woocommerce/+page.svelte" },
|
||||
{ "key": "stores.woocommerce.categories", "section": "stores", "label": "WooCommerce categories sync", "route": "/woocommerce", "component": "apps/web/src/routes/woocommerce/+page.svelte" },
|
||||
{ "key": "stores.woocommerce.attributes", "section": "stores", "label": "WooCommerce attributes sync", "route": "/woocommerce", "component": "apps/web/src/routes/woocommerce/+page.svelte" },
|
||||
{ "key": "stores.woocommerce.orders", "section": "stores", "label": "WooCommerce orders", "route": "/woocommerce", "component": "apps/web/src/routes/woocommerce/+page.svelte" },
|
||||
{ "key": "stores.woocommerce.reviews", "section": "stores", "label": "WooCommerce reviews", "route": "/woocommerce?tab=reviews", "component": "apps/web/src/routes/woocommerce/+page.svelte" },
|
||||
{ "key": "stores.woocommerce.settings", "section": "stores", "label": "WooCommerce settings", "route": "/woocommerce", "component": "apps/web/src/routes/woocommerce/+page.svelte" },
|
||||
{ "key": "stores.shopify", "section": "stores", "label": "Shopify integration", "route": "/stores/shopify", "component": "apps/web/src/routes/stores/shopify/+page.svelte" },
|
||||
{ "key": "stores.shopify.connection", "section": "stores", "label": "Shopify connection", "route": "/stores/shopify", "component": "apps/web/src/routes/stores/shopify/+page.svelte" },
|
||||
{ "key": "stores.shopify.orders", "section": "stores", "label": "Shopify orders", "route": "/stores/shopify", "component": "apps/web/src/routes/stores/shopify/+page.svelte" },
|
||||
{ "key": "stores.shopify.settings", "section": "stores", "label": "Shopify settings", "route": "/stores/shopify", "component": "apps/web/src/routes/stores/shopify/+page.svelte" },
|
||||
|
||||
{ "key": "processing.monitor", "section": "processing", "label": "Processing monitor", "route": "/processing", "component": "apps/web/src/routes/processing/+page.svelte" },
|
||||
|
||||
{ "key": "marketing.campaigns", "section": "marketing", "label": "Campaigns", "route": "/campaigns", "component": "apps/web/src/routes/campaigns/+page.svelte" },
|
||||
{ "key": "marketing.campaigns.create", "section": "marketing", "label": "New campaign wizard", "route": "/campaigns/new", "component": "apps/web/src/lib/components/campaigns/CampaignWizard.svelte" },
|
||||
{ "key": "marketing.campaigns.generate_ai", "section": "marketing", "label": "Campaign AI generate", "route": "/campaigns/new", "component": "apps/web/src/lib/components/campaigns/CampaignWizard.svelte" },
|
||||
{ "key": "marketing.campaigns.send", "section": "marketing", "label": "Campaign send / blast", "route": "/campaigns/[id]", "component": "apps/web/src/routes/campaigns/[id]/+page.svelte" },
|
||||
{ "key": "marketing.content_calendar", "section": "marketing", "label": "Content calendar", "route": "/marketing/calendar", "component": "apps/web/src/routes/marketing/calendar/+page.svelte" },
|
||||
{ "key": "marketing.brand_kit", "section": "marketing", "label": "Brand kit", "route": "/brand", "component": "apps/web/src/routes/brand/+page.svelte" },
|
||||
{ "key": "marketing.brand_ai_apply", "section": "marketing", "label": "Brand AI apply in prompts", "route": "/brand", "component": "apps/web/src/routes/brand/+page.svelte" },
|
||||
{ "key": "marketing.seo", "section": "marketing", "label": "SEO recommendations", "route": "/seo", "component": "apps/web/src/routes/seo/+page.svelte" },
|
||||
{ "key": "marketing.seo.template_fill", "section": "marketing", "label": "SEO template fill", "route": "/seo", "component": "apps/web/src/routes/seo/+page.svelte" },
|
||||
{ "key": "marketing.seo.ai_rewrite", "section": "marketing", "label": "SEO AI rewrite", "route": "/seo", "component": "apps/web/src/routes/seo/+page.svelte" },
|
||||
{ "key": "marketing.reviews", "section": "marketing", "label": "Reviews", "route": "/woocommerce?tab=reviews", "component": "apps/web/src/routes/woocommerce/+page.svelte" },
|
||||
|
||||
{ "key": "integrations.ai", "section": "integrations", "label": "AI integrations", "route": "/integrations/ai", "component": "apps/web/src/routes/integrations/ai/+page.svelte" },
|
||||
{ "key": "integrations.ai.byok", "section": "integrations", "label": "Bring your own AI key", "route": "/integrations/ai", "component": "apps/web/src/routes/integrations/ai/+page.svelte" },
|
||||
{ "key": "integrations.email", "section": "integrations", "label": "Email sending", "route": "/integrations/email", "component": "apps/web/src/routes/integrations/email/+page.svelte" },
|
||||
{ "key": "integrations.email.test", "section": "integrations", "label": "Email test send", "route": "/integrations/email", "component": "apps/web/src/routes/integrations/email/+page.svelte" },
|
||||
{ "key": "integrations.email.blast", "section": "integrations", "label": "Email blast", "route": "/integrations/email", "component": "apps/web/src/routes/integrations/email/+page.svelte" },
|
||||
|
||||
{ "key": "billing.overview", "section": "billing", "label": "Billing overview", "route": "/billing", "component": "apps/web/src/routes/billing/+page.svelte" },
|
||||
{ "key": "billing.customer_portal", "section": "billing", "label": "Stripe customer portal", "route": "/billing", "component": "apps/web/src/routes/billing/+page.svelte" },
|
||||
{ "key": "billing.quick_upgrade", "section": "billing", "label": "Quick upgrade checkout", "route": "/billing", "component": "apps/web/src/routes/billing/+page.svelte" },
|
||||
{ "key": "billing.plans_compare", "section": "billing", "label": "Compare plans", "route": "/plans", "component": "apps/web/src/routes/plans/+page.svelte" },
|
||||
{ "key": "billing.checkout", "section": "billing", "label": "Stripe checkout", "route": "/plans", "component": "apps/web/src/routes/plans/+page.svelte" },
|
||||
|
||||
{ "key": "settings.profile", "section": "settings", "label": "Profile settings", "route": "/settings?tab=profile", "component": "apps/web/src/routes/settings/+page.svelte" },
|
||||
{ "key": "settings.company", "section": "settings", "label": "Company settings", "route": "/settings?tab=company", "component": "apps/web/src/routes/settings/+page.svelte" },
|
||||
{ "key": "settings.alerts", "section": "settings", "label": "Alert preferences", "route": "/settings?tab=alerts", "component": "apps/web/src/routes/settings/+page.svelte" },
|
||||
{ "key": "settings.api_keys", "section": "settings", "label": "API keys", "route": "/settings?tab=api-keys", "component": "apps/web/src/routes/settings/+page.svelte" },
|
||||
{ "key": "settings.team", "section": "settings", "label": "Team management", "route": "/settings?tab=team", "component": "apps/web/src/routes/settings/+page.svelte" },
|
||||
{ "key": "settings.team_invite", "section": "settings", "label": "Invite teammate", "route": "/settings?tab=team", "component": "apps/web/src/routes/settings/+page.svelte" },
|
||||
|
||||
{ "key": "support.center", "section": "support", "label": "Support center", "route": "/support", "component": "apps/web/src/routes/support/+page.svelte" },
|
||||
{ "key": "support.ticket_create", "section": "support", "label": "New support ticket", "route": "/support/new", "component": "apps/web/src/routes/support/new/+page.svelte" },
|
||||
{ "key": "support.ticket_thread", "section": "support", "label": "Support ticket thread", "route": "/support/[ticketId]", "component": "apps/web/src/routes/support/[ticketId]/+page.svelte" },
|
||||
|
||||
{ "key": "capability.sku_cap", "section": "capabilities", "label": "Product SKU cap", "route": null, "component": "apps/api/internal/billing/service.go" },
|
||||
{ "key": "capability.ai_credits", "section": "capabilities", "label": "AI credit wallet", "route": null, "component": "apps/api/internal/billing/entitlements.go" },
|
||||
{ "key": "capability.ai_processing", "section": "capabilities", "label": "AI processing jobs", "route": null, "component": "apps/api/internal/billing/service.go" },
|
||||
{ "key": "capability.eprel", "section": "capabilities", "label": "EPREL enrichment", "route": null, "component": "apps/api/internal/billing/entitlements.go" },
|
||||
{ "key": "capability.normalize_specs_fill", "section": "capabilities", "label": "Normalize / specs / fill", "route": null, "component": "apps/api/internal/processing/steps.go" },
|
||||
{ "key": "capability.campaign_ai", "section": "capabilities", "label": "Campaign AI generation", "route": null, "component": "apps/api/internal/campaigns/generate_send.go" },
|
||||
{ "key": "capability.email_live_send", "section": "capabilities", "label": "Live email delivery", "route": null, "component": "apps/api/internal/email/service.go" },
|
||||
{ "key": "capability.brand_ai_apply", "section": "capabilities", "label": "Brand voice in AI prompts", "route": null, "component": "apps/api/internal/billing/service.go" },
|
||||
{ "key": "capability.seo_ai_rewrite", "section": "capabilities", "label": "SEO AI rewrite", "route": "/seo", "component": "apps/web/src/routes/seo/+page.svelte" },
|
||||
{ "key": "capability.feed_source_limit", "section": "capabilities", "label": "Feed source limit (marketing)", "route": null, "component": "apps/web/src/lib/components/pricing/pricing-data.ts" },
|
||||
{ "key": "capability.export_feed_limit", "section": "capabilities", "label": "Export feed limit (marketing)", "route": null, "component": "apps/web/src/lib/components/pricing/pricing-data.ts" },
|
||||
{ "key": "capability.storage_limit", "section": "capabilities", "label": "Storage limit (marketing)", "route": null, "component": "apps/web/src/lib/components/pricing/pricing-data.ts" },
|
||||
{ "key": "capability.api_access", "section": "capabilities", "label": "REST API access (marketing)", "route": "/settings?tab=api-keys", "component": "apps/web/src/lib/components/pricing/pricing-data.ts" },
|
||||
{ "key": "capability.byok", "section": "capabilities", "label": "Bring your own AI key", "route": "/integrations/ai", "component": "apps/web/src/routes/integrations/ai/+page.svelte" }
|
||||
]
|
||||
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"agent": "02",
|
||||
"title": "Plans/packages/permissions extension points",
|
||||
"sibling_docs_status": "docs/plan-permissions/ empty at write time (no 01-* / 03-* yet)",
|
||||
"recommended_approach": "Extend Entitlements + Plan/UpsertPlan + AssertCanStartProcessing-style gates; expose via CreditsOverview/me; admin billing UI; do not add a parallel packages/permissions system.",
|
||||
"extension_points": [
|
||||
{
|
||||
"priority": 1,
|
||||
"role": "capability_source_of_truth",
|
||||
"path": "apps/api/internal/billing/entitlements.go",
|
||||
"symbols": [
|
||||
"Entitlements",
|
||||
"ComputeEntitlements",
|
||||
"EntitlementsForCompany",
|
||||
"IsFreePlanName",
|
||||
"ProcessingTypeRequiresAI",
|
||||
"ProcessingTypeRequiresEPREL"
|
||||
],
|
||||
"intent": "Add plan feature/section booleans here; keep pure ComputeEntitlements testable."
|
||||
},
|
||||
{
|
||||
"priority": 1,
|
||||
"role": "plan_catalog_and_meters",
|
||||
"path": "apps/api/internal/billing/service.go",
|
||||
"symbols": [
|
||||
"Plan",
|
||||
"CreditsOverview",
|
||||
"defaultPublicPlans",
|
||||
"IsPublicProductPlan",
|
||||
"EnsureDefaultPlans",
|
||||
"ListPlans",
|
||||
"ListPublicPlans",
|
||||
"UpsertPlan",
|
||||
"AssignPlan",
|
||||
"ProvisionFreePlan",
|
||||
"AssertCanStartProcessing",
|
||||
"ProcessingGateOpts",
|
||||
"AIBrandApplyAllowed",
|
||||
"ConsumeCredits",
|
||||
"EnsureDefaultCosts",
|
||||
"EnterpriseUnlimitedCredits"
|
||||
],
|
||||
"intent": "Extend Plan fields / defaults; SKU+AI gates; wallet assignment; public vs deal listing."
|
||||
},
|
||||
{
|
||||
"priority": 1,
|
||||
"role": "schema",
|
||||
"path": "apps/api/sql/schema/001_platform.sql",
|
||||
"symbols": ["plans", "company_plans", "credit_balances", "processing_costs", "billing_cycles"],
|
||||
"intent": "Add feature/permission columns or JSON on plans; optionally wire unused custom_monthly_credits / custom_max_products."
|
||||
},
|
||||
{
|
||||
"priority": 2,
|
||||
"role": "schema_stripe",
|
||||
"path": "apps/api/sql/schema/016_stripe_billing.sql",
|
||||
"symbols": ["company_plans.stripe_subscription_id", "company_plans.stripe_price_id"],
|
||||
"intent": "Preserve Stripe linkage when altering company_plans."
|
||||
},
|
||||
{
|
||||
"priority": 1,
|
||||
"role": "admin_http",
|
||||
"path": "apps/api/internal/httpapi/billing_handlers.go",
|
||||
"symbols": [
|
||||
"handleListPlans",
|
||||
"handleListPublicPlans",
|
||||
"handleUpsertPlan",
|
||||
"handleAssignPlan",
|
||||
"handleAddCredits",
|
||||
"handleCreditsOverview"
|
||||
],
|
||||
"intent": "Accept/return new plan permission fields on admin upsert and credits overview."
|
||||
},
|
||||
{
|
||||
"priority": 1,
|
||||
"role": "route_mount",
|
||||
"path": "apps/api/internal/httpapi/server.go",
|
||||
"symbols": ["Router", "RequirePlatformAdmin"],
|
||||
"intent": "Keep /api/admin/plans* under platform admin; tenant /api/billing/plans public ladder."
|
||||
},
|
||||
{
|
||||
"priority": 1,
|
||||
"role": "processing_enforcement",
|
||||
"path": "apps/api/internal/processing/pipeline.go",
|
||||
"symbols": ["StartJob", "processOne"],
|
||||
"intent": "Primary AssertCanStartProcessing + step AllowAI policy; add section asserts near StartJob."
|
||||
},
|
||||
{
|
||||
"priority": 2,
|
||||
"role": "http_402_mapping",
|
||||
"path": "apps/api/internal/httpapi/processing_handlers.go",
|
||||
"symbols": ["handleStartProcessingJob", "planGateCode"],
|
||||
"intent": "Reuse 402 + code mapping for new plan_gate errors."
|
||||
},
|
||||
{
|
||||
"priority": 2,
|
||||
"role": "v1_process",
|
||||
"path": "apps/api/internal/httpapi/v1_process_handlers.go",
|
||||
"symbols": ["handleV1StartProcess", "startV1Jobs"],
|
||||
"intent": "Same gate errors as SPA processing."
|
||||
},
|
||||
{
|
||||
"priority": 2,
|
||||
"role": "seo_gate",
|
||||
"path": "apps/api/internal/seo/service.go",
|
||||
"symbols": ["Report", "Apply"],
|
||||
"intent": "Section toggle for SEO AI; already uses EntitlementsForCompany + ConsumeCredits."
|
||||
},
|
||||
{
|
||||
"priority": 2,
|
||||
"role": "seo_http",
|
||||
"path": "apps/api/internal/httpapi/seo_handlers.go",
|
||||
"symbols": [],
|
||||
"intent": "Map billing insufficient / upgrade errors for SEO routes."
|
||||
},
|
||||
{
|
||||
"priority": 2,
|
||||
"role": "campaigns_gate",
|
||||
"path": "apps/api/internal/campaigns/generate_send.go",
|
||||
"symbols": ["Generate"],
|
||||
"intent": "Section toggle for campaign AI; already checks CanUseAI / IsFreePlan."
|
||||
},
|
||||
{
|
||||
"priority": 2,
|
||||
"role": "brand_gate",
|
||||
"path": "apps/api/internal/httpapi/brand_handlers.go",
|
||||
"symbols": ["brandResponse", "AIBrandApplyAllowed"],
|
||||
"intent": "Pattern for soft feature flag exposed to UI (ai_apply_allowed)."
|
||||
},
|
||||
{
|
||||
"priority": 1,
|
||||
"role": "admin_ui",
|
||||
"path": "apps/web/src/routes/admin/billing/+page.svelte",
|
||||
"symbols": ["createPlan", "assignPlan", "reload", "customPlanCount", "maxProductsLabel"],
|
||||
"intent": "Add edit + permission toggles; send full Plan including id/max_products."
|
||||
},
|
||||
{
|
||||
"priority": 2,
|
||||
"role": "admin_nav",
|
||||
"path": "apps/web/src/lib/components/AdminNav.svelte",
|
||||
"symbols": [],
|
||||
"intent": "Entry to /admin/billing already present."
|
||||
},
|
||||
{
|
||||
"priority": 1,
|
||||
"role": "tenant_display_and_client_gates",
|
||||
"path": "apps/web/src/lib/billing-display.ts",
|
||||
"symbols": [
|
||||
"canUseAIFromCredits",
|
||||
"isFreePlan",
|
||||
"isEnterprisePlan",
|
||||
"isSelfServeCheckoutPlan",
|
||||
"formatSkuCap",
|
||||
"withUpgradeHint"
|
||||
],
|
||||
"intent": "Mirror new entitlement flags for UX gating/hints."
|
||||
},
|
||||
{
|
||||
"priority": 2,
|
||||
"role": "tenant_types",
|
||||
"path": "apps/web/src/lib/types.ts",
|
||||
"symbols": [],
|
||||
"intent": "Extend credits/plan TypeScript shapes."
|
||||
},
|
||||
{
|
||||
"priority": 2,
|
||||
"role": "tenant_nav",
|
||||
"path": "apps/web/src/lib/components/Nav.svelte",
|
||||
"symbols": [],
|
||||
"intent": "Optional hide/disable tabs from me.credits entitlements."
|
||||
},
|
||||
{
|
||||
"priority": 2,
|
||||
"role": "me_embedding",
|
||||
"path": "apps/api/internal/httpapi/auth_handlers.go",
|
||||
"symbols": ["handleMe"],
|
||||
"intent": "Already embeds CreditsOverview; new flags flow to SPA automatically if added to struct."
|
||||
},
|
||||
{
|
||||
"priority": 3,
|
||||
"role": "seed_demo",
|
||||
"path": "apps/api/cmd/seed-demo/main.go",
|
||||
"symbols": ["EnsureDefaultPlans", "AssignPlan"],
|
||||
"intent": "Keep Free=0 / Enterprise demo invariants when changing defaults."
|
||||
},
|
||||
{
|
||||
"priority": 3,
|
||||
"role": "migrator",
|
||||
"path": "apps/api/cmd/migrator/main.go",
|
||||
"symbols": ["loadPlans", "loadCompanyPlans"],
|
||||
"intent": "Map new plan columns from MySQL if present."
|
||||
},
|
||||
{
|
||||
"priority": 3,
|
||||
"role": "missing_plans_repair",
|
||||
"path": "apps/api/internal/billing/missing_plans.go",
|
||||
"symbols": ["AssignPlanIfMissing", "PlanIDByName", "HasActivePlan"],
|
||||
"intent": "Assignment helpers for tenants without active plans."
|
||||
},
|
||||
{
|
||||
"priority": 3,
|
||||
"role": "stripe_assign",
|
||||
"path": "apps/api/internal/billing/stripe.go",
|
||||
"symbols": ["applyPlanPurchase", "downgradeToFree", "CreateCheckoutSession"],
|
||||
"intent": "Checkout must remain public-ladder-only; custom deals stay admin-assign."
|
||||
},
|
||||
{
|
||||
"priority": 3,
|
||||
"role": "tests",
|
||||
"path": "apps/api/internal/billing/gate_test.go",
|
||||
"symbols": [
|
||||
"TestComputeEntitlements",
|
||||
"TestDefaultPublicPlansEnterpriseUnlimited",
|
||||
"TestDefaultPublicPlansFreeZeroCredits"
|
||||
],
|
||||
"intent": "Extend entitlement/default-plan tests for new flags."
|
||||
}
|
||||
],
|
||||
"do_not_duplicate": [
|
||||
"Separate packages table parallel to plans",
|
||||
"Frontend-only permission checks without EntitlementsForCompany",
|
||||
"New Free/paid heuristics outside IsFreePlanName / ComputeEntitlements",
|
||||
"Second SKU limit path ignoring plans.max_products / AssertCanStartProcessing"
|
||||
],
|
||||
"unused_schema_to_consider": [
|
||||
"company_plans.custom_monthly_credits",
|
||||
"company_plans.custom_max_products"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
# 02 — Plans / packages / permissions (current system)
|
||||
|
||||
Inventory of how plans (“packages”) work today in descrybe-v2.
|
||||
Sibling folder `docs/plan-permissions/` was empty at write time (no `01-*` / `03-*` artifacts yet). Aligns with existing product docs: `docs/free-tier.md`, `docs/billing-credits-audit.md`.
|
||||
|
||||
ASSUMPTION: “packages” in product language maps 1:1 to DB table `plans` + assignment via `company_plans`. There is no separate `packages` table.
|
||||
|
||||
---
|
||||
|
||||
## 1. Models, tables, DTOs, seeders
|
||||
|
||||
### Tables (`apps/api/sql/schema/001_platform.sql`, Stripe extras in `016_stripe_billing.sql`)
|
||||
|
||||
| Table | Role |
|
||||
|---|---|
|
||||
| `plans` | Catalog of packages: meters + flags |
|
||||
| `company_plans` | Per-company assignment (active row, trial, Stripe ids, unused override columns) |
|
||||
| `credit_balances` | Wallet: `total_credits`, `used_credits` |
|
||||
| `billing_cycles` | Cycle usage counters / invoicing |
|
||||
| `processing_costs` | Per-feature credit unit costs (`product_processing`, `openai_token_k`, `seo_meta_ai`, `campaign_copy`) |
|
||||
| `companies.stripe_customer_id` | Stripe customer link |
|
||||
| `company_plans.stripe_subscription_id` / `stripe_price_id` | Subscription linkage |
|
||||
|
||||
**`plans` columns**
|
||||
|
||||
- `id`, `name`, `description`
|
||||
- `monthly_credits`, `yearly_credits` (nullable)
|
||||
- `max_products` (nullable = unlimited SKU cap)
|
||||
- `is_custom` (boolean, default false)
|
||||
- `term` (default `'monthly'`)
|
||||
- timestamps
|
||||
|
||||
**`company_plans` columns (assignment + overrides)**
|
||||
|
||||
- `plan_id`, `is_active`, billing/contract dates
|
||||
- `custom_monthly_credits`, `custom_max_products` — **present in schema, unused by Go billing gates**
|
||||
- `total_credits_allocated`, `contract_reference`, `notes`
|
||||
- `is_trial`, `trial_ends_at`, `trial_credits`
|
||||
- Stripe subscription/price ids (migration 016)
|
||||
|
||||
### Go DTOs / types (`apps/api/internal/billing/`)
|
||||
|
||||
| Symbol | File | Purpose |
|
||||
|---|---|---|
|
||||
| `Plan` | `service.go` | JSON DTO for catalog CRUD/list |
|
||||
| `CreditsOverview` | `service.go` | Wallet + plan snapshot + entitlement booleans for `/api/billing/credits` and `/api/auth/me` |
|
||||
| `Entitlements` | `entitlements.go` | Pure capability snapshot (`can_use_ai`, `can_use_eprel`, free/paid/trial) |
|
||||
| `ProcessingGateOpts` | `service.go` | `{RequiresAI, RequiresEPREL}` for start-job gate |
|
||||
| Web `Plan` / credits types | `apps/web/src/lib/types.ts`, `billing-display.ts`, admin page local types | Mirror API shapes |
|
||||
|
||||
### Seed / bootstrap (not SQL seeders)
|
||||
|
||||
| Mechanism | Symbol / path | What it does |
|
||||
|---|---|---|
|
||||
| Public ladder upsert | `defaultPublicPlans` → `EnsureDefaultPlans` | Free / Starter / Growth / Business / Enterprise by name |
|
||||
| Signup Free | `ProvisionFreePlan` | Assign Free if present |
|
||||
| Credit cost rows | `EnsureDefaultCosts` | Idempotent `processing_costs` insert |
|
||||
| Demo seed | `apps/api/cmd/seed-demo` | `EnsureDefaultPlans` then assign **Enterprise** to Local Demo Co; asserts Free stays at 0 credits |
|
||||
| Migrator ETL | `apps/api/cmd/migrator` (`loadPlans`, `loadCompanyPlans`, `company_plans_repair.go`) | MySQL → Postgres plans + repair missing assignments |
|
||||
| On-demand | Admin/public list handlers call `EnsureDefaultPlans` before list |
|
||||
|
||||
**Default public packaging** (`defaultPublicPlans`):
|
||||
|
||||
| Name | Monthly credits | Max products | `is_custom` |
|
||||
|---|---|---|---|
|
||||
| Free | 0 | 100 | false |
|
||||
| Starter | 300 | 1_000 | false |
|
||||
| Growth | 2_000 | 10_000 | false |
|
||||
| Business | 10_000 | 100_000 | false |
|
||||
| Enterprise | 1_000_000 (`EnterpriseUnlimitedCredits`) | `null` (unlimited) | **true** |
|
||||
|
||||
---
|
||||
|
||||
## 2. Existing feature / limit / permission fields
|
||||
|
||||
There is **no** plan-scoped permission matrix (no per-tab / per-section toggles, no `plan_permissions` table, no feature-flag JSON on `plans`).
|
||||
|
||||
Capabilities today are **derived** or **metered**:
|
||||
|
||||
### Entitlement booleans (`ComputeEntitlements` / `CreditsOverview`)
|
||||
|
||||
| Field | Meaning (current code) |
|
||||
|---|---|
|
||||
| `is_free_plan` / `is_paid_plan` | Name equals `"free"` (or empty) vs not |
|
||||
| `is_trial` | From active `company_plans.is_trial` |
|
||||
| `monthly_credits` | From `plans.monthly_credits` |
|
||||
| `remaining_credits` | `max(0, total - used)` wallet |
|
||||
| `can_use_ai` | `remaining > 0` **OR** paid plan (not Free). Paid with empty wallet still has `can_use_ai=true`, but AI **jobs** require remaining ≥ batch size |
|
||||
| `can_use_eprel` | **Always `true`** in code (EU public data). Platform can still disable enricher via settings/env. Note: `docs/free-tier.md` still describes paid-only EPREL — **docs lag code** |
|
||||
|
||||
### Hard limits
|
||||
|
||||
| Limit | Source | Enforced by |
|
||||
|---|---|---|
|
||||
| SKU / product cap | `plans.max_products` (null/≤0 = unlimited) | `AssertCanStartProcessing` vs `count(processed_products)` |
|
||||
| AI credit wallet | `credit_balances` | Gate on AI job types + `ConsumeCredits` / batch |
|
||||
| Brand voice in AI | Paid or trial | `AIBrandApplyAllowed` |
|
||||
|
||||
### Credit feature keys (`processing_costs.feature_name`)
|
||||
|
||||
Metering only (cost per unit), **not** allow/deny:
|
||||
|
||||
- `product_processing`
|
||||
- `openai_token_k`
|
||||
- `seo_meta_ai`
|
||||
- `campaign_copy`
|
||||
|
||||
### AI-only vs free-path processing types
|
||||
|
||||
- `ProcessingTypeRequiresAI`: `enhance`, `enhance_only`, `title`, `description`, `seo`, `seo_ai`
|
||||
- `ProcessingTypeRequiresEPREL`: `eprel`, `eprel_only`
|
||||
- Non-AI types (normalize/specs/fill/full with step policy) can run on Free with 0 credits; pipeline may skip AI/EPREL steps
|
||||
|
||||
### Membership / platform roles (orthogonal to plans)
|
||||
|
||||
Company `admin` / member roles and `users.is_platform_admin` gate **who** can call admin APIs — not which product tabs a plan unlocks.
|
||||
|
||||
---
|
||||
|
||||
## 3. Default vs custom packages
|
||||
|
||||
Two **different** notions exist; do not conflate them.
|
||||
|
||||
### A. Public product ladder vs client deals (listing / checkout)
|
||||
|
||||
- **Public**: name ∈ `{Free, Starter, Growth, Business, Enterprise}` via `IsPublicProductPlan`
|
||||
- Exposed to tenants: `GET /api/billing/plans` → `ListPublicPlans`
|
||||
- Stripe self-serve checkout also keys off public names
|
||||
- **Client / custom deals** (A1, Merkur trial, legacy names, admin-created packages): remain in `plans`, visible only via **admin** `GET /api/admin/plans` → `ListPlans` (all rows)
|
||||
|
||||
`EnsureDefaultPlans` syncs only names in `defaultPublicPlans`; named client deals are left untouched.
|
||||
|
||||
### B. `plans.is_custom` flag
|
||||
|
||||
- Boolean on the row; admin UI Badge “Custom” vs “Standard”
|
||||
- Admin create dialog defaults `is_custom: true`
|
||||
- Public **Enterprise** is seeded with `is_custom: true` even though it is a public ladder plan
|
||||
- **Not** used by `ListPublicPlans` (name whitelist wins)
|
||||
- **Not** used by entitlement/gates
|
||||
|
||||
### Per-company overrides
|
||||
|
||||
Schema columns `company_plans.custom_monthly_credits` / `custom_max_products` exist for deal overrides, but **billing Go code does not read them** today. Assignment always copies `plans.monthly_credits` (or trial credits) into the wallet; SKU gate reads `plans.max_products` only.
|
||||
|
||||
---
|
||||
|
||||
## 4. Admin UI for editing packages
|
||||
|
||||
| Path | Role |
|
||||
|---|---|
|
||||
| `apps/web/src/routes/admin/billing/+page.svelte` | Platform billing admin page |
|
||||
| `apps/web/src/lib/components/AdminNav.svelte` | Nav link “Platform billing” → `/admin/billing` |
|
||||
| Gate | `requirePlatformAdmin` (`$lib/admin-gate`) |
|
||||
|
||||
**Capabilities on the page**
|
||||
|
||||
- List all plans (`GET /api/admin/plans`)
|
||||
- **Create** plan dialog (`POST /api/admin/plans`) — body: `name`, `monthly_credits`, `is_custom` (no `max_products` / description / term in UI)
|
||||
- Assign plan to company (`POST /api/admin/plans/assign`)
|
||||
- Add credits (`POST /api/admin/credits`)
|
||||
- Run due billing cycles (`POST /api/admin/billing/run-cycles`)
|
||||
- Companies tab shows wallet totals from `GET /api/admin/companies`
|
||||
|
||||
**Gaps in admin UI**
|
||||
|
||||
- No in-place **edit** of existing plan meters (API `UpsertPlan` supports update when `id > 0`, UI never sends `id`)
|
||||
- No delete plan
|
||||
- No editors for `max_products`, description, term, yearly credits
|
||||
- No UI for `company_plans` custom override columns or permission toggles
|
||||
|
||||
Related tenant-facing (not admin edit): `/plans`, `/billing`, `/pricing` + `billing-display.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 5. API endpoints that enforce plan limits
|
||||
|
||||
### Catalog / admin (mutate packages & assignment)
|
||||
|
||||
Mounted under `/api/admin` with `RequireSession` + `RequirePlatformAdmin` (`server.go`):
|
||||
|
||||
| Method | Path | Handler |
|
||||
|---|---|---|
|
||||
| GET | `/api/admin/plans` | `handleListPlans` |
|
||||
| POST | `/api/admin/plans` | `handleUpsertPlan` |
|
||||
| POST | `/api/admin/plans/assign` | `handleAssignPlan` |
|
||||
| POST | `/api/admin/credits` | `handleAddCredits` |
|
||||
| POST | `/api/admin/billing/run-cycles` | `handleRunBillingCycles` |
|
||||
|
||||
### Tenant billing read / Stripe
|
||||
|
||||
Under `/api` + session + company:
|
||||
|
||||
| Method | Path | Notes |
|
||||
|---|---|---|
|
||||
| GET | `/api/billing/credits` | `CreditsOverview` (includes entitlement flags + SKU usage) |
|
||||
| GET | `/api/billing/usage` | Usage summary |
|
||||
| GET | `/api/billing/plans` | Public ladder only |
|
||||
| POST | `/api/billing/checkout` / `portal` | Stripe; public plans |
|
||||
| GET | `/api/auth/me` | Embeds `credits` overview |
|
||||
|
||||
### Enforcement call sites (402 Payment Required pattern)
|
||||
|
||||
Central gate: `Service.AssertCanStartProcessing` → called from `processing.Pipeline.StartJob`.
|
||||
|
||||
| Surface | How limits apply |
|
||||
|---|---|
|
||||
| `POST` processing jobs (`processing_handlers`, `v1_process_handlers`) | StartJob → AssertCanStartProcessing; maps `ErrInsufficientCredits` / `ErrProductLimitExceeded` / `ErrAIRequiresUpgrade` / `ErrEPRELRequiresUpgrade` → **402** + `upgrade_url` |
|
||||
| Pipeline `processOne` | Step policy: AI only if `CanUseAI && RemainingCredits > 0`; `ConsumeCredits` |
|
||||
| SEO AI apply (`seo/service.go` + handlers) | Entitlements + `ConsumeCredits("seo_meta_ai")` |
|
||||
| Campaign AI generate (`campaigns/generate_send.go`) | Entitlements; Free blocked; `ConsumeCredits("campaign_copy")` |
|
||||
| Brand kit (`brand_handlers.go`) | `ai_apply_allowed` via `AIBrandApplyAllowed` (edit kit allowed on Free; AI inject gated) |
|
||||
| Feed sample sync+process | Shares StartJob path (`handleSyncAndProcessSample`) |
|
||||
|
||||
Wallet debit: `ConsumeCredits` / `ConsumeCreditsBatch` (atomic remaining check).
|
||||
|
||||
---
|
||||
|
||||
## 6. Gaps vs per-tab / section permission toggles
|
||||
|
||||
| Need | Current state |
|
||||
|---|---|
|
||||
| Toggle “Feeds / Products / SEO / Campaigns / Exports / …” per plan | **Missing** — nav is role-based, not plan-based |
|
||||
| Boolean feature flags on `plans` | **Missing** — only meters + `is_custom` + name heuristics |
|
||||
| Per-company override of feature set | Schema has unused credit/SKU override columns; **no** feature override |
|
||||
| Distinguish marketing “custom package” vs Enterprise `is_custom` | Confusing: Enterprise is public **and** `is_custom` |
|
||||
| Admin edit of full plan surface | Create-only UI; no permission matrix editor |
|
||||
| Server enforcement for “section disabled” | Only AI/SKU/credit gates — disabled sections would still be reachable unless added |
|
||||
| Docs drift | `free-tier.md` EPREL paid-only vs code `CanUseEPREL=true` |
|
||||
|
||||
---
|
||||
|
||||
## 7. Recommended extension points (reuse, don’t duplicate)
|
||||
|
||||
Prefer extending the existing entitlements + plan catalog path over a parallel permissions subsystem.
|
||||
|
||||
1. **Catalog shape** — Extend `plans` (or a JSON `features` / `permissions` column) and `billing.Plan` + `UpsertPlan` / `EnsureDefaultPlans` / migrator load. Keep `is_custom` as “deal packaging” metadata; use **name whitelist or a new `is_public`** for self-serve listing if `is_custom` remains overloaded.
|
||||
|
||||
2. **Capability resolution** — Extend `Entitlements` + `ComputeEntitlements` / `EntitlementsForCompany` so one function remains the source of truth. Expose via `CreditsOverview` and `/api/auth/me` (web already reads `can_use_*` from credits).
|
||||
|
||||
3. **Server enforcement** — Add small helpers next to `AssertCanStartProcessing` / `AIBrandApplyAllowed` (e.g. `AssertFeature(ctx, companyID, "seo")`) and call from handlers that own each surface (processing, seo, campaigns, feeds, exports). Reuse 402 + `planGateCode` mapping in `processing_handlers.go`.
|
||||
|
||||
4. **Optional per-company overrides** — Wire `company_plans.custom_*` (and any new feature override) **into** `EntitlementsForCompany` / SKU query instead of inventing a second assignment table.
|
||||
|
||||
5. **Admin UI** — Extend `admin/billing/+page.svelte` create/edit dialog and table (send full `Plan` including `id` for update). Add toggle group UI bound to the same fields `UpsertPlan` persists.
|
||||
|
||||
6. **Web UX** — Gate nav/sections from `me.credits` / entitlements in `Nav.svelte` + page loads; keep display helpers in `billing-display.ts`.
|
||||
|
||||
7. **Do not** invent a second “packages” model, duplicate Free/paid heuristics outside `IsFreePlanName` / `ComputeEntitlements`, or add ad-hoc flags only in the frontend.
|
||||
|
||||
---
|
||||
|
||||
## Quick reference — key files
|
||||
|
||||
| Area | Path |
|
||||
|---|---|
|
||||
| Schema | `apps/api/sql/schema/001_platform.sql`, `016_stripe_billing.sql` |
|
||||
| Service / Plan / gates | `apps/api/internal/billing/service.go` |
|
||||
| Entitlements | `apps/api/internal/billing/entitlements.go` |
|
||||
| HTTP billing/admin | `apps/api/internal/httpapi/billing_handlers.go`, `server.go` |
|
||||
| Processing gate | `apps/api/internal/processing/pipeline.go` |
|
||||
| Admin UI | `apps/web/src/routes/admin/billing/+page.svelte` |
|
||||
| Display helpers | `apps/web/src/lib/billing-display.ts` |
|
||||
| Seed | `apps/api/cmd/seed-demo/main.go` |
|
||||
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"agent": "03",
|
||||
"title": "Plan permission contract",
|
||||
"status": "design_only",
|
||||
"version": "1.0.0",
|
||||
"coordinates_with": [
|
||||
"docs/plan-permissions/01-dashboard-feature-catalog.md",
|
||||
"docs/plan-permissions/01-feature-keys.json",
|
||||
"docs/plan-permissions/02-plans-permissions-current.md",
|
||||
"docs/plan-permissions/02-extension-points.json"
|
||||
],
|
||||
"assumptions": [
|
||||
"Package equals plans row; no separate packages table",
|
||||
"is_custom=true (including seeded Enterprise) defaults all features ON unless override false",
|
||||
"Meters (max_products, credits, can_use_ai) remain; feature keys gate surfaces/actions",
|
||||
"Client canFeature fail-open when features map missing until API cutover"
|
||||
],
|
||||
"runtime_formula": {
|
||||
"effective": "plan_allows(key) AND global_section_enabled(section(key)) AND global_feature_enabled(key)",
|
||||
"plan_allows": [
|
||||
"if key in plans.features -> plans.features[key]",
|
||||
"else if plans.is_custom -> true",
|
||||
"else -> DefaultMatrix[normalizePlanName(name)][key] (unknown -> false unless catalog All-plans)"
|
||||
],
|
||||
"global_missing_default": true,
|
||||
"parent_prefix_rule": "UI treats disabled parent as disabling children; Assert uses specific action key"
|
||||
},
|
||||
"data_model": {
|
||||
"plans_features_column": {
|
||||
"table": "plans",
|
||||
"column": "features",
|
||||
"type": "JSONB NOT NULL DEFAULT '{}'",
|
||||
"semantics": "sparse overrides only; not full expanded matrix"
|
||||
},
|
||||
"platform_feature_gates": {
|
||||
"table": "platform_feature_gates",
|
||||
"columns": {
|
||||
"gate_key": "TEXT PRIMARY KEY",
|
||||
"kind": "TEXT CHECK IN ('section','feature')",
|
||||
"enabled": "BOOLEAN NOT NULL DEFAULT true",
|
||||
"updated_at": "TIMESTAMPTZ",
|
||||
"updated_by": "UUID NULL REFERENCES users(id)"
|
||||
},
|
||||
"section_keys": [
|
||||
"shell",
|
||||
"dashboard",
|
||||
"catalog",
|
||||
"feeds",
|
||||
"stores",
|
||||
"processing",
|
||||
"marketing",
|
||||
"integrations",
|
||||
"billing",
|
||||
"settings",
|
||||
"support",
|
||||
"capabilities"
|
||||
]
|
||||
},
|
||||
"do_not_add": [
|
||||
"packages table",
|
||||
"parallel permissions package outside billing",
|
||||
"v1 company-level feature overrides"
|
||||
]
|
||||
},
|
||||
"feature_key_namespace": {
|
||||
"authority": "docs/plan-permissions/01-feature-keys.json",
|
||||
"pattern": "section.leaf(.subleaf)* snake_case",
|
||||
"capability_prefix": "capability.",
|
||||
"exclude": [
|
||||
"/admin/*"
|
||||
]
|
||||
},
|
||||
"default_matrix_sketch": {
|
||||
"custom_and_enterprise_is_custom": "all_true_unless_override",
|
||||
"free_denies": [
|
||||
"catalog.products.process_ai_titles",
|
||||
"catalog.products.process_ai_descriptions",
|
||||
"marketing.campaigns.generate_ai",
|
||||
"marketing.campaigns.send",
|
||||
"marketing.brand_ai_apply",
|
||||
"marketing.seo.ai_rewrite",
|
||||
"integrations.ai.byok",
|
||||
"settings.api_keys",
|
||||
"capability.ai_processing",
|
||||
"capability.campaign_ai",
|
||||
"capability.email_live_send",
|
||||
"capability.brand_ai_apply",
|
||||
"capability.seo_ai_rewrite",
|
||||
"capability.api_access",
|
||||
"capability.byok"
|
||||
],
|
||||
"starter_plus_allows_ai_surfaces": true,
|
||||
"growth_byok_default": true,
|
||||
"all_plans_core": [
|
||||
"shell.navigation",
|
||||
"dashboard.overview",
|
||||
"catalog.products",
|
||||
"catalog.products.process_categories",
|
||||
"catalog.products.process_attributes",
|
||||
"feeds.list",
|
||||
"feeds.export_feeds",
|
||||
"stores.hub",
|
||||
"processing.monitor",
|
||||
"billing.overview",
|
||||
"settings.profile",
|
||||
"support.center",
|
||||
"capability.normalize_specs_fill",
|
||||
"capability.eprel",
|
||||
"capability.sku_cap",
|
||||
"capability.ai_credits"
|
||||
],
|
||||
"notes": [
|
||||
"Expand leaves from 01 catalog; parent deny implies children deny for UI",
|
||||
"capability.sku_cap / ai_credits enable hooks; numeric limits stay on plans meters"
|
||||
]
|
||||
},
|
||||
"api": {
|
||||
"admin_plans": {
|
||||
"list": "GET /api/admin/plans",
|
||||
"upsert": "POST /api/admin/plans",
|
||||
"plan_fields_added": [
|
||||
"features",
|
||||
"resolved_features"
|
||||
],
|
||||
"features_write_semantics": "replace stored overrides object"
|
||||
},
|
||||
"admin_feature_gates": {
|
||||
"list": "GET /api/admin/feature-gates",
|
||||
"put": "PUT /api/admin/feature-gates",
|
||||
"put_section": "PUT /api/admin/feature-gates/sections/{section}",
|
||||
"auth": "RequirePlatformAdmin",
|
||||
"response_shape": {
|
||||
"sections": {
|
||||
"marketing": true
|
||||
},
|
||||
"features": {
|
||||
"capability.byok": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_capabilities": {
|
||||
"embed_in_credits_overview": true,
|
||||
"endpoints": [
|
||||
"GET /api/billing/credits",
|
||||
"GET /api/auth/me",
|
||||
"GET /api/billing/capabilities"
|
||||
],
|
||||
"credits_overview_fields_added": [
|
||||
"features",
|
||||
"disabled_features",
|
||||
"feature_etag"
|
||||
],
|
||||
"capabilities_response": {
|
||||
"plan_id": "int64",
|
||||
"plan_name": "string",
|
||||
"is_custom": "bool",
|
||||
"features": "map[string]bool effective full registry",
|
||||
"sections": "map[string]bool global section enables",
|
||||
"entitlements": {
|
||||
"can_use_ai": "bool",
|
||||
"can_use_eprel": "bool",
|
||||
"is_free_plan": "bool",
|
||||
"is_paid_plan": "bool"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enforcement_error": {
|
||||
"http_status": 402,
|
||||
"body": {
|
||||
"error": "feature_disabled",
|
||||
"code": "plan_gate",
|
||||
"feature": "marketing.campaigns.generate_ai",
|
||||
"upgrade_url": "/plans"
|
||||
}
|
||||
}
|
||||
},
|
||||
"backend_symbols_to_extend": {
|
||||
"reuse": [
|
||||
"apps/api/internal/billing/entitlements.go#EntitlementsForCompany",
|
||||
"apps/api/internal/billing/entitlements.go#ComputeEntitlements",
|
||||
"apps/api/internal/billing/service.go#Plan",
|
||||
"apps/api/internal/billing/service.go#CreditsOverview",
|
||||
"apps/api/internal/billing/service.go#UpsertPlan",
|
||||
"apps/api/internal/billing/service.go#EnsureDefaultPlans",
|
||||
"apps/api/internal/billing/service.go#AssertCanStartProcessing",
|
||||
"apps/api/internal/httpapi/billing_handlers.go",
|
||||
"apps/web/src/routes/admin/billing/+page.svelte",
|
||||
"apps/web/src/lib/billing-display.ts",
|
||||
"apps/web/src/lib/plan-gates.ts",
|
||||
"apps/web/src/lib/components/Nav.svelte"
|
||||
],
|
||||
"new_symbols": [
|
||||
"DefaultPlanFeatures",
|
||||
"ResolveFeatures",
|
||||
"FeaturesForCompany",
|
||||
"AssertFeature",
|
||||
"canFeature"
|
||||
]
|
||||
},
|
||||
"migration": {
|
||||
"file_suggestion": "apps/api/sql/schema/026_plan_features.sql",
|
||||
"idempotent": true,
|
||||
"destructive": false,
|
||||
"steps": [
|
||||
"ALTER TABLE plans ADD COLUMN IF NOT EXISTS features JSONB NOT NULL DEFAULT '{}'",
|
||||
"CREATE TABLE IF NOT EXISTS platform_feature_gates (...)",
|
||||
"EnsureDefaultPlans must not clobber non-empty features",
|
||||
"Migrator inserts '{}' when source lacks column"
|
||||
]
|
||||
},
|
||||
"frontend_gating": {
|
||||
"helper": "canFeature(credits, key)",
|
||||
"nav_filter": "Nav.svelte primaryItems + moreItemsAll by parent feature_key",
|
||||
"fail_open_until_cutover": true,
|
||||
"optional_env": "FEATURES_ENFORCE=1"
|
||||
},
|
||||
"breaking_changes": [],
|
||||
"breaking_changes_policy": "prefer_none_additive_only",
|
||||
"preserve_contracts": [
|
||||
"can_use_ai",
|
||||
"can_use_eprel",
|
||||
"is_free_plan",
|
||||
"is_paid_plan",
|
||||
"public plan names Free/Starter/Growth/Business/Enterprise",
|
||||
"Stripe checkout public-ladder-only",
|
||||
"402 plan_gate for existing credit/SKU errors"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
# 03 — Plan permission CONTRACT (agent 3/10)
|
||||
|
||||
**Status:** Design only — implementers own schema/API/UI.
|
||||
**Coordinates with:** `01-dashboard-feature-catalog.md`, `01-feature-keys.json`, `02-plans-permissions-current.md`, `02-extension-points.json`.
|
||||
**Machine-readable twin:** `03-permission-contract.json`.
|
||||
|
||||
---
|
||||
|
||||
## PROBLEM
|
||||
|
||||
Admins need to enable/disable every dashboard tab/section/functionality **per plan**, seed sensible defaults for the public ladder, default **all-on** for custom deals, flip **global section master switches**, and have runtime checks enforce:
|
||||
|
||||
```
|
||||
effective(feature) = plan_allows(feature) AND global_section_enabled(section(feature)) AND global_feature_enabled(feature)
|
||||
```
|
||||
|
||||
UI may hide; **API must fail closed** (reuse 402 `plan_gate` where an action is blocked).
|
||||
|
||||
---
|
||||
|
||||
## CONTEXT (tools / sources)
|
||||
|
||||
| Source | Finding |
|
||||
|--------|---------|
|
||||
| codehelper kickoff/investigate | Reuse `Entitlements` / `ComputeEntitlements` / `EntitlementsForCompany`, `Plan` / `UpsertPlan` / `EnsureDefaultPlans`, `CreditsOverview`, `AssertCanStartProcessing` |
|
||||
| Agent 1 | Canonical `feature_key` catalog + section groups + default-by-plan hints |
|
||||
| Agent 2 | No permission matrix today; capabilities derived; `plans.is_custom` exists; unused `company_plans.custom_*`; extension points list |
|
||||
| Schema | `plans` has meters only — no features column yet |
|
||||
| Platform settings | SystemCompanyID JSON exists, but **dedicated gate table** preferred for typed admin UX |
|
||||
|
||||
**ASSUMPTION:** "Package" equals a `plans` row (no separate packages table) — per agent 2.
|
||||
**ASSUMPTION:** Enterprise remains public ladder + `is_custom=true`; custom-package "all features on" applies to any `is_custom=true` including Enterprise unless a key is explicitly `false` in `plans.features`.
|
||||
**ASSUMPTION:** Metering (`max_products`, credit wallet, `can_use_ai`) stays as today; feature keys gate access/visibility and compose with entitlement booleans.
|
||||
|
||||
---
|
||||
|
||||
## 1. Runtime resolution (normative)
|
||||
|
||||
```
|
||||
plan_allows(key) =
|
||||
if key present in plans.features -> plans.features[key]
|
||||
else if plans.is_custom -> true
|
||||
else -> DefaultMatrix[normalizePlanName(plans.name)][key]
|
||||
(missing matrix entry -> false for unknown keys;
|
||||
true only if catalog marks "All plans")
|
||||
|
||||
global_section_enabled(section) =
|
||||
if section present in platform_feature_gates (kind=section) -> enabled
|
||||
else -> true # migration-safe: globals default ON
|
||||
|
||||
global_feature_enabled(key) =
|
||||
if key present in platform_feature_gates (kind=feature) -> enabled
|
||||
else -> true
|
||||
|
||||
effective(key) = plan_allows(key)
|
||||
AND global_section_enabled(section_of(key))
|
||||
AND global_feature_enabled(key)
|
||||
```
|
||||
|
||||
**Parent implication (UI convenience, not storage):** If a parent key is disabled (e.g. `catalog.products`), children under that prefix SHOULD be treated as disabled for nav/tabs even if a child row is `true`. Server enforcement uses the **specific** action key; also check the nearest nav parent for route-level capabilities.
|
||||
|
||||
**Orthogonal (unchanged):** company role / `is_platform_admin`, Stripe state, env/platform enricher flags.
|
||||
|
||||
---
|
||||
|
||||
## 2. Data model
|
||||
|
||||
### 2.1 `plans.features` (per-plan overrides)
|
||||
|
||||
| Column | Type | Default | Notes |
|
||||
|--------|------|---------|-------|
|
||||
| `features` | `JSONB NOT NULL` | `'{}'` | Sparse map `feature_key -> boolean` overrides only |
|
||||
|
||||
Extend Go `Plan` with `Features map[string]bool \`json:"features,omitempty"\``.
|
||||
`UpsertPlan` / list handlers read/write the column. Do **not** store the full expanded matrix on every row.
|
||||
|
||||
### 2.2 `platform_feature_gates` (global master switches)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS platform_feature_gates (
|
||||
gate_key TEXT PRIMARY KEY,
|
||||
kind TEXT NOT NULL CHECK (kind IN ('section', 'feature')),
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_by UUID NULL REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS platform_feature_gates_kind_idx
|
||||
ON platform_feature_gates (kind);
|
||||
```
|
||||
|
||||
- **Section master:** `kind='section'`, keys from agent 1: `shell`, `dashboard`, `catalog`, `feeds`, `stores`, `processing`, `marketing`, `integrations`, `billing`, `settings`, `support`, `capabilities`.
|
||||
- **Optional feature master:** `kind='feature'`, full feature key kill-switch across all plans.
|
||||
- Missing row => enabled (non-breaking).
|
||||
|
||||
### 2.3 Do not add
|
||||
|
||||
| Avoid | Why |
|
||||
|-------|-----|
|
||||
| Separate `packages` table | packages ≡ `plans` |
|
||||
| Parallel permission service outside `billing` | Keep entitlements as SOT |
|
||||
| Per-company feature overrides in v1 | plan ∩ global only |
|
||||
| Replacing meters with feature keys alone | SKU/credits stay |
|
||||
|
||||
---
|
||||
|
||||
## 3. Feature key namespace
|
||||
|
||||
**Authority:** `01-feature-keys.json` + `01-dashboard-feature-catalog.md`.
|
||||
|
||||
- Dotted segments, `snake_case` leaves (`marketing.campaigns.generate_ai`)
|
||||
- First segment = section
|
||||
- Nav/route parents are first-class keys
|
||||
- Cross-cutting gates under `capability.*`
|
||||
- `/admin/*` excluded from tenant matrix
|
||||
- Admin write rejects unknown keys (`400`); unknown stored keys ignored at resolve, stripped on validated upsert
|
||||
|
||||
---
|
||||
|
||||
## 4. Default matrix sketch
|
||||
|
||||
Custom (`is_custom`) / Enterprise (seeded `is_custom=true`) = **all ON** unless override.
|
||||
|
||||
| Key group | Free | Starter | Growth | Business | Custom |
|
||||
|-----------|------|---------|--------|----------|--------|
|
||||
| `shell.*`, `dashboard.*` | ON | ON | ON | ON | ON |
|
||||
| `catalog.products` + non-AI process | ON | ON | ON | ON | ON |
|
||||
| `catalog.products.process_ai_*` | OFF | ON | ON | ON | ON |
|
||||
| `catalog.categories` / attributes / standard_fields | ON | ON | ON | ON | ON |
|
||||
| `feeds.*`, `stores.*`, `processing.monitor` | ON | ON | ON | ON | ON |
|
||||
| `marketing.campaigns` list/edit | ON | ON | ON | ON | ON |
|
||||
| `marketing.campaigns.generate_ai` / live `send` | OFF | ON | ON | ON | ON |
|
||||
| `marketing.brand_ai_apply` / `seo.ai_rewrite` | OFF | ON | ON | ON | ON |
|
||||
| `integrations.ai` / `email` | ON | ON | ON | ON | ON |
|
||||
| `integrations.ai.byok` | OFF | OFF | ON | ON | ON |
|
||||
| `billing.*`, `support.*` | ON | ON | ON | ON | ON |
|
||||
| `settings.profile` / company / alerts / team | ON | ON | ON | ON | ON |
|
||||
| `settings.api_keys` | OFF | ON | ON | ON | ON |
|
||||
| `capability.normalize_specs_fill` / `eprel` / `sku_cap` / `ai_credits` | ON | ON | ON | ON | ON |
|
||||
| `capability.ai_processing` / `campaign_ai` / `email_live_send` / `brand_ai_apply` / `seo_ai_rewrite` | OFF | ON | ON | ON | ON |
|
||||
| `capability.api_access` | OFF | ON | ON | ON | ON |
|
||||
| `capability.byok` | OFF | OFF | ON | ON | ON |
|
||||
|
||||
AI feature keys compose with existing `can_use_ai` + wallet. Numeric SKU/credit limits remain on `plans` meters.
|
||||
`DefaultPlanFeatures(name, isCustom)` lives next to `defaultPublicPlans`. **`EnsureDefaultPlans` must not clobber non-empty `features`.**
|
||||
|
||||
---
|
||||
|
||||
## 5. API shapes
|
||||
|
||||
### Admin plans (extend existing)
|
||||
|
||||
`GET /api/admin/plans` / `POST /api/admin/plans` — add:
|
||||
|
||||
```json
|
||||
{
|
||||
"features": { "settings.api_keys": true },
|
||||
"resolved_features": { "catalog.products": true, "marketing.campaigns.generate_ai": true }
|
||||
}
|
||||
```
|
||||
|
||||
- `features` = stored overrides (may be `{}`)
|
||||
- `resolved_features` = `plan_allows` only (ignore globals so admin sees package intent)
|
||||
- Upsert **replaces** the overrides object; validate registry keys
|
||||
|
||||
### Admin global gates (new)
|
||||
|
||||
| Method | Path |
|
||||
|--------|------|
|
||||
| `GET` | `/api/admin/feature-gates` |
|
||||
| `PUT` | `/api/admin/feature-gates` |
|
||||
| `PUT` | `/api/admin/feature-gates/sections/{section}` |
|
||||
|
||||
Response/body: `{ "sections": { "marketing": true }, "features": { "capability.byok": false } }`.
|
||||
Auth: `RequirePlatformAdmin`.
|
||||
|
||||
### User capabilities
|
||||
|
||||
Extend `CreditsOverview` (`GET /api/billing/credits`, `/api/auth/me`):
|
||||
|
||||
```json
|
||||
{
|
||||
"can_use_ai": false,
|
||||
"features": { "catalog.products": true, "marketing.campaigns.generate_ai": false },
|
||||
"disabled_features": ["marketing.campaigns.generate_ai"],
|
||||
"feature_etag": "sha256:…"
|
||||
}
|
||||
```
|
||||
|
||||
Also: `GET /api/billing/capabilities` with full effective registry + `sections` + nested `entitlements`.
|
||||
|
||||
### Enforcement error
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "feature_disabled",
|
||||
"code": "plan_gate",
|
||||
"feature": "marketing.campaigns.generate_ai",
|
||||
"upgrade_url": "/plans"
|
||||
}
|
||||
```
|
||||
|
||||
HTTP 402; map beside existing plan_gate codes.
|
||||
|
||||
---
|
||||
|
||||
## 6. Backend placement
|
||||
|
||||
| Concern | Where |
|
||||
|---------|--------|
|
||||
| Resolve | `billing.ResolveFeatures` |
|
||||
| Load | Wire into `CreditsOverview` / `EntitlementsForCompany` assembly (additive fields) |
|
||||
| Defaults | `DefaultPlanFeatures` beside `defaultPublicPlans` |
|
||||
| Assert | `AssertFeature(ctx, companyID, key)` at agent-2 extension points |
|
||||
| Admin HTTP | New handlers under `/api/admin` |
|
||||
|
||||
UI-only checks without server `AssertFeature` are out of contract.
|
||||
|
||||
---
|
||||
|
||||
## 7. Migration (idempotent, non-destructive)
|
||||
|
||||
File suggestion: `apps/api/sql/schema/026_plan_features.sql`
|
||||
|
||||
1. `ALTER TABLE plans ADD COLUMN IF NOT EXISTS features JSONB NOT NULL DEFAULT '{}'::jsonb`
|
||||
2. `CREATE TABLE IF NOT EXISTS platform_feature_gates (...)`
|
||||
3. No meter rewrites; ETL inserts `'{}'` if source lacks column
|
||||
4. Down migration optional; prefer leave additive artifacts
|
||||
|
||||
---
|
||||
|
||||
## 8. Frontend gating pattern
|
||||
|
||||
1. Read `credits.features` from `/api/auth/me`
|
||||
2. Helper `canFeature(credits, key)` beside `plan-gates.ts` / `billing-display.ts`
|
||||
3. Filter `Nav.svelte` items via parent feature keys (`href -> feature_key` map)
|
||||
4. Gate tabs/actions; show upgrade CTA when false
|
||||
5. Fail-open in helper when `features` missing (pre-cutover); optional `FEATURES_ENFORCE=1` for new 402s
|
||||
|
||||
---
|
||||
|
||||
## 9. BREAKING changes
|
||||
|
||||
**None preferred.** Additive columns, fields, and routes only. Preserve `can_use_ai`, `can_use_eprel`, free/paid flags, public plan names, Stripe public-ladder checkout.
|
||||
|
||||
---
|
||||
|
||||
## 10. Verification checklist
|
||||
|
||||
- [ ] Migrate empty + existing DB
|
||||
- [ ] `TestResolveFeatures_*` for Free / custom / global section off
|
||||
- [ ] Admin upsert round-trip; `EnsureDefaultPlans` does not clobber overrides
|
||||
- [ ] Global section off affects all plans
|
||||
- [ ] Custom plan => all registry keys effective true
|
||||
- [ ] `/api/auth/me` carries effective `features`
|
||||
- [ ] Mutating AI/campaign/SEO/API-key paths call `AssertFeature` plus existing meters
|
||||
|
||||
---
|
||||
|
||||
## Open questions (defaults chosen)
|
||||
|
||||
1. Full registry on `/me` for v1 (not sparse).
|
||||
2. Enterprise follows custom-all-on via `is_custom`.
|
||||
3. Client fail-open until features payload exists.
|
||||
@@ -0,0 +1,99 @@
|
||||
# 04 — Backend model (agent 4/10)
|
||||
|
||||
Implements storage + billing service APIs for plan dashboard permissions and global section/feature master switches, aligned with `03-permission-contract.md`.
|
||||
|
||||
**BREAKING:** none (additive schema + fields + methods only).
|
||||
|
||||
---
|
||||
|
||||
## Schema (`apps/api/sql/schema/026_plan_features.sql`)
|
||||
|
||||
| Artifact | Purpose |
|
||||
|----------|---------|
|
||||
| `plans.features` | `JSONB NOT NULL DEFAULT '{}'` — sparse `feature_key → bool` overrides |
|
||||
| `platform_feature_gates` | Global masters: `gate_key`, `kind` (`section`\|`feature`), `enabled`, `updated_at`, `updated_by` |
|
||||
|
||||
Missing gate rows ⇒ enabled (migration-safe). Goose Up/Down included.
|
||||
|
||||
---
|
||||
|
||||
## Go package (`apps/api/internal/billing/`)
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `feature_catalog.go` | Registry keys, sections, Free/Starter deny lists |
|
||||
| `plan_features.go` | Resolve + admin CRUD (`Get/SetFeatureGates`, `Get/SetPlanFeatures`, `CapabilitiesForCompany`, …) |
|
||||
| `features_api.go` | Task-named surface: `ListFeatures`, `IsAllowed`, `SetPlanFeature`, `SetGlobalFeature`, `EnableAllForPlan`, `ApplyDefaultMatrix`, `AssertFeature`, `ResolveFeatures` |
|
||||
| `default_plan_features_seed.go` | `EnsureDefaultFeatureSeeds` (idempotent section rows; never clobber non-empty plan overrides) |
|
||||
| `service.go` | `Plan.Features` / `ResolvedFeatures`; `ListPlans` / `UpsertPlan` read/write JSON; `CreditsOverview` embeds effective features |
|
||||
| `client_errors.go` | Maps feature gate errors for HTTP clients |
|
||||
| `features_api_test.go` | Pure resolve/default/validate tests |
|
||||
|
||||
`EnsureDefaultPlans` still updates meters only — **does not** overwrite `features`.
|
||||
|
||||
---
|
||||
|
||||
## Resolution (normative)
|
||||
|
||||
```
|
||||
effective(key) = plan_allows(key)
|
||||
AND global_section_enabled(section(key))
|
||||
AND global_feature_enabled(key)
|
||||
```
|
||||
|
||||
- `plan_allows`: override in `plans.features` if present; else `is_custom` ⇒ true; else `DefaultPlanFeatures(name, false)`.
|
||||
- Globals: missing row ⇒ true.
|
||||
|
||||
---
|
||||
|
||||
## How to use the service
|
||||
|
||||
```go
|
||||
svc := &billing.Service{Pool: pool}
|
||||
|
||||
// Catalog
|
||||
defs, _ := svc.ListFeatures(ctx)
|
||||
|
||||
// Effective check (company's active plan ∧ globals)
|
||||
ok, err := svc.IsAllowed(ctx, companyID, "marketing.campaigns.generate_ai")
|
||||
if err := svc.AssertFeature(ctx, companyID, "marketing.campaigns.generate_ai"); err != nil {
|
||||
// errors.Is(err, billing.ErrFeatureDisabled) → HTTP 402 plan_gate
|
||||
}
|
||||
|
||||
// Admin: per-plan override merge / replace / all-on / reset defaults
|
||||
_ = svc.SetPlanFeature(ctx, planID, "settings.api_keys", true)
|
||||
_, _ = svc.SetPlanFeatures(ctx, planID, map[string]bool{"settings.api_keys": true})
|
||||
_ = svc.EnableAllForPlan(ctx, planID)
|
||||
_ = svc.ApplyDefaultMatrix(ctx, planID) // sparse Free/Starter offs; custom → {}
|
||||
|
||||
// Admin: global masters (section or feature key)
|
||||
_ = svc.SetGlobalFeature(ctx, "marketing", false, &adminUserID)
|
||||
_ = svc.SetGlobalFeature(ctx, "capability.byok", false, &adminUserID)
|
||||
gates, _ := svc.GetFeatureGates(ctx)
|
||||
|
||||
// Tenant payload (also on CreditsOverview /auth/me)
|
||||
caps, _ := svc.CapabilitiesForCompany(ctx, companyID)
|
||||
// caps.Features, caps.DisabledFeatures, caps.FeatureETag, caps.Entitlements
|
||||
```
|
||||
|
||||
### Pure helpers (no DB)
|
||||
|
||||
- `DefaultPlanFeatures(name, isCustom)`
|
||||
- `PlanAllowsFeature(name, isCustom, overrides, key)`
|
||||
- `ResolveFeatures` / `ResolveEffectiveFeatures`
|
||||
- `SparseDefaultOverrides(name, isCustom)`
|
||||
- `IsKnownFeatureKey` / `IsKnownFeatureSection` / `SectionOfFeature`
|
||||
|
||||
---
|
||||
|
||||
## Seed / bootstrap
|
||||
|
||||
Call `EnsureDefaultFeatureSeeds` after migrations (alongside `EnsureDefaultPlans` if desired). Seeds section gates ON; leaves empty `{}` plan overrides so runtime defaults apply.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope (other agents)
|
||||
|
||||
- HTTP routes under `/api/admin/feature-gates` and capabilities endpoints
|
||||
- Frontend / admin UI toggles
|
||||
- Wiring `AssertFeature` into every handler (processing, SEO, campaigns, …)
|
||||
@@ -0,0 +1,128 @@
|
||||
# 05 — Plan permissions API (agent 5/10)
|
||||
|
||||
Session/dashboard HTTP surface for plan feature permissions. Aligns with
|
||||
`03-permission-contract.md`. Public `/api/v1` OpenAPI is unchanged (API-key product
|
||||
API); schemas below are the session contract for admin UI + dashboard gating.
|
||||
|
||||
Backend storage + resolve live in `billing` per `04-backend-model.md` /
|
||||
contract §2 / §6 (`plans.features` JSONB + `platform_feature_gates`). Migration:
|
||||
`apps/api/sql/schema/026_plan_features.sql`. Apply with `make migrate` /
|
||||
`.\scripts\migrate.ps1` before relying on writes.
|
||||
|
||||
Handlers call: `CapabilitiesForCompany`, `GetPlanFeatures`, `SetPlanFeatures`,
|
||||
`EnableAllPlanFeatures`, `GetFeatureGates`, `SetFeatureGates`, `SetSectionGate`
|
||||
(and agent-4 aliases `EnableAllForPlan` / `SetGlobalFeature` / `AssertFeature`).
|
||||
|
||||
---
|
||||
|
||||
## Endpoint list + auth
|
||||
|
||||
| Method | Path | Auth | Purpose |
|
||||
|--------|------|------|---------|
|
||||
| `GET` | `/api/billing/capabilities` | Session + company | Effective features = plan ∩ global |
|
||||
| `GET` | `/api/billing/credits` | Session + company | Existing credits overview **plus** `features`, `disabled_features`, `feature_etag` |
|
||||
| `GET` | `/api/auth/me` | Session | Embeds credits overview (same feature fields when credits load) |
|
||||
| `GET` | `/api/admin/plans` | Session + **platform admin** | Plans include `features` (overrides) + `resolved_features` (plan_allows) |
|
||||
| `POST` | `/api/admin/plans` | Session + **platform admin** | Upsert plan; optional `features` replaces overrides when present |
|
||||
| `GET` | `/api/admin/plans/{planID}/features` | Session + **platform admin** | Plan feature editor payload |
|
||||
| `PUT` | `/api/admin/plans/{planID}/features` | Session + **platform admin** | Replace overrides `{ "features": { "key": true } }` |
|
||||
| `POST` | `/api/admin/plans/{planID}/features/enable-all` | Session + **platform admin** | Set every registry key `true` (custom packages) |
|
||||
| `GET` | `/api/admin/feature-gates` | Session + **platform admin** | Global section + feature master switches |
|
||||
| `PUT` | `/api/admin/feature-gates` | Session + **platform admin** | Partial upsert `{ "sections": {...}, "features": {...} }` |
|
||||
| `PUT` | `/api/admin/feature-gates/sections/{section}` | Session + **platform admin** | Enable/disable one section for **ALL** plans |
|
||||
|
||||
Mutations require CSRF (dashboard session stack) + platform admin for `/api/admin/*`.
|
||||
|
||||
Unknown feature keys / sections → `400` `{ "error": "…" }` via `billing.ClientError`.
|
||||
|
||||
---
|
||||
|
||||
## Response shapes
|
||||
|
||||
### `GET /api/billing/capabilities`
|
||||
|
||||
```json
|
||||
{
|
||||
"plan_id": 3,
|
||||
"plan_name": "Starter",
|
||||
"is_custom": false,
|
||||
"has_active_plan": true,
|
||||
"features": { "catalog.products": true, "settings.api_keys": true },
|
||||
"sections": { "catalog": true, "marketing": true },
|
||||
"disabled_features": ["integrations.ai.byok"],
|
||||
"feature_etag": "sha256:…",
|
||||
"entitlements": {
|
||||
"plan_name": "Starter",
|
||||
"is_free_plan": false,
|
||||
"is_paid_plan": true,
|
||||
"can_use_ai": true,
|
||||
"can_use_eprel": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `GET|PUT /api/admin/plans/{planID}/features`
|
||||
|
||||
```json
|
||||
{
|
||||
"plan_id": 3,
|
||||
"plan_name": "Starter",
|
||||
"is_custom": false,
|
||||
"features": { "settings.api_keys": true },
|
||||
"resolved_features": { "catalog.products": true, "integrations.ai.byok": false }
|
||||
}
|
||||
```
|
||||
|
||||
- `features` = stored sparse overrides (`{}` if none)
|
||||
- `resolved_features` = full registry via `plan_allows` only (globals ignored)
|
||||
|
||||
### `GET|PUT /api/admin/feature-gates`
|
||||
|
||||
```json
|
||||
{
|
||||
"sections": { "shell": true, "marketing": false },
|
||||
"features": { "capability.byok": false }
|
||||
}
|
||||
```
|
||||
|
||||
Missing gate rows default **enabled** (migration-safe).
|
||||
|
||||
### `PUT /api/admin/feature-gates/sections/{section}`
|
||||
|
||||
```json
|
||||
{ "enabled": false }
|
||||
```
|
||||
|
||||
Returns full `FeatureGatesView`.
|
||||
|
||||
---
|
||||
|
||||
## OpenAPI (session fragment)
|
||||
|
||||
See `docs/plan-permissions/05-openapi-fragment.yaml` for OpenAPI 3.1 path/components
|
||||
suitable for admin/dashboard clients. Not mounted on `/api/v1/openapi.yaml`.
|
||||
|
||||
---
|
||||
|
||||
## Files touched (agent 5)
|
||||
|
||||
| File | Intent |
|
||||
|------|--------|
|
||||
| `apps/api/internal/httpapi/plan_features_handlers.go` | Handlers |
|
||||
| `apps/api/internal/httpapi/server.go` | Route mount |
|
||||
| `apps/api/internal/billing/plan_features.go` | Service + DTOs (shared with agent 4) |
|
||||
| `apps/api/internal/billing/feature_catalog.go` | Registry from `01-feature-keys.json` |
|
||||
| `apps/api/internal/billing/service.go` | `Plan`/`CreditsOverview`/`ListPlans`/`UpsertPlan` |
|
||||
| `apps/api/internal/billing/client_errors.go` | Client-facing feature errors |
|
||||
| `apps/api/sql/schema/026_plan_features.sql` | Additive migration |
|
||||
|
||||
---
|
||||
|
||||
## Manual verify
|
||||
|
||||
1. `.\scripts\migrate.ps1` (or `make migrate`)
|
||||
2. Platform admin: `GET /api/admin/feature-gates` → all sections true
|
||||
3. `PUT /api/admin/feature-gates/sections/marketing` `{ "enabled": false }`
|
||||
4. Tenant session: `GET /api/billing/capabilities` → marketing keys in `disabled_features`
|
||||
5. `POST /api/admin/plans/{id}/features/enable-all` then `GET` → all `resolved_features` true
|
||||
6. Non-admin `PUT /api/admin/feature-gates` → 403
|
||||
@@ -0,0 +1,243 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Descrybe plan feature permissions (session API)
|
||||
version: "1.0.0"
|
||||
description: >
|
||||
Dashboard/admin session endpoints for plan ∩ global feature permissions.
|
||||
Not part of the public /api/v1 OpenAPI document.
|
||||
paths:
|
||||
/api/billing/capabilities:
|
||||
get:
|
||||
operationId: getBillingCapabilities
|
||||
tags: [billing]
|
||||
security: [{ sessionCookie: [] }]
|
||||
summary: Effective plan ∩ global feature matrix for the active company
|
||||
responses:
|
||||
"200":
|
||||
description: Capabilities
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Capabilities"
|
||||
"401":
|
||||
$ref: "#/components/responses/Error"
|
||||
/api/admin/plans/{planID}/features:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PlanID"
|
||||
get:
|
||||
operationId: adminGetPlanFeatures
|
||||
tags: [admin]
|
||||
security: [{ sessionCookie: [], platformAdmin: [] }]
|
||||
summary: Plan feature overrides + resolved plan_allows matrix
|
||||
responses:
|
||||
"200":
|
||||
description: Plan features
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PlanFeaturesView"
|
||||
"400":
|
||||
$ref: "#/components/responses/Error"
|
||||
"403":
|
||||
$ref: "#/components/responses/Error"
|
||||
put:
|
||||
operationId: adminPutPlanFeatures
|
||||
tags: [admin]
|
||||
security: [{ sessionCookie: [], platformAdmin: [] }]
|
||||
summary: Replace plan feature overrides
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PlanFeaturesUpdate"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated plan features
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PlanFeaturesView"
|
||||
"400":
|
||||
$ref: "#/components/responses/Error"
|
||||
"403":
|
||||
$ref: "#/components/responses/Error"
|
||||
/api/admin/plans/{planID}/features/enable-all:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PlanID"
|
||||
post:
|
||||
operationId: adminEnableAllPlanFeatures
|
||||
tags: [admin]
|
||||
security: [{ sessionCookie: [], platformAdmin: [] }]
|
||||
summary: Enable every registry feature on the plan (custom packages)
|
||||
responses:
|
||||
"200":
|
||||
description: Updated plan features
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PlanFeaturesView"
|
||||
"400":
|
||||
$ref: "#/components/responses/Error"
|
||||
"403":
|
||||
$ref: "#/components/responses/Error"
|
||||
/api/admin/feature-gates:
|
||||
get:
|
||||
operationId: adminGetFeatureGates
|
||||
tags: [admin]
|
||||
security: [{ sessionCookie: [], platformAdmin: [] }]
|
||||
summary: Global section and feature master switches
|
||||
responses:
|
||||
"200":
|
||||
description: Feature gates
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/FeatureGatesView"
|
||||
"403":
|
||||
$ref: "#/components/responses/Error"
|
||||
put:
|
||||
operationId: adminPutFeatureGates
|
||||
tags: [admin]
|
||||
security: [{ sessionCookie: [], platformAdmin: [] }]
|
||||
summary: Partial upsert of global gates
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/FeatureGatesUpdate"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated gates
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/FeatureGatesView"
|
||||
"400":
|
||||
$ref: "#/components/responses/Error"
|
||||
"403":
|
||||
$ref: "#/components/responses/Error"
|
||||
/api/admin/feature-gates/sections/{section}:
|
||||
parameters:
|
||||
- name: section
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
enum: [shell, dashboard, catalog, feeds, stores, processing, marketing, integrations, billing, settings, support, capabilities]
|
||||
put:
|
||||
operationId: adminPutFeatureGateSection
|
||||
tags: [admin]
|
||||
security: [{ sessionCookie: [], platformAdmin: [] }]
|
||||
summary: Enable or disable a section for ALL plans
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SectionGateUpdate"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated gates
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/FeatureGatesView"
|
||||
"400":
|
||||
$ref: "#/components/responses/Error"
|
||||
"403":
|
||||
$ref: "#/components/responses/Error"
|
||||
components:
|
||||
securitySchemes:
|
||||
sessionCookie:
|
||||
type: apiKey
|
||||
in: cookie
|
||||
name: session
|
||||
platformAdmin:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: Requires users.is_platform_admin (session-derived; bearer shown for tooling only)
|
||||
parameters:
|
||||
PlanID:
|
||||
name: planID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 1
|
||||
responses:
|
||||
Error:
|
||||
description: Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [error]
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
schemas:
|
||||
Capabilities:
|
||||
type: object
|
||||
properties:
|
||||
plan_id: { type: integer, format: int64 }
|
||||
plan_name: { type: string }
|
||||
is_custom: { type: boolean }
|
||||
has_active_plan: { type: boolean }
|
||||
features:
|
||||
type: object
|
||||
additionalProperties: { type: boolean }
|
||||
sections:
|
||||
type: object
|
||||
additionalProperties: { type: boolean }
|
||||
disabled_features:
|
||||
type: array
|
||||
items: { type: string }
|
||||
feature_etag: { type: string }
|
||||
entitlements:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
PlanFeaturesView:
|
||||
type: object
|
||||
properties:
|
||||
plan_id: { type: integer, format: int64 }
|
||||
plan_name: { type: string }
|
||||
is_custom: { type: boolean }
|
||||
features:
|
||||
type: object
|
||||
additionalProperties: { type: boolean }
|
||||
resolved_features:
|
||||
type: object
|
||||
additionalProperties: { type: boolean }
|
||||
PlanFeaturesUpdate:
|
||||
type: object
|
||||
required: [features]
|
||||
properties:
|
||||
features:
|
||||
type: object
|
||||
additionalProperties: { type: boolean }
|
||||
FeatureGatesView:
|
||||
type: object
|
||||
properties:
|
||||
sections:
|
||||
type: object
|
||||
additionalProperties: { type: boolean }
|
||||
features:
|
||||
type: object
|
||||
additionalProperties: { type: boolean }
|
||||
FeatureGatesUpdate:
|
||||
type: object
|
||||
properties:
|
||||
sections:
|
||||
type: object
|
||||
additionalProperties: { type: boolean }
|
||||
features:
|
||||
type: object
|
||||
additionalProperties: { type: boolean }
|
||||
SectionGateUpdate:
|
||||
type: object
|
||||
required: [enabled]
|
||||
properties:
|
||||
enabled: { type: boolean }
|
||||
@@ -0,0 +1,717 @@
|
||||
{
|
||||
"agent": "06",
|
||||
"source": [
|
||||
"docs/plan-permissions/01-feature-keys.json",
|
||||
"docs/plan-permissions/03-permission-contract.md",
|
||||
"apps/api/internal/billing/feature_catalog.go",
|
||||
"apps/api/internal/billing/plan_features.go#DefaultPlanFeatures",
|
||||
"apps/web/src/lib/components/pricing/pricing-data.ts",
|
||||
"apps/web/src/lib/billing-display.ts",
|
||||
"docs/free-tier.md"
|
||||
],
|
||||
"assumptions": [
|
||||
"No plans.features_customized flag; non-empty plans.features = admin customization and is never wiped by EnsureDefaultFeatureSeeds",
|
||||
"Empty plans.features '{}' is unset; resolve uses DefaultPlanFeatures(name, is_custom)",
|
||||
"Enterprise is seeded is_custom=true so all features ON (custom path)",
|
||||
"Global section gates missing row => enabled; seeder inserts section rows as enabled ON CONFLICT DO NOTHING",
|
||||
"capability.eprel ON for all plans (matches CanUseEPREL always true; free-tier.md lag noted)",
|
||||
"Meters (SKU/credits/feed counts) stay on plans columns / marketing copy \u2014 feature keys gate access only"
|
||||
],
|
||||
"global_sections_default": {
|
||||
"shell": true,
|
||||
"dashboard": true,
|
||||
"catalog": true,
|
||||
"feeds": true,
|
||||
"stores": true,
|
||||
"processing": true,
|
||||
"marketing": true,
|
||||
"integrations": true,
|
||||
"billing": true,
|
||||
"settings": true,
|
||||
"support": true,
|
||||
"capabilities": true
|
||||
},
|
||||
"denied_by_plan": {
|
||||
"free": [
|
||||
"capability.ai_processing",
|
||||
"capability.api_access",
|
||||
"capability.brand_ai_apply",
|
||||
"capability.byok",
|
||||
"capability.campaign_ai",
|
||||
"capability.email_live_send",
|
||||
"capability.seo_ai_rewrite",
|
||||
"catalog.products.process_ai_descriptions",
|
||||
"catalog.products.process_ai_titles",
|
||||
"integrations.ai.byok",
|
||||
"marketing.brand_ai_apply",
|
||||
"marketing.campaigns.generate_ai",
|
||||
"marketing.campaigns.send",
|
||||
"marketing.seo.ai_rewrite",
|
||||
"settings.api_keys"
|
||||
],
|
||||
"starter": [
|
||||
"capability.byok",
|
||||
"integrations.ai.byok"
|
||||
],
|
||||
"growth": [],
|
||||
"business": [],
|
||||
"enterprise": [],
|
||||
"custom": []
|
||||
},
|
||||
"matrices": {
|
||||
"free": {
|
||||
"shell.navigation": true,
|
||||
"shell.command_palette": true,
|
||||
"shell.company_switcher": true,
|
||||
"shell.support_notifications": true,
|
||||
"shell.tutorial": true,
|
||||
"shell.account_menu": true,
|
||||
"shell.billing_recovery_banner": true,
|
||||
"dashboard.overview": true,
|
||||
"dashboard.stats": true,
|
||||
"dashboard.quick_links": true,
|
||||
"dashboard.recent_jobs": true,
|
||||
"dashboard.news_feed": true,
|
||||
"dashboard.activation_checklist": true,
|
||||
"dashboard.migrated_checklist": true,
|
||||
"dashboard.etl_gaps": true,
|
||||
"dashboard.store_reconnect": true,
|
||||
"dashboard.upgrade_banners": true,
|
||||
"catalog.products": true,
|
||||
"catalog.products.tab_processed": true,
|
||||
"catalog.products.tab_needs_review": true,
|
||||
"catalog.products.tab_error": true,
|
||||
"catalog.products.tab_processing": true,
|
||||
"catalog.products.tab_unprocessed": true,
|
||||
"catalog.products.process_categories": true,
|
||||
"catalog.products.process_attributes": true,
|
||||
"catalog.products.process_ai_titles": false,
|
||||
"catalog.products.process_ai_descriptions": false,
|
||||
"catalog.products.enrichment_review": true,
|
||||
"catalog.products.export_selection": true,
|
||||
"catalog.products.upgrade_prompt": true,
|
||||
"catalog.categories": true,
|
||||
"catalog.categories.title_formula": true,
|
||||
"catalog.categories.description_formula": true,
|
||||
"catalog.attributes": true,
|
||||
"catalog.attributes.bulk_import": true,
|
||||
"catalog.standard_fields": true,
|
||||
"catalog.standard_fields.groups": true,
|
||||
"catalog.structured_descriptions": true,
|
||||
"catalog.vector_categories": true,
|
||||
"feeds.list": true,
|
||||
"feeds.add_url": true,
|
||||
"feeds.add_csv": true,
|
||||
"feeds.sync": true,
|
||||
"feeds.mapping": true,
|
||||
"feeds.mapping.select_item": true,
|
||||
"feeds.mapping.map_fields": true,
|
||||
"feeds.export_feeds": true,
|
||||
"feeds.export_feeds.create": true,
|
||||
"feeds.export_feeds.generate": true,
|
||||
"feeds.uploads": true,
|
||||
"stores.hub": true,
|
||||
"stores.woocommerce": true,
|
||||
"stores.woocommerce.connection": true,
|
||||
"stores.woocommerce.categories": true,
|
||||
"stores.woocommerce.attributes": true,
|
||||
"stores.woocommerce.orders": true,
|
||||
"stores.woocommerce.reviews": true,
|
||||
"stores.woocommerce.settings": true,
|
||||
"stores.shopify": true,
|
||||
"stores.shopify.connection": true,
|
||||
"stores.shopify.orders": true,
|
||||
"stores.shopify.settings": true,
|
||||
"processing.monitor": true,
|
||||
"marketing.campaigns": true,
|
||||
"marketing.campaigns.create": true,
|
||||
"marketing.campaigns.generate_ai": false,
|
||||
"marketing.campaigns.send": false,
|
||||
"marketing.content_calendar": true,
|
||||
"marketing.brand_kit": true,
|
||||
"marketing.brand_ai_apply": false,
|
||||
"marketing.seo": true,
|
||||
"marketing.seo.template_fill": true,
|
||||
"marketing.seo.ai_rewrite": false,
|
||||
"marketing.reviews": true,
|
||||
"integrations.ai": true,
|
||||
"integrations.ai.byok": false,
|
||||
"integrations.email": true,
|
||||
"integrations.email.test": true,
|
||||
"integrations.email.blast": true,
|
||||
"billing.overview": true,
|
||||
"billing.customer_portal": true,
|
||||
"billing.quick_upgrade": true,
|
||||
"billing.plans_compare": true,
|
||||
"billing.checkout": true,
|
||||
"settings.profile": true,
|
||||
"settings.company": true,
|
||||
"settings.alerts": true,
|
||||
"settings.api_keys": false,
|
||||
"settings.team": true,
|
||||
"settings.team_invite": true,
|
||||
"support.center": true,
|
||||
"support.ticket_create": true,
|
||||
"support.ticket_thread": true,
|
||||
"capability.sku_cap": true,
|
||||
"capability.ai_credits": true,
|
||||
"capability.ai_processing": false,
|
||||
"capability.eprel": true,
|
||||
"capability.normalize_specs_fill": true,
|
||||
"capability.campaign_ai": false,
|
||||
"capability.email_live_send": false,
|
||||
"capability.brand_ai_apply": false,
|
||||
"capability.seo_ai_rewrite": false,
|
||||
"capability.feed_source_limit": true,
|
||||
"capability.export_feed_limit": true,
|
||||
"capability.storage_limit": true,
|
||||
"capability.api_access": false,
|
||||
"capability.byok": false
|
||||
},
|
||||
"starter": {
|
||||
"shell.navigation": true,
|
||||
"shell.command_palette": true,
|
||||
"shell.company_switcher": true,
|
||||
"shell.support_notifications": true,
|
||||
"shell.tutorial": true,
|
||||
"shell.account_menu": true,
|
||||
"shell.billing_recovery_banner": true,
|
||||
"dashboard.overview": true,
|
||||
"dashboard.stats": true,
|
||||
"dashboard.quick_links": true,
|
||||
"dashboard.recent_jobs": true,
|
||||
"dashboard.news_feed": true,
|
||||
"dashboard.activation_checklist": true,
|
||||
"dashboard.migrated_checklist": true,
|
||||
"dashboard.etl_gaps": true,
|
||||
"dashboard.store_reconnect": true,
|
||||
"dashboard.upgrade_banners": true,
|
||||
"catalog.products": true,
|
||||
"catalog.products.tab_processed": true,
|
||||
"catalog.products.tab_needs_review": true,
|
||||
"catalog.products.tab_error": true,
|
||||
"catalog.products.tab_processing": true,
|
||||
"catalog.products.tab_unprocessed": true,
|
||||
"catalog.products.process_categories": true,
|
||||
"catalog.products.process_attributes": true,
|
||||
"catalog.products.process_ai_titles": true,
|
||||
"catalog.products.process_ai_descriptions": true,
|
||||
"catalog.products.enrichment_review": true,
|
||||
"catalog.products.export_selection": true,
|
||||
"catalog.products.upgrade_prompt": true,
|
||||
"catalog.categories": true,
|
||||
"catalog.categories.title_formula": true,
|
||||
"catalog.categories.description_formula": true,
|
||||
"catalog.attributes": true,
|
||||
"catalog.attributes.bulk_import": true,
|
||||
"catalog.standard_fields": true,
|
||||
"catalog.standard_fields.groups": true,
|
||||
"catalog.structured_descriptions": true,
|
||||
"catalog.vector_categories": true,
|
||||
"feeds.list": true,
|
||||
"feeds.add_url": true,
|
||||
"feeds.add_csv": true,
|
||||
"feeds.sync": true,
|
||||
"feeds.mapping": true,
|
||||
"feeds.mapping.select_item": true,
|
||||
"feeds.mapping.map_fields": true,
|
||||
"feeds.export_feeds": true,
|
||||
"feeds.export_feeds.create": true,
|
||||
"feeds.export_feeds.generate": true,
|
||||
"feeds.uploads": true,
|
||||
"stores.hub": true,
|
||||
"stores.woocommerce": true,
|
||||
"stores.woocommerce.connection": true,
|
||||
"stores.woocommerce.categories": true,
|
||||
"stores.woocommerce.attributes": true,
|
||||
"stores.woocommerce.orders": true,
|
||||
"stores.woocommerce.reviews": true,
|
||||
"stores.woocommerce.settings": true,
|
||||
"stores.shopify": true,
|
||||
"stores.shopify.connection": true,
|
||||
"stores.shopify.orders": true,
|
||||
"stores.shopify.settings": true,
|
||||
"processing.monitor": true,
|
||||
"marketing.campaigns": true,
|
||||
"marketing.campaigns.create": true,
|
||||
"marketing.campaigns.generate_ai": true,
|
||||
"marketing.campaigns.send": true,
|
||||
"marketing.content_calendar": true,
|
||||
"marketing.brand_kit": true,
|
||||
"marketing.brand_ai_apply": true,
|
||||
"marketing.seo": true,
|
||||
"marketing.seo.template_fill": true,
|
||||
"marketing.seo.ai_rewrite": true,
|
||||
"marketing.reviews": true,
|
||||
"integrations.ai": true,
|
||||
"integrations.ai.byok": false,
|
||||
"integrations.email": true,
|
||||
"integrations.email.test": true,
|
||||
"integrations.email.blast": true,
|
||||
"billing.overview": true,
|
||||
"billing.customer_portal": true,
|
||||
"billing.quick_upgrade": true,
|
||||
"billing.plans_compare": true,
|
||||
"billing.checkout": true,
|
||||
"settings.profile": true,
|
||||
"settings.company": true,
|
||||
"settings.alerts": true,
|
||||
"settings.api_keys": true,
|
||||
"settings.team": true,
|
||||
"settings.team_invite": true,
|
||||
"support.center": true,
|
||||
"support.ticket_create": true,
|
||||
"support.ticket_thread": true,
|
||||
"capability.sku_cap": true,
|
||||
"capability.ai_credits": true,
|
||||
"capability.ai_processing": true,
|
||||
"capability.eprel": true,
|
||||
"capability.normalize_specs_fill": true,
|
||||
"capability.campaign_ai": true,
|
||||
"capability.email_live_send": true,
|
||||
"capability.brand_ai_apply": true,
|
||||
"capability.seo_ai_rewrite": true,
|
||||
"capability.feed_source_limit": true,
|
||||
"capability.export_feed_limit": true,
|
||||
"capability.storage_limit": true,
|
||||
"capability.api_access": true,
|
||||
"capability.byok": false
|
||||
},
|
||||
"growth": {
|
||||
"shell.navigation": true,
|
||||
"shell.command_palette": true,
|
||||
"shell.company_switcher": true,
|
||||
"shell.support_notifications": true,
|
||||
"shell.tutorial": true,
|
||||
"shell.account_menu": true,
|
||||
"shell.billing_recovery_banner": true,
|
||||
"dashboard.overview": true,
|
||||
"dashboard.stats": true,
|
||||
"dashboard.quick_links": true,
|
||||
"dashboard.recent_jobs": true,
|
||||
"dashboard.news_feed": true,
|
||||
"dashboard.activation_checklist": true,
|
||||
"dashboard.migrated_checklist": true,
|
||||
"dashboard.etl_gaps": true,
|
||||
"dashboard.store_reconnect": true,
|
||||
"dashboard.upgrade_banners": true,
|
||||
"catalog.products": true,
|
||||
"catalog.products.tab_processed": true,
|
||||
"catalog.products.tab_needs_review": true,
|
||||
"catalog.products.tab_error": true,
|
||||
"catalog.products.tab_processing": true,
|
||||
"catalog.products.tab_unprocessed": true,
|
||||
"catalog.products.process_categories": true,
|
||||
"catalog.products.process_attributes": true,
|
||||
"catalog.products.process_ai_titles": true,
|
||||
"catalog.products.process_ai_descriptions": true,
|
||||
"catalog.products.enrichment_review": true,
|
||||
"catalog.products.export_selection": true,
|
||||
"catalog.products.upgrade_prompt": true,
|
||||
"catalog.categories": true,
|
||||
"catalog.categories.title_formula": true,
|
||||
"catalog.categories.description_formula": true,
|
||||
"catalog.attributes": true,
|
||||
"catalog.attributes.bulk_import": true,
|
||||
"catalog.standard_fields": true,
|
||||
"catalog.standard_fields.groups": true,
|
||||
"catalog.structured_descriptions": true,
|
||||
"catalog.vector_categories": true,
|
||||
"feeds.list": true,
|
||||
"feeds.add_url": true,
|
||||
"feeds.add_csv": true,
|
||||
"feeds.sync": true,
|
||||
"feeds.mapping": true,
|
||||
"feeds.mapping.select_item": true,
|
||||
"feeds.mapping.map_fields": true,
|
||||
"feeds.export_feeds": true,
|
||||
"feeds.export_feeds.create": true,
|
||||
"feeds.export_feeds.generate": true,
|
||||
"feeds.uploads": true,
|
||||
"stores.hub": true,
|
||||
"stores.woocommerce": true,
|
||||
"stores.woocommerce.connection": true,
|
||||
"stores.woocommerce.categories": true,
|
||||
"stores.woocommerce.attributes": true,
|
||||
"stores.woocommerce.orders": true,
|
||||
"stores.woocommerce.reviews": true,
|
||||
"stores.woocommerce.settings": true,
|
||||
"stores.shopify": true,
|
||||
"stores.shopify.connection": true,
|
||||
"stores.shopify.orders": true,
|
||||
"stores.shopify.settings": true,
|
||||
"processing.monitor": true,
|
||||
"marketing.campaigns": true,
|
||||
"marketing.campaigns.create": true,
|
||||
"marketing.campaigns.generate_ai": true,
|
||||
"marketing.campaigns.send": true,
|
||||
"marketing.content_calendar": true,
|
||||
"marketing.brand_kit": true,
|
||||
"marketing.brand_ai_apply": true,
|
||||
"marketing.seo": true,
|
||||
"marketing.seo.template_fill": true,
|
||||
"marketing.seo.ai_rewrite": true,
|
||||
"marketing.reviews": true,
|
||||
"integrations.ai": true,
|
||||
"integrations.ai.byok": true,
|
||||
"integrations.email": true,
|
||||
"integrations.email.test": true,
|
||||
"integrations.email.blast": true,
|
||||
"billing.overview": true,
|
||||
"billing.customer_portal": true,
|
||||
"billing.quick_upgrade": true,
|
||||
"billing.plans_compare": true,
|
||||
"billing.checkout": true,
|
||||
"settings.profile": true,
|
||||
"settings.company": true,
|
||||
"settings.alerts": true,
|
||||
"settings.api_keys": true,
|
||||
"settings.team": true,
|
||||
"settings.team_invite": true,
|
||||
"support.center": true,
|
||||
"support.ticket_create": true,
|
||||
"support.ticket_thread": true,
|
||||
"capability.sku_cap": true,
|
||||
"capability.ai_credits": true,
|
||||
"capability.ai_processing": true,
|
||||
"capability.eprel": true,
|
||||
"capability.normalize_specs_fill": true,
|
||||
"capability.campaign_ai": true,
|
||||
"capability.email_live_send": true,
|
||||
"capability.brand_ai_apply": true,
|
||||
"capability.seo_ai_rewrite": true,
|
||||
"capability.feed_source_limit": true,
|
||||
"capability.export_feed_limit": true,
|
||||
"capability.storage_limit": true,
|
||||
"capability.api_access": true,
|
||||
"capability.byok": true
|
||||
},
|
||||
"business": {
|
||||
"shell.navigation": true,
|
||||
"shell.command_palette": true,
|
||||
"shell.company_switcher": true,
|
||||
"shell.support_notifications": true,
|
||||
"shell.tutorial": true,
|
||||
"shell.account_menu": true,
|
||||
"shell.billing_recovery_banner": true,
|
||||
"dashboard.overview": true,
|
||||
"dashboard.stats": true,
|
||||
"dashboard.quick_links": true,
|
||||
"dashboard.recent_jobs": true,
|
||||
"dashboard.news_feed": true,
|
||||
"dashboard.activation_checklist": true,
|
||||
"dashboard.migrated_checklist": true,
|
||||
"dashboard.etl_gaps": true,
|
||||
"dashboard.store_reconnect": true,
|
||||
"dashboard.upgrade_banners": true,
|
||||
"catalog.products": true,
|
||||
"catalog.products.tab_processed": true,
|
||||
"catalog.products.tab_needs_review": true,
|
||||
"catalog.products.tab_error": true,
|
||||
"catalog.products.tab_processing": true,
|
||||
"catalog.products.tab_unprocessed": true,
|
||||
"catalog.products.process_categories": true,
|
||||
"catalog.products.process_attributes": true,
|
||||
"catalog.products.process_ai_titles": true,
|
||||
"catalog.products.process_ai_descriptions": true,
|
||||
"catalog.products.enrichment_review": true,
|
||||
"catalog.products.export_selection": true,
|
||||
"catalog.products.upgrade_prompt": true,
|
||||
"catalog.categories": true,
|
||||
"catalog.categories.title_formula": true,
|
||||
"catalog.categories.description_formula": true,
|
||||
"catalog.attributes": true,
|
||||
"catalog.attributes.bulk_import": true,
|
||||
"catalog.standard_fields": true,
|
||||
"catalog.standard_fields.groups": true,
|
||||
"catalog.structured_descriptions": true,
|
||||
"catalog.vector_categories": true,
|
||||
"feeds.list": true,
|
||||
"feeds.add_url": true,
|
||||
"feeds.add_csv": true,
|
||||
"feeds.sync": true,
|
||||
"feeds.mapping": true,
|
||||
"feeds.mapping.select_item": true,
|
||||
"feeds.mapping.map_fields": true,
|
||||
"feeds.export_feeds": true,
|
||||
"feeds.export_feeds.create": true,
|
||||
"feeds.export_feeds.generate": true,
|
||||
"feeds.uploads": true,
|
||||
"stores.hub": true,
|
||||
"stores.woocommerce": true,
|
||||
"stores.woocommerce.connection": true,
|
||||
"stores.woocommerce.categories": true,
|
||||
"stores.woocommerce.attributes": true,
|
||||
"stores.woocommerce.orders": true,
|
||||
"stores.woocommerce.reviews": true,
|
||||
"stores.woocommerce.settings": true,
|
||||
"stores.shopify": true,
|
||||
"stores.shopify.connection": true,
|
||||
"stores.shopify.orders": true,
|
||||
"stores.shopify.settings": true,
|
||||
"processing.monitor": true,
|
||||
"marketing.campaigns": true,
|
||||
"marketing.campaigns.create": true,
|
||||
"marketing.campaigns.generate_ai": true,
|
||||
"marketing.campaigns.send": true,
|
||||
"marketing.content_calendar": true,
|
||||
"marketing.brand_kit": true,
|
||||
"marketing.brand_ai_apply": true,
|
||||
"marketing.seo": true,
|
||||
"marketing.seo.template_fill": true,
|
||||
"marketing.seo.ai_rewrite": true,
|
||||
"marketing.reviews": true,
|
||||
"integrations.ai": true,
|
||||
"integrations.ai.byok": true,
|
||||
"integrations.email": true,
|
||||
"integrations.email.test": true,
|
||||
"integrations.email.blast": true,
|
||||
"billing.overview": true,
|
||||
"billing.customer_portal": true,
|
||||
"billing.quick_upgrade": true,
|
||||
"billing.plans_compare": true,
|
||||
"billing.checkout": true,
|
||||
"settings.profile": true,
|
||||
"settings.company": true,
|
||||
"settings.alerts": true,
|
||||
"settings.api_keys": true,
|
||||
"settings.team": true,
|
||||
"settings.team_invite": true,
|
||||
"support.center": true,
|
||||
"support.ticket_create": true,
|
||||
"support.ticket_thread": true,
|
||||
"capability.sku_cap": true,
|
||||
"capability.ai_credits": true,
|
||||
"capability.ai_processing": true,
|
||||
"capability.eprel": true,
|
||||
"capability.normalize_specs_fill": true,
|
||||
"capability.campaign_ai": true,
|
||||
"capability.email_live_send": true,
|
||||
"capability.brand_ai_apply": true,
|
||||
"capability.seo_ai_rewrite": true,
|
||||
"capability.feed_source_limit": true,
|
||||
"capability.export_feed_limit": true,
|
||||
"capability.storage_limit": true,
|
||||
"capability.api_access": true,
|
||||
"capability.byok": true
|
||||
},
|
||||
"enterprise": {
|
||||
"shell.navigation": true,
|
||||
"shell.command_palette": true,
|
||||
"shell.company_switcher": true,
|
||||
"shell.support_notifications": true,
|
||||
"shell.tutorial": true,
|
||||
"shell.account_menu": true,
|
||||
"shell.billing_recovery_banner": true,
|
||||
"dashboard.overview": true,
|
||||
"dashboard.stats": true,
|
||||
"dashboard.quick_links": true,
|
||||
"dashboard.recent_jobs": true,
|
||||
"dashboard.news_feed": true,
|
||||
"dashboard.activation_checklist": true,
|
||||
"dashboard.migrated_checklist": true,
|
||||
"dashboard.etl_gaps": true,
|
||||
"dashboard.store_reconnect": true,
|
||||
"dashboard.upgrade_banners": true,
|
||||
"catalog.products": true,
|
||||
"catalog.products.tab_processed": true,
|
||||
"catalog.products.tab_needs_review": true,
|
||||
"catalog.products.tab_error": true,
|
||||
"catalog.products.tab_processing": true,
|
||||
"catalog.products.tab_unprocessed": true,
|
||||
"catalog.products.process_categories": true,
|
||||
"catalog.products.process_attributes": true,
|
||||
"catalog.products.process_ai_titles": true,
|
||||
"catalog.products.process_ai_descriptions": true,
|
||||
"catalog.products.enrichment_review": true,
|
||||
"catalog.products.export_selection": true,
|
||||
"catalog.products.upgrade_prompt": true,
|
||||
"catalog.categories": true,
|
||||
"catalog.categories.title_formula": true,
|
||||
"catalog.categories.description_formula": true,
|
||||
"catalog.attributes": true,
|
||||
"catalog.attributes.bulk_import": true,
|
||||
"catalog.standard_fields": true,
|
||||
"catalog.standard_fields.groups": true,
|
||||
"catalog.structured_descriptions": true,
|
||||
"catalog.vector_categories": true,
|
||||
"feeds.list": true,
|
||||
"feeds.add_url": true,
|
||||
"feeds.add_csv": true,
|
||||
"feeds.sync": true,
|
||||
"feeds.mapping": true,
|
||||
"feeds.mapping.select_item": true,
|
||||
"feeds.mapping.map_fields": true,
|
||||
"feeds.export_feeds": true,
|
||||
"feeds.export_feeds.create": true,
|
||||
"feeds.export_feeds.generate": true,
|
||||
"feeds.uploads": true,
|
||||
"stores.hub": true,
|
||||
"stores.woocommerce": true,
|
||||
"stores.woocommerce.connection": true,
|
||||
"stores.woocommerce.categories": true,
|
||||
"stores.woocommerce.attributes": true,
|
||||
"stores.woocommerce.orders": true,
|
||||
"stores.woocommerce.reviews": true,
|
||||
"stores.woocommerce.settings": true,
|
||||
"stores.shopify": true,
|
||||
"stores.shopify.connection": true,
|
||||
"stores.shopify.orders": true,
|
||||
"stores.shopify.settings": true,
|
||||
"processing.monitor": true,
|
||||
"marketing.campaigns": true,
|
||||
"marketing.campaigns.create": true,
|
||||
"marketing.campaigns.generate_ai": true,
|
||||
"marketing.campaigns.send": true,
|
||||
"marketing.content_calendar": true,
|
||||
"marketing.brand_kit": true,
|
||||
"marketing.brand_ai_apply": true,
|
||||
"marketing.seo": true,
|
||||
"marketing.seo.template_fill": true,
|
||||
"marketing.seo.ai_rewrite": true,
|
||||
"marketing.reviews": true,
|
||||
"integrations.ai": true,
|
||||
"integrations.ai.byok": true,
|
||||
"integrations.email": true,
|
||||
"integrations.email.test": true,
|
||||
"integrations.email.blast": true,
|
||||
"billing.overview": true,
|
||||
"billing.customer_portal": true,
|
||||
"billing.quick_upgrade": true,
|
||||
"billing.plans_compare": true,
|
||||
"billing.checkout": true,
|
||||
"settings.profile": true,
|
||||
"settings.company": true,
|
||||
"settings.alerts": true,
|
||||
"settings.api_keys": true,
|
||||
"settings.team": true,
|
||||
"settings.team_invite": true,
|
||||
"support.center": true,
|
||||
"support.ticket_create": true,
|
||||
"support.ticket_thread": true,
|
||||
"capability.sku_cap": true,
|
||||
"capability.ai_credits": true,
|
||||
"capability.ai_processing": true,
|
||||
"capability.eprel": true,
|
||||
"capability.normalize_specs_fill": true,
|
||||
"capability.campaign_ai": true,
|
||||
"capability.email_live_send": true,
|
||||
"capability.brand_ai_apply": true,
|
||||
"capability.seo_ai_rewrite": true,
|
||||
"capability.feed_source_limit": true,
|
||||
"capability.export_feed_limit": true,
|
||||
"capability.storage_limit": true,
|
||||
"capability.api_access": true,
|
||||
"capability.byok": true
|
||||
},
|
||||
"custom": {
|
||||
"shell.navigation": true,
|
||||
"shell.command_palette": true,
|
||||
"shell.company_switcher": true,
|
||||
"shell.support_notifications": true,
|
||||
"shell.tutorial": true,
|
||||
"shell.account_menu": true,
|
||||
"shell.billing_recovery_banner": true,
|
||||
"dashboard.overview": true,
|
||||
"dashboard.stats": true,
|
||||
"dashboard.quick_links": true,
|
||||
"dashboard.recent_jobs": true,
|
||||
"dashboard.news_feed": true,
|
||||
"dashboard.activation_checklist": true,
|
||||
"dashboard.migrated_checklist": true,
|
||||
"dashboard.etl_gaps": true,
|
||||
"dashboard.store_reconnect": true,
|
||||
"dashboard.upgrade_banners": true,
|
||||
"catalog.products": true,
|
||||
"catalog.products.tab_processed": true,
|
||||
"catalog.products.tab_needs_review": true,
|
||||
"catalog.products.tab_error": true,
|
||||
"catalog.products.tab_processing": true,
|
||||
"catalog.products.tab_unprocessed": true,
|
||||
"catalog.products.process_categories": true,
|
||||
"catalog.products.process_attributes": true,
|
||||
"catalog.products.process_ai_titles": true,
|
||||
"catalog.products.process_ai_descriptions": true,
|
||||
"catalog.products.enrichment_review": true,
|
||||
"catalog.products.export_selection": true,
|
||||
"catalog.products.upgrade_prompt": true,
|
||||
"catalog.categories": true,
|
||||
"catalog.categories.title_formula": true,
|
||||
"catalog.categories.description_formula": true,
|
||||
"catalog.attributes": true,
|
||||
"catalog.attributes.bulk_import": true,
|
||||
"catalog.standard_fields": true,
|
||||
"catalog.standard_fields.groups": true,
|
||||
"catalog.structured_descriptions": true,
|
||||
"catalog.vector_categories": true,
|
||||
"feeds.list": true,
|
||||
"feeds.add_url": true,
|
||||
"feeds.add_csv": true,
|
||||
"feeds.sync": true,
|
||||
"feeds.mapping": true,
|
||||
"feeds.mapping.select_item": true,
|
||||
"feeds.mapping.map_fields": true,
|
||||
"feeds.export_feeds": true,
|
||||
"feeds.export_feeds.create": true,
|
||||
"feeds.export_feeds.generate": true,
|
||||
"feeds.uploads": true,
|
||||
"stores.hub": true,
|
||||
"stores.woocommerce": true,
|
||||
"stores.woocommerce.connection": true,
|
||||
"stores.woocommerce.categories": true,
|
||||
"stores.woocommerce.attributes": true,
|
||||
"stores.woocommerce.orders": true,
|
||||
"stores.woocommerce.reviews": true,
|
||||
"stores.woocommerce.settings": true,
|
||||
"stores.shopify": true,
|
||||
"stores.shopify.connection": true,
|
||||
"stores.shopify.orders": true,
|
||||
"stores.shopify.settings": true,
|
||||
"processing.monitor": true,
|
||||
"marketing.campaigns": true,
|
||||
"marketing.campaigns.create": true,
|
||||
"marketing.campaigns.generate_ai": true,
|
||||
"marketing.campaigns.send": true,
|
||||
"marketing.content_calendar": true,
|
||||
"marketing.brand_kit": true,
|
||||
"marketing.brand_ai_apply": true,
|
||||
"marketing.seo": true,
|
||||
"marketing.seo.template_fill": true,
|
||||
"marketing.seo.ai_rewrite": true,
|
||||
"marketing.reviews": true,
|
||||
"integrations.ai": true,
|
||||
"integrations.ai.byok": true,
|
||||
"integrations.email": true,
|
||||
"integrations.email.test": true,
|
||||
"integrations.email.blast": true,
|
||||
"billing.overview": true,
|
||||
"billing.customer_portal": true,
|
||||
"billing.quick_upgrade": true,
|
||||
"billing.plans_compare": true,
|
||||
"billing.checkout": true,
|
||||
"settings.profile": true,
|
||||
"settings.company": true,
|
||||
"settings.alerts": true,
|
||||
"settings.api_keys": true,
|
||||
"settings.team": true,
|
||||
"settings.team_invite": true,
|
||||
"support.center": true,
|
||||
"support.ticket_create": true,
|
||||
"support.ticket_thread": true,
|
||||
"capability.sku_cap": true,
|
||||
"capability.ai_credits": true,
|
||||
"capability.ai_processing": true,
|
||||
"capability.eprel": true,
|
||||
"capability.normalize_specs_fill": true,
|
||||
"capability.campaign_ai": true,
|
||||
"capability.email_live_send": true,
|
||||
"capability.brand_ai_apply": true,
|
||||
"capability.seo_ai_rewrite": true,
|
||||
"capability.feed_source_limit": true,
|
||||
"capability.export_feed_limit": true,
|
||||
"capability.storage_limit": true,
|
||||
"capability.api_access": true,
|
||||
"capability.byok": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
# 06 — Default plan feature matrix (agent 6/10)
|
||||
|
||||
Seeds / defaults for dashboard feature permissions. Coordinates with `01-feature-keys.json`, `03-permission-contract.md`, and backend `DefaultPlanFeatures` / `EnsureDefaultFeatureSeeds`.
|
||||
|
||||
Machine-readable twin: [`06-defaults-matrix.json`](./06-defaults-matrix.json).
|
||||
|
||||
---
|
||||
|
||||
## PROBLEM
|
||||
|
||||
Built-in packages must expose the features each plan allows; custom packages enable **all** registry keys; global section switches default **ON**.
|
||||
|
||||
---
|
||||
|
||||
## ASSUMPTIONS
|
||||
|
||||
1. **No `features_customized` flag.** Non-empty `plans.features` JSON = admin customization — `EnsureDefaultFeatureSeeds` / `EnsureDefaultPlans` never overwrite it. Empty `'{}'` = unset → `DefaultPlanFeatures(name, is_custom)` at resolve.
|
||||
2. **Enterprise** is public ladder with `is_custom=true` → all features ON (same as named client deals).
|
||||
3. **EPREL** (`capability.eprel`) is ON for all plans (matches `CanUseEPREL`); `docs/free-tier.md` still says paid-only — code wins.
|
||||
4. **Meters** (SKU cap, monthly credits, feed/export/storage counts) stay on `plans` columns / marketing copy; feature keys gate UI/API access only.
|
||||
5. **Global gates:** missing `platform_feature_gates` row ⇒ enabled. Seeder inserts section rows as `enabled=true` with `ON CONFLICT DO NOTHING` (preserves admin OFF).
|
||||
|
||||
---
|
||||
|
||||
## Implementation map
|
||||
|
||||
| Piece | Location |
|
||||
|-------|----------|
|
||||
| Registry keys / Free+Starter deny lists | `apps/api/internal/billing/feature_catalog.go` |
|
||||
| `DefaultPlanFeatures` / resolve | `apps/api/internal/billing/plan_features.go` |
|
||||
| Global section seed + sparse helpers | `apps/api/internal/billing/default_plan_features_seed.go` |
|
||||
| Hooked from | `EnsureDefaultPlans` → `EnsureDefaultFeatureSeeds` |
|
||||
| Schema | `apps/api/sql/schema/026_plan_features.sql` |
|
||||
|
||||
---
|
||||
|
||||
## Plan packaging (meters)
|
||||
|
||||
| Plan | SKU cap | Monthly AI credits | `is_custom` |
|
||||
|------|--------:|-------------------:|:-----------:|
|
||||
| Free | 50 | 0 | false |
|
||||
| Starter | 100 | 100 | false |
|
||||
| Plus | 400 | 400 | false |
|
||||
| Growth | 1,200 | 1,200 | false |
|
||||
| Business | 4,000 | 4,000 | false |
|
||||
| Scale | 12,000 | 12,000 | false |
|
||||
| Enterprise | unlimited | 1,000,000 | **true** |
|
||||
| Custom deals | deal | deal | **true** |
|
||||
|
||||
---
|
||||
|
||||
## Denied keys (sparse false)
|
||||
|
||||
### Free
|
||||
|
||||
- `capability.ai_processing`
|
||||
- `capability.api_access`
|
||||
- `capability.brand_ai_apply`
|
||||
- `capability.byok`
|
||||
- `capability.campaign_ai`
|
||||
- `capability.email_live_send`
|
||||
- `capability.seo_ai_rewrite`
|
||||
- `catalog.products.process_ai_descriptions`
|
||||
- `catalog.products.process_ai_titles`
|
||||
- `integrations.ai.byok`
|
||||
- `marketing.brand_ai_apply`
|
||||
- `marketing.campaigns.generate_ai`
|
||||
- `marketing.campaigns.send`
|
||||
- `marketing.seo.ai_rewrite`
|
||||
- `settings.api_keys`
|
||||
|
||||
### Starter
|
||||
|
||||
- `capability.byok`
|
||||
- `integrations.ai.byok`
|
||||
|
||||
### Growth / Business / Enterprise / Custom
|
||||
|
||||
No denials — all registry keys ON (Enterprise/Custom via `is_custom`).
|
||||
|
||||
---
|
||||
|
||||
## Full matrix
|
||||
|
||||
Legend: **ON** = allowed by `DefaultPlanFeatures` / custom-all-on; **OFF** = denied until upgrade or admin override.
|
||||
|
||||
| feature_key | Free | Starter | Growth | Business | Enterprise/Custom |
|
||||
|---|:---:|:---:|:---:|:---:|:---:|
|
||||
| `shell.navigation` | ON | ON | ON | ON | ON |
|
||||
| `shell.command_palette` | ON | ON | ON | ON | ON |
|
||||
| `shell.company_switcher` | ON | ON | ON | ON | ON |
|
||||
| `shell.support_notifications` | ON | ON | ON | ON | ON |
|
||||
| `shell.tutorial` | ON | ON | ON | ON | ON |
|
||||
| `shell.account_menu` | ON | ON | ON | ON | ON |
|
||||
| `shell.billing_recovery_banner` | ON | ON | ON | ON | ON |
|
||||
| `dashboard.overview` | ON | ON | ON | ON | ON |
|
||||
| `dashboard.stats` | ON | ON | ON | ON | ON |
|
||||
| `dashboard.quick_links` | ON | ON | ON | ON | ON |
|
||||
| `dashboard.recent_jobs` | ON | ON | ON | ON | ON |
|
||||
| `dashboard.news_feed` | ON | ON | ON | ON | ON |
|
||||
| `dashboard.activation_checklist` | ON | ON | ON | ON | ON |
|
||||
| `dashboard.migrated_checklist` | ON | ON | ON | ON | ON |
|
||||
| `dashboard.etl_gaps` | ON | ON | ON | ON | ON |
|
||||
| `dashboard.store_reconnect` | ON | ON | ON | ON | ON |
|
||||
| `dashboard.upgrade_banners` | ON | ON | ON | ON | ON |
|
||||
| `catalog.products` | ON | ON | ON | ON | ON |
|
||||
| `catalog.products.tab_processed` | ON | ON | ON | ON | ON |
|
||||
| `catalog.products.tab_needs_review` | ON | ON | ON | ON | ON |
|
||||
| `catalog.products.tab_error` | ON | ON | ON | ON | ON |
|
||||
| `catalog.products.tab_processing` | ON | ON | ON | ON | ON |
|
||||
| `catalog.products.tab_unprocessed` | ON | ON | ON | ON | ON |
|
||||
| `catalog.products.process_categories` | ON | ON | ON | ON | ON |
|
||||
| `catalog.products.process_attributes` | ON | ON | ON | ON | ON |
|
||||
| `catalog.products.process_ai_titles` | OFF | ON | ON | ON | ON |
|
||||
| `catalog.products.process_ai_descriptions` | OFF | ON | ON | ON | ON |
|
||||
| `catalog.products.enrichment_review` | ON | ON | ON | ON | ON |
|
||||
| `catalog.products.export_selection` | ON | ON | ON | ON | ON |
|
||||
| `catalog.products.upgrade_prompt` | ON | ON | ON | ON | ON |
|
||||
| `catalog.categories` | ON | ON | ON | ON | ON |
|
||||
| `catalog.categories.title_formula` | ON | ON | ON | ON | ON |
|
||||
| `catalog.categories.description_formula` | ON | ON | ON | ON | ON |
|
||||
| `catalog.attributes` | ON | ON | ON | ON | ON |
|
||||
| `catalog.attributes.bulk_import` | ON | ON | ON | ON | ON |
|
||||
| `catalog.standard_fields` | ON | ON | ON | ON | ON |
|
||||
| `catalog.standard_fields.groups` | ON | ON | ON | ON | ON |
|
||||
| `catalog.structured_descriptions` | ON | ON | ON | ON | ON |
|
||||
| `catalog.vector_categories` | ON | ON | ON | ON | ON |
|
||||
| `feeds.list` | ON | ON | ON | ON | ON |
|
||||
| `feeds.add_url` | ON | ON | ON | ON | ON |
|
||||
| `feeds.add_csv` | ON | ON | ON | ON | ON |
|
||||
| `feeds.sync` | ON | ON | ON | ON | ON |
|
||||
| `feeds.mapping` | ON | ON | ON | ON | ON |
|
||||
| `feeds.mapping.select_item` | ON | ON | ON | ON | ON |
|
||||
| `feeds.mapping.map_fields` | ON | ON | ON | ON | ON |
|
||||
| `feeds.export_feeds` | ON | ON | ON | ON | ON |
|
||||
| `feeds.export_feeds.create` | ON | ON | ON | ON | ON |
|
||||
| `feeds.export_feeds.generate` | ON | ON | ON | ON | ON |
|
||||
| `feeds.uploads` | ON | ON | ON | ON | ON |
|
||||
| `stores.hub` | ON | ON | ON | ON | ON |
|
||||
| `stores.woocommerce` | ON | ON | ON | ON | ON |
|
||||
| `stores.woocommerce.connection` | ON | ON | ON | ON | ON |
|
||||
| `stores.woocommerce.categories` | ON | ON | ON | ON | ON |
|
||||
| `stores.woocommerce.attributes` | ON | ON | ON | ON | ON |
|
||||
| `stores.woocommerce.orders` | ON | ON | ON | ON | ON |
|
||||
| `stores.woocommerce.reviews` | ON | ON | ON | ON | ON |
|
||||
| `stores.woocommerce.settings` | ON | ON | ON | ON | ON |
|
||||
| `stores.shopify` | ON | ON | ON | ON | ON |
|
||||
| `stores.shopify.connection` | ON | ON | ON | ON | ON |
|
||||
| `stores.shopify.orders` | ON | ON | ON | ON | ON |
|
||||
| `stores.shopify.settings` | ON | ON | ON | ON | ON |
|
||||
| `processing.monitor` | ON | ON | ON | ON | ON |
|
||||
| `marketing.campaigns` | ON | ON | ON | ON | ON |
|
||||
| `marketing.campaigns.create` | ON | ON | ON | ON | ON |
|
||||
| `marketing.campaigns.generate_ai` | OFF | ON | ON | ON | ON |
|
||||
| `marketing.campaigns.send` | OFF | ON | ON | ON | ON |
|
||||
| `marketing.content_calendar` | ON | ON | ON | ON | ON |
|
||||
| `marketing.brand_kit` | ON | ON | ON | ON | ON |
|
||||
| `marketing.brand_ai_apply` | OFF | ON | ON | ON | ON |
|
||||
| `marketing.seo` | ON | ON | ON | ON | ON |
|
||||
| `marketing.seo.template_fill` | ON | ON | ON | ON | ON |
|
||||
| `marketing.seo.ai_rewrite` | OFF | ON | ON | ON | ON |
|
||||
| `marketing.reviews` | ON | ON | ON | ON | ON |
|
||||
| `integrations.ai` | ON | ON | ON | ON | ON |
|
||||
| `integrations.ai.byok` | OFF | OFF | ON | ON | ON |
|
||||
| `integrations.email` | ON | ON | ON | ON | ON |
|
||||
| `integrations.email.test` | ON | ON | ON | ON | ON |
|
||||
| `integrations.email.blast` | ON | ON | ON | ON | ON |
|
||||
| `billing.overview` | ON | ON | ON | ON | ON |
|
||||
| `billing.customer_portal` | ON | ON | ON | ON | ON |
|
||||
| `billing.quick_upgrade` | ON | ON | ON | ON | ON |
|
||||
| `billing.plans_compare` | ON | ON | ON | ON | ON |
|
||||
| `billing.checkout` | ON | ON | ON | ON | ON |
|
||||
| `settings.profile` | ON | ON | ON | ON | ON |
|
||||
| `settings.company` | ON | ON | ON | ON | ON |
|
||||
| `settings.alerts` | ON | ON | ON | ON | ON |
|
||||
| `settings.api_keys` | OFF | ON | ON | ON | ON |
|
||||
| `settings.team` | ON | ON | ON | ON | ON |
|
||||
| `settings.team_invite` | ON | ON | ON | ON | ON |
|
||||
| `support.center` | ON | ON | ON | ON | ON |
|
||||
| `support.ticket_create` | ON | ON | ON | ON | ON |
|
||||
| `support.ticket_thread` | ON | ON | ON | ON | ON |
|
||||
| `capability.sku_cap` | ON | ON | ON | ON | ON |
|
||||
| `capability.ai_credits` | ON | ON | ON | ON | ON |
|
||||
| `capability.ai_processing` | OFF | ON | ON | ON | ON |
|
||||
| `capability.eprel` | ON | ON | ON | ON | ON |
|
||||
| `capability.normalize_specs_fill` | ON | ON | ON | ON | ON |
|
||||
| `capability.campaign_ai` | OFF | ON | ON | ON | ON |
|
||||
| `capability.email_live_send` | OFF | ON | ON | ON | ON |
|
||||
| `capability.brand_ai_apply` | OFF | ON | ON | ON | ON |
|
||||
| `capability.seo_ai_rewrite` | OFF | ON | ON | ON | ON |
|
||||
| `capability.feed_source_limit` | ON | ON | ON | ON | ON |
|
||||
| `capability.export_feed_limit` | ON | ON | ON | ON | ON |
|
||||
| `capability.storage_limit` | ON | ON | ON | ON | ON |
|
||||
| `capability.api_access` | OFF | ON | ON | ON | ON |
|
||||
| `capability.byok` | OFF | OFF | ON | ON | ON |
|
||||
|
||||
---
|
||||
|
||||
## Global section defaults
|
||||
|
||||
All section masters default **ON**: `shell`, `dashboard`, `catalog`, `feeds`, `stores`, `processing`, `marketing`, `integrations`, `billing`, `settings`, `support`, `capabilities`.
|
||||
|
||||
---
|
||||
|
||||
## Seeder behavior
|
||||
|
||||
1. `EnsureDefaultPlans` upserts ladder meters **without** writing `plans.features`.
|
||||
2. Then `EnsureDefaultFeatureSeeds` inserts missing section gates as enabled (`ON CONFLICT DO NOTHING`).
|
||||
3. Empty `features` left empty — resolve expands via `DefaultPlanFeatures`.
|
||||
4. Custom create/update “enable all” helpers (`EnableAllPlanFeatures`) owned with agent 7; matrix already returns all-true for `is_custom`.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit: `TestDefaultPlanFeaturesMatrix` (Free/Starter/Growth/Custom).
|
||||
- `go test ./internal/billing/ -count=1 -run DefaultPlanFeatures`
|
||||
@@ -0,0 +1,58 @@
|
||||
# 07 — Custom packages: enable-all / disable-all / global sections (agent 7/10)
|
||||
|
||||
## Behavior
|
||||
|
||||
### Custom detection (`IsCustomPackage`)
|
||||
|
||||
Matches existing product semantics — **not** `is_custom` alone:
|
||||
|
||||
| Case | Result |
|
||||
|------|--------|
|
||||
| `plans.is_custom = true` (includes seeded **Enterprise**) | custom |
|
||||
| Plan name **not** in public ladder (`IsPublicProductPlan` = false: A1, Merkur, admin deals, …) | custom |
|
||||
| Free / Starter / Growth / Business with `is_custom=false` | standard |
|
||||
|
||||
Public listing still uses `IsPublicProductPlan` / `ListPublicPlans`. Feature defaults use `IsCustomPackage`.
|
||||
|
||||
### On custom package **create** (`UpsertPlan`, `id == 0`)
|
||||
|
||||
1. Non-public names force `is_custom = true` (client-deal alignment).
|
||||
2. If the request **omits** `features`, materialize **all registry keys = true** (`AllRegistryFeatures(true)`).
|
||||
3. If the request **provides** `features` (even `{}`), that map is respected (replace semantics).
|
||||
|
||||
Updates (`id > 0`) do **not** auto-rewrite features when `features` is omitted.
|
||||
|
||||
### Per-plan helpers
|
||||
|
||||
| Service | HTTP |
|
||||
|---------|------|
|
||||
| `EnableAllPlanFeatures(planID)` | `POST /api/admin/plans/{planID}/features/enable-all` |
|
||||
| `DisableAllPlanFeatures(planID)` | `POST /api/admin/plans/{planID}/features/disable-all` |
|
||||
|
||||
Both write the full registry into `plans.features` (all `true` / all `false`).
|
||||
|
||||
### Global section helpers (all plans)
|
||||
|
||||
| Service | HTTP |
|
||||
|---------|------|
|
||||
| `EnableSectionForAllPlans(section)` / `DisableSectionForAllPlans(section)` | wrappers around `SetSectionGate` |
|
||||
| `SetSectionGate(section, enabled)` | `PUT /api/admin/feature-gates/sections/{section}` body `{ "enabled": bool }` |
|
||||
|
||||
Runtime: `effective = plan_allows ∩ global_section ∩ global_feature` (see `03-permission-contract.md`).
|
||||
|
||||
### Resolve note
|
||||
|
||||
Even with empty `features` `{}`, `PlanAllowsFeature` returns **true** for every key when `IsCustomPackage` — create-time materialization is for explicit admin UI state / enable-all parity.
|
||||
|
||||
## Files touched (agent 7)
|
||||
|
||||
- `apps/api/internal/billing/custom_package_features.go` — `IsCustomPackage`, `AllRegistryFeatures`, section wrappers, create prep
|
||||
- `apps/api/internal/billing/custom_package_features_test.go`
|
||||
- `apps/api/internal/billing/plan_features.go` — resolve/default use `IsCustomPackage`; `DisableAllPlanFeatures`
|
||||
- `apps/api/internal/billing/service.go` — `UpsertPlan` create hook
|
||||
- `apps/api/internal/billing/default_plan_features_seed.go` — sparse defaults use `IsCustomPackage`
|
||||
- `apps/api/internal/httpapi/plan_features_handlers.go` — disable-all handler
|
||||
- `apps/api/internal/httpapi/server.go` — route mount
|
||||
- `apps/api/internal/httpapi/plan_features_handlers_test.go` — mount coverage
|
||||
|
||||
Shared with agents 4/5: `EnableAllPlanFeatures`, `SetSectionGate`, migration `026_plan_features.sql`, feature catalog.
|
||||
@@ -0,0 +1,75 @@
|
||||
# 08 — Admin UI: plan features & global section switches (agent 8/10)
|
||||
|
||||
> **Superseded for the admin-roles wave:** see [`../admin-roles-support/08-permissions-ui.md`](../admin-roles-support/08-permissions-ui.md) (agent 8/20) — profiles, Legacy/A1 one-click, real APIs, shell tokens.
|
||||
|
||||
**Owns:** Admin frontend only — package feature checklist + global section masters.
|
||||
**Entry:** `/admin/billing` → **Permissions** tab (nav: Platform billing).
|
||||
**Design system:** Same cards/tabs/badges/checkboxes as existing admin billing & settings.
|
||||
|
||||
---
|
||||
|
||||
## UI entry paths
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `/admin/billing` | Platform billing admin (gate: `requirePlatformAdmin`) |
|
||||
| Tab **Permissions** | Plan feature matrix + global section controls |
|
||||
| Plans table → **Edit features** | Jumps to Permissions tab |
|
||||
| `AdminNav` → Platform billing | `apps/web/src/lib/components/AdminNav.svelte` |
|
||||
|
||||
### Source files
|
||||
|
||||
| File | Intent |
|
||||
|------|--------|
|
||||
| `apps/web/src/routes/admin/billing/+page.svelte` | Hosts Permissions tab |
|
||||
| `apps/web/src/lib/components/admin/PlanPermissionsPanel.svelte` | Checklist UI |
|
||||
| `apps/web/src/lib/admin-plan-permissions.ts` | Admin API client + defaults |
|
||||
| `apps/web/src/lib/plan-feature-catalog.ts` | Catalog from `01-feature-keys.json` |
|
||||
|
||||
---
|
||||
|
||||
## Features delivered
|
||||
|
||||
1. **Per-plan checklist** — all catalog keys grouped by section; toggle each feature.
|
||||
2. **Enable all / Disable all** — for the selected plan.
|
||||
3. **Global section controls** — enable/disable a section for all plans; optional “section + features”.
|
||||
4. **Default vs custom** — badges: Default ladder / Default · custom flag (Enterprise) / Custom deal / Client deal + `is_custom` / Standard.
|
||||
5. **Saved state** — optimistic toggle with revert on error; clear loading (“Saving…”) and success/error alerts.
|
||||
6. **API wiring** — contract paths from `03-permission-contract.md`; stub/in-session fallback when agent 5 routes are 404/501.
|
||||
|
||||
i18n: admin surfaces use plain English (no `__()` in existing admin pages) — matched that pattern.
|
||||
|
||||
---
|
||||
|
||||
## API contract used (agent 5 / agent 3)
|
||||
|
||||
| Method | Path | Use |
|
||||
|--------|------|-----|
|
||||
| `GET` | `/api/admin/plans` | List plans; read `features` / `resolved_features` when present |
|
||||
| `POST` | `/api/admin/plans` | Upsert plan including `features` override map |
|
||||
| `GET` | `/api/admin/feature-gates` | Global `sections` + `features` masters |
|
||||
| `PUT` | `/api/admin/feature-gates` | Replace global maps |
|
||||
| `PUT` | `/api/admin/feature-gates/sections/{section}` | Body `{ enabled, apply_features? }` |
|
||||
| `POST` | `/api/admin/plans/{id}/features/enable-all` | Convenience (optional; falls back to upsert all-true) |
|
||||
| `POST` | `/api/admin/plans/{id}/features/disable-all` | Convenience (optional; falls back to upsert all-false) |
|
||||
|
||||
**ASSUMPTION:** Until agent 5 mounts feature fields/gates, UI shows client-side default matrix (contract §4) and keeps toggles in-session when APIs return 404/501. Info banner explains stub mode.
|
||||
|
||||
---
|
||||
|
||||
## Manual verify
|
||||
|
||||
1. Sign in as platform admin → `/admin/billing`.
|
||||
2. Open **Permissions**.
|
||||
3. Select Free vs a custom plan — badges and default checklist differ.
|
||||
4. Toggle one feature; Enable all / Disable all.
|
||||
5. Flip a global section; try “Disable section + features”.
|
||||
6. When backend ready: reload page and confirm persisted state.
|
||||
|
||||
---
|
||||
|
||||
## Blockers / notes for siblings
|
||||
|
||||
- Agent 5 must return `features` / `resolved_features` on admin plans and mount `/api/admin/feature-gates*`.
|
||||
- Agent 4 must add `plans.features` + `platform_feature_gates`.
|
||||
- Agent 9 consumes effective features for dashboard gating (not this UI).
|
||||
@@ -0,0 +1,97 @@
|
||||
# 09 — Dashboard gating (agent 9/10)
|
||||
|
||||
User-facing dashboard UI respects resolved plan capabilities (`plan ∩ global`).
|
||||
|
||||
**Coordinates with:** `01-feature-keys.json`, `03-permission-contract.md`, `05-api.md`.
|
||||
|
||||
---
|
||||
|
||||
## ASSUMPTIONS
|
||||
|
||||
1. **Primary source:** `credits.features` on `GET /api/auth/me` (and `/api/billing/credits`), per contract §8.
|
||||
2. **Secondary fetch once:** `GET /api/billing/capabilities` from layout after `/me` (full registry + `sections` + etag). Safe if 404/unavailable.
|
||||
3. **Fail behavior when features map is absent** (pre-cutover or capabilities error without a map):
|
||||
- **Fail-open** for core Free-loop + account/billing/support keys (`CORE_ALWAYS_ON`).
|
||||
- **Fail-closed** for paid keys in `PAID_FAIL_CLOSED` (AI process, campaign AI/send, brand AI apply, SEO AI, BYOK, API keys, …) — matches agent-9 brief while keeping billing/settings reachable.
|
||||
4. **When features map is present:** enforce map + parent-prefix deny (disabled `catalog.products` hides children). Missing key → false.
|
||||
5. **Admin shell** (`/admin/*`) is out of scope.
|
||||
6. **Meters unchanged:** `can_use_ai` / wallet still compose with feature keys for AI actions.
|
||||
|
||||
---
|
||||
|
||||
## Implementation
|
||||
|
||||
| Piece | Path |
|
||||
|-------|------|
|
||||
| Helpers + route map | `apps/web/src/lib/plan-capabilities.ts` |
|
||||
| Session store (fetch once) | `apps/web/src/lib/plan-capabilities.svelte.ts` |
|
||||
| `canFeature` re-export | `apps/web/src/lib/plan-gates.ts` |
|
||||
| Types | `CreditBalance.features` / `disabled_features` / `feature_etag` |
|
||||
| Route guard | `PlanRouteGuard.svelte` in dashboard layout |
|
||||
| Upgrade CTA | `PlanUpgradePanel.svelte` / `FeatureGate.svelte` |
|
||||
| Nav filter | `Nav.svelte` primary + More |
|
||||
| Command palette | `CommandPalette.svelte` |
|
||||
| Settings tabs | `settings/+page.svelte` |
|
||||
| Section actions | Products AI, Campaign AI, SEO AI, Brand AI apply, AI BYOK |
|
||||
|
||||
Hydration: layout `/api/auth/me` → `planCapabilities.hydrateFromCredits` → `planCapabilities.refresh()`.
|
||||
|
||||
---
|
||||
|
||||
## Gated routes
|
||||
|
||||
| Path | Feature key |
|
||||
|------|-------------|
|
||||
| `/dashboard` | `dashboard.overview` |
|
||||
| `/products` | `catalog.products` |
|
||||
| `/feeds` | `feeds.list` |
|
||||
| `/feeds/[feedId]/mapping` | `feeds.mapping` |
|
||||
| `/export-feeds` | `feeds.export_feeds` |
|
||||
| `/files` | `feeds.uploads` |
|
||||
| `/stores` | `stores.hub` |
|
||||
| `/woocommerce` | `stores.woocommerce` |
|
||||
| `/woocommerce?tab=reviews` | `marketing.reviews` |
|
||||
| `/stores/shopify`, `/shopify` | `stores.shopify` |
|
||||
| `/processing`, `/tasks` | `processing.monitor` |
|
||||
| `/categories` | `catalog.categories` |
|
||||
| `/categories/[id]/title-formula` | `catalog.categories.title_formula` |
|
||||
| `/categories/[id]/description-formula` | `catalog.categories.description_formula` |
|
||||
| `/attributes` | `catalog.attributes` |
|
||||
| `/standard-fields` | `catalog.standard_fields` |
|
||||
| `/structured-descriptions` | `catalog.structured_descriptions` |
|
||||
| `/vector-categories` | `catalog.vector_categories` |
|
||||
| `/campaigns`, `/campaigns/new`, `/campaigns/[id]` | `marketing.campaigns` |
|
||||
| `/marketing/calendar` | `marketing.content_calendar` |
|
||||
| `/brand` | `marketing.brand_kit` |
|
||||
| `/seo` | `marketing.seo` |
|
||||
| `/integrations/ai` | `integrations.ai` |
|
||||
| `/integrations/email` | `integrations.email` |
|
||||
| `/billing` | `billing.overview` |
|
||||
| `/plans` | `billing.plans_compare` |
|
||||
| `/settings` | `settings.profile` (+ tab keys) |
|
||||
| `/settings?tab=api-keys` | `settings.api_keys` |
|
||||
| `/support` | `support.center` |
|
||||
| `/support/new` | `support.ticket_create` |
|
||||
| `/support/[ticketId]` | `support.ticket_thread` |
|
||||
|
||||
Deep-link to a disabled route → in-place upgrade panel (not hard redirect).
|
||||
|
||||
---
|
||||
|
||||
## Section gates (shared pages)
|
||||
|
||||
| Surface | Keys |
|
||||
|---------|------|
|
||||
| Settings tabs | `settings.profile` / `company` / `alerts` / `api_keys` / `team` |
|
||||
| Products AI actions | `catalog.products.process_ai_*` + `capability.ai_processing` |
|
||||
| Campaign AI / send | `marketing.campaigns.generate_ai` + `capability.campaign_ai` |
|
||||
| SEO AI rewrite | `marketing.seo.ai_rewrite` + `capability.seo_ai_rewrite` |
|
||||
| Brand AI apply banner | `marketing.brand_ai_apply` + `capability.brand_ai_apply` |
|
||||
| AI BYOK modes | `integrations.ai.byok` + `capability.byok` |
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
- `npm run check` in `apps/web` (svelte-check)
|
||||
- Manual: Free user — API keys tab hidden/upgraded; AI process locked; billing/settings reachable when capabilities 404
|
||||
@@ -0,0 +1,80 @@
|
||||
# 10 — Backend enforcement + integration (agent 10/10)
|
||||
|
||||
## Status
|
||||
|
||||
**Enforcement wired** for the highest-risk mutating surfaces. Focused resolve/default/custom/global tests added. Docs index: [README.md](./README.md).
|
||||
|
||||
Claim gate: run `verify` / `finish_check` after tests in this session.
|
||||
|
||||
---
|
||||
|
||||
## What was enforced
|
||||
|
||||
| Surface | Keys checked | Where |
|
||||
|---------|--------------|--------|
|
||||
| Processing `StartJob` | `FeatureKeysForProcessingType` (AI / EPREL / normalize / campaign AI) | `processing/pipeline.go` → `AssertProcessingFeatures` |
|
||||
| Campaign AI generate | `capability.campaign_ai`, `marketing.campaigns.generate_ai` | `campaigns/generate_send.go` |
|
||||
| Campaign live send | `capability.email_live_send`, `marketing.campaigns.send` | `campaigns/generate_send.go` (non–dry-run) |
|
||||
| SEO AI apply | `capability.seo_ai_rewrite`, `marketing.seo.ai_rewrite` | `seo/service.go` |
|
||||
| API key create | `settings.api_keys`, `capability.api_access` | `httpapi/apikey_handlers.go` |
|
||||
| Email blast send | `capability.email_live_send`, `integrations.email.blast` | `httpapi/email_handlers.go` |
|
||||
| Brand AI inject | `capability.brand_ai_apply`, `marketing.brand_ai_apply` | `AIBrandApplyAllowed` |
|
||||
|
||||
HTTP: `writePlanGate` + `RequireFeature` middleware (`httpapi/plan_gate.go`). Processing / SEO / campaigns / v1 process map `ErrFeatureDisabled` → **402** with `code=plan_gate`, `error=feature_disabled`, optional `feature`.
|
||||
|
||||
Helpers: `FeatureKeyFromError`, `AssertFeatures`, `FeatureKeysForProcessingType` in `billing/feature_enforcement.go`.
|
||||
|
||||
---
|
||||
|
||||
## Tests added
|
||||
|
||||
`apps/api/internal/billing/feature_enforcement_test.go`:
|
||||
|
||||
- Free defaults deny AI / API keys
|
||||
- Custom / Enterprise enable-all
|
||||
- Global section disable-all (marketing off)
|
||||
- Custom override false wins
|
||||
- Capability resolution Starter vs Growth BYOK
|
||||
- Feature key extraction + processing-type map
|
||||
|
||||
Existing: `features_api_test.go`, `custom_package_features_test.go`, `plan_features_handlers_test.go`.
|
||||
|
||||
---
|
||||
|
||||
## Glue confirmed from siblings
|
||||
|
||||
- Migration `026_plan_features.sql`
|
||||
- Routes mounted in `server.go` (capabilities, plan features, feature-gates, enable/disable-all)
|
||||
- `EnsureDefaultFeatureSeeds` called from `EnsureDefaultPlans`
|
||||
- CreditsOverview /auth/me embed effective features
|
||||
- Admin Permissions UI + dashboard gating docs present
|
||||
|
||||
---
|
||||
|
||||
## Remaining blockers
|
||||
|
||||
1. **Migrate required** — writes to `plans.features` / `platform_feature_gates` need `026` applied (`.\scripts\migrate.ps1`). Resolve falls back without column but admin PUT fails closed with a clear error.
|
||||
2. **Not every route is AssertFeature'd** — list/read endpoints and many store/feed mutations are still UI-gated only. Extend using `RequireFeature` or `AssertFeature` when a section is product-critical.
|
||||
3. **OpenAPI** — session fragment lives in `05-openapi-fragment.yaml`; public `/api/v1/openapi.yaml` intentionally unchanged. Optional: merge fragment into internal docs.
|
||||
4. **Numeric marketing limits** (feed source count, export count, storage GB) remain documentation-only — not feature keys with counters.
|
||||
5. **Agent 9 UI** — confirm live that nav hides after capabilities load; stub mode in admin UI should clear once APIs return 200.
|
||||
6. **EPREL** — `capability.eprel` is ON for Free in the matrix; product docs historically disagreed with code (`CanUseEPREL` always true). Align product copy separately.
|
||||
|
||||
---
|
||||
|
||||
## Files touched (agent 10)
|
||||
|
||||
| File | Intent |
|
||||
|------|--------|
|
||||
| `billing/feature_enforcement.go` | Assert helpers + processing key map |
|
||||
| `billing/feature_enforcement_test.go` | Defaults / custom / global / resolve tests |
|
||||
| `billing/service.go` | Brand AI feature check |
|
||||
| `processing/pipeline.go` | Assert before meter gate |
|
||||
| `campaigns/generate_send.go` | Generate AI + live send |
|
||||
| `seo/service.go` | SEO AI apply |
|
||||
| `httpapi/plan_gate.go` | 402 writer + `RequireFeature` |
|
||||
| `httpapi/processing_handlers.go` / `v1_process_handlers.go` | Feature 402 |
|
||||
| `httpapi/seo_handlers.go` / `campaigns_handlers.go` | Feature 402 |
|
||||
| `httpapi/apikey_handlers.go` / `email_handlers.go` | Create/send gates |
|
||||
| `docs/plan-permissions/README.md` | System index |
|
||||
| `docs/plan-permissions/10-enforcement.md` | This note |
|
||||
@@ -0,0 +1,124 @@
|
||||
# Plan permissions — system index
|
||||
|
||||
Per-plan dashboard feature toggles, global section master switches, defaults for the public ladder, custom enable-all, admin UI, dashboard gating, and **API enforcement**.
|
||||
|
||||
**Runtime rule:**
|
||||
|
||||
```
|
||||
effective(feature) = plan_allows(feature)
|
||||
AND global_section_enabled(section(feature))
|
||||
AND global_feature_enabled(feature)
|
||||
```
|
||||
|
||||
Meters (`max_products`, credit wallet, `can_use_ai`) stay as today and compose with feature keys.
|
||||
|
||||
---
|
||||
|
||||
## Doc map (agents 1–10)
|
||||
|
||||
| Doc | Owner | What it covers |
|
||||
|-----|-------|----------------|
|
||||
| [01-dashboard-feature-catalog.md](./01-dashboard-feature-catalog.md) | 1 | Every tab/section + suggested defaults |
|
||||
| [01-feature-keys.json](./01-feature-keys.json) | 1 | Machine-readable registry |
|
||||
| [02-plans-permissions-current.md](./02-plans-permissions-current.md) | 2 | How plans/entitlements worked before |
|
||||
| [02-extension-points.json](./02-extension-points.json) | 2 | Files/symbols to extend |
|
||||
| [03-permission-contract.md](./03-permission-contract.md) / [`.json`](./03-permission-contract.json) | 3 | Normative contract |
|
||||
| [04-backend-model.md](./04-backend-model.md) | 4 | Schema + `billing` service |
|
||||
| [05-api.md](./05-api.md) / [05-openapi-fragment.yaml](./05-openapi-fragment.yaml) | 5 | Admin + tenant HTTP surface |
|
||||
| [06-defaults-matrix.md](./06-defaults-matrix.md) / [`.json`](./06-defaults-matrix.json) | 6 | Default matrix per plan |
|
||||
| [07-custom-enable-all.md](./07-custom-enable-all.md) | 7 | Custom create + enable/disable-all |
|
||||
| [08-admin-ui.md](./08-admin-ui.md) | 8 | `/admin/billing` → Permissions tab |
|
||||
| [09-dashboard-gating.md](./09-dashboard-gating.md) | 9 | User nav/route/action gating |
|
||||
| [10-enforcement.md](./10-enforcement.md) | 10 | API AssertFeature wiring + tests |
|
||||
|
||||
---
|
||||
|
||||
## How an admin configures it
|
||||
|
||||
1. Sign in as **platform admin** → **Platform billing** (`/admin/billing`).
|
||||
2. Open the **Permissions** tab (`PlanPermissionsPanel`).
|
||||
3. **Per plan:** select a package → toggle feature keys → **Enable all** / **Disable all**.
|
||||
4. **Global:** flip a section master (e.g. disable `marketing` for **all** plans) via section controls / `PUT /api/admin/feature-gates/sections/{section}`.
|
||||
5. Tenant UI reads `features` from `/api/auth/me` (credits) and `/api/billing/capabilities`.
|
||||
6. Sensitive API mutations call `billing.AssertFeature` / `AssertProcessingFeatures` and return **402** `{ "error":"feature_disabled", "code":"plan_gate", "feature":"…", "upgrade_url":"/pricing" }`.
|
||||
|
||||
### API cheat sheet
|
||||
|
||||
| Action | Endpoint |
|
||||
|--------|----------|
|
||||
| Tenant capabilities | `GET /api/billing/capabilities` |
|
||||
| Credits + features | `GET /api/billing/credits` / `GET /api/auth/me` |
|
||||
| Plan feature editor | `GET\|PUT /api/admin/plans/{id}/features` |
|
||||
| Enable / disable all on a plan | `POST …/features/enable-all` \| `disable-all` |
|
||||
| Global gates | `GET\|PUT /api/admin/feature-gates` |
|
||||
| Section for all plans | `PUT /api/admin/feature-gates/sections/{section}` |
|
||||
|
||||
Auth: session + company for tenant; session + **platform admin** for admin mutations. CSRF on dashboard POSTs.
|
||||
|
||||
---
|
||||
|
||||
## Code map
|
||||
|
||||
| Layer | Path |
|
||||
|-------|------|
|
||||
| Migration | `apps/api/sql/schema/026_plan_features.sql` |
|
||||
| Catalog | `apps/api/internal/billing/feature_catalog.go` |
|
||||
| Resolve + CRUD | `apps/api/internal/billing/plan_features.go`, `features_api.go` |
|
||||
| Custom enable-all | `apps/api/internal/billing/custom_package_features.go` |
|
||||
| Seed | `EnsureDefaultFeatureSeeds` (from `EnsureDefaultPlans`) |
|
||||
| Enforcement | `apps/api/internal/billing/feature_enforcement.go` + call sites |
|
||||
| HTTP | `apps/api/internal/httpapi/plan_features_handlers.go`, `plan_gate.go` |
|
||||
| Admin UI | `PlanPermissionsPanel.svelte`, `admin-plan-permissions.ts` |
|
||||
| Dashboard gate | `plan-capabilities.ts`, `PlanRouteGuard.svelte`, `Nav.svelte` |
|
||||
|
||||
---
|
||||
|
||||
## Defaults (summary)
|
||||
|
||||
| Package | Behavior |
|
||||
|---------|----------|
|
||||
| **Free** | Core catalog/feeds/stores ON; AI / API keys / live email / campaign AI / SEO AI / BYOK OFF |
|
||||
| **Starter** | AI + API keys ON; BYOK OFF |
|
||||
| **Growth / Business** | All registry keys ON |
|
||||
| **Custom** (`is_custom` or non-public name, incl. Enterprise) | All ON (create materializes full true map) |
|
||||
| **Global gates** | Missing row ⇒ enabled |
|
||||
|
||||
---
|
||||
|
||||
## Verify locally
|
||||
|
||||
```powershell
|
||||
.\scripts\migrate.ps1 # applies 026_plan_features.sql
|
||||
cd apps\api
|
||||
go test ./internal/billing/ -count=1 -run "DefaultPlanFeatures|ResolveEffective|FeatureKey|Custom|Assert"
|
||||
go test ./internal/httpapi/ -count=1 -run "PlanFeatures|PlanGate"
|
||||
```
|
||||
|
||||
Manual:
|
||||
|
||||
1. Platform admin → Permissions → disable `marketing` globally → tenant `GET /api/billing/capabilities` lists marketing keys in `disabled_features`.
|
||||
2. Free company → `POST /api/processing/jobs` with `processing_type=title` → 402 `feature_disabled`.
|
||||
3. Create custom plan (omit features) → all `resolved_features` true; enable-all / disable-all round-trip.
|
||||
|
||||
---
|
||||
|
||||
## Post-rollout verify (2026-08-05)
|
||||
|
||||
| Check | Result |
|
||||
|-------|--------|
|
||||
| Migrate path (Windows/Laragon) | `.\scripts\migrate.ps1` (Makefile `migrate` needs bash) |
|
||||
| `026_plan_features.sql` | Applied — goose version **26** (BOM stripped so goose could parse the file) |
|
||||
| `go test ./internal/billing/ -count=1` | **PASS** (`ok` ~0.1s) |
|
||||
| Key files present | Admin `PlanPermissionsPanel.svelte` + `admin-plan-permissions.ts`; dashboard `plan-capabilities.ts`, `PlanRouteGuard.svelte`, `Nav.svelte`; API `plan_features_handlers.go`, `plan_gate.go`, `features_api.go` |
|
||||
|
||||
### Remaining blockers (from agent 10)
|
||||
|
||||
1. Not every route is `AssertFeature`'d — list/read and many store/feed mutations are still UI-gated only.
|
||||
2. OpenAPI — session fragment in `05-openapi-fragment.yaml`; public `/api/v1/openapi.yaml` unchanged (optional merge).
|
||||
3. Numeric marketing limits remain documentation-only (no feature-key counters).
|
||||
4. Agent 9 UI — confirm live that nav hides after capabilities load; clear admin stub mode once APIs return 200.
|
||||
5. EPREL — `capability.eprel` ON for Free in matrix vs historical product copy; align separately.
|
||||
|
||||
## Remaining blockers
|
||||
|
||||
See [10-enforcement.md](./10-enforcement.md#remaining-blockers).
|
||||
Reference in New Issue
Block a user