Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
229 lines
7.3 KiB
JSON
229 lines
7.3 KiB
JSON
{
|
|
"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"
|
|
]
|
|
}
|