@nextsparkjs/ai-workflow 0.1.0-beta.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/claude/_docs/workflows-optimizations.md +359 -0
- package/claude/agents/api-tester.md +634 -0
- package/claude/agents/architecture-supervisor.md +1351 -0
- package/claude/agents/backend-developer.md +997 -0
- package/claude/agents/backend-validator.md +417 -0
- package/claude/agents/bdd-docs-writer.md +737 -0
- package/claude/agents/block-developer.md +677 -0
- package/claude/agents/code-reviewer.md +1432 -0
- package/claude/agents/db-developer.md +721 -0
- package/claude/agents/db-validator.md +407 -0
- package/claude/agents/demo-video-generator.md +493 -0
- package/claude/agents/documentation-writer.md +1268 -0
- package/claude/agents/frontend-developer.md +1234 -0
- package/claude/agents/frontend-validator.md +777 -0
- package/claude/agents/functional-validator.md +630 -0
- package/claude/agents/mock-analyst.md +387 -0
- package/claude/agents/product-manager.md +963 -0
- package/claude/agents/qa-automation.md +1762 -0
- package/claude/agents/release-manager.md +634 -0
- package/claude/agents/selectors-translator.md +262 -0
- package/claude/agents/unit-test-writer.md +785 -0
- package/claude/agents/visual-comparator.md +329 -0
- package/claude/agents/workflow-maintainer.md +352 -0
- package/claude/commands/do/README.md +88 -0
- package/claude/commands/do/create-api.md +64 -0
- package/claude/commands/do/create-entity.md +66 -0
- package/claude/commands/do/create-migration.md +64 -0
- package/claude/commands/do/create-plugin.md +56 -0
- package/claude/commands/do/create-theme.md +70 -0
- package/claude/commands/do/mock-data.md +67 -0
- package/claude/commands/do/reset-db.md +71 -0
- package/claude/commands/do/setup-scheduled-action.md +75 -0
- package/claude/commands/do/sync-code-review.md +117 -0
- package/claude/commands/do/update-selectors.md +112 -0
- package/claude/commands/do/use-skills.md +90 -0
- package/claude/commands/do/validate-blocks.md +69 -0
- package/claude/commands/how-to/README.md +261 -0
- package/claude/commands/how-to/add-metadata.md +692 -0
- package/claude/commands/how-to/add-taxonomies.md +806 -0
- package/claude/commands/how-to/add-translations.md +571 -0
- package/claude/commands/how-to/create-api.md +577 -0
- package/claude/commands/how-to/create-block.md +575 -0
- package/claude/commands/how-to/create-child-entities.md +771 -0
- package/claude/commands/how-to/create-entity.md +597 -0
- package/claude/commands/how-to/create-migrations.md +605 -0
- package/claude/commands/how-to/create-plugin.md +654 -0
- package/claude/commands/how-to/customize-app.md +481 -0
- package/claude/commands/how-to/customize-dashboard.md +553 -0
- package/claude/commands/how-to/customize-theme.md +438 -0
- package/claude/commands/how-to/define-features-flows.md +632 -0
- package/claude/commands/how-to/deploy.md +507 -0
- package/claude/commands/how-to/handle-file-uploads.md +746 -0
- package/claude/commands/how-to/implement-search.md +1001 -0
- package/claude/commands/how-to/install-plugins.md +352 -0
- package/claude/commands/how-to/manage-test-coverage.md +984 -0
- package/claude/commands/how-to/run-tests.md +400 -0
- package/claude/commands/how-to/set-app-languages.md +601 -0
- package/claude/commands/how-to/set-plans-and-permissions.md +575 -0
- package/claude/commands/how-to/set-scheduled-actions.md +527 -0
- package/claude/commands/how-to/set-user-roles-and-permissions.md +550 -0
- package/claude/commands/how-to/setup-authentication.md +388 -0
- package/claude/commands/how-to/setup-claude-code.md +440 -0
- package/claude/commands/how-to/setup-database.md +274 -0
- package/claude/commands/how-to/setup-email-providers.md +598 -0
- package/claude/commands/how-to/setup-mobile-dev.md +627 -0
- package/claude/commands/how-to/start.md +500 -0
- package/claude/commands/how-to/use-devtools.md +639 -0
- package/claude/commands/how-to/use-superadmin.md +622 -0
- package/claude/commands/session/README.md +193 -0
- package/claude/commands/session/block-create.md +190 -0
- package/claude/commands/session/block-list.md +203 -0
- package/claude/commands/session/block-update.md +192 -0
- package/claude/commands/session/block-validate.md +218 -0
- package/claude/commands/session/changelog.md +115 -0
- package/claude/commands/session/close.md +225 -0
- package/claude/commands/session/commit.md +174 -0
- package/claude/commands/session/db-entity.md +206 -0
- package/claude/commands/session/db-fix.md +212 -0
- package/claude/commands/session/db-sample.md +206 -0
- package/claude/commands/session/demo.md +178 -0
- package/claude/commands/session/doc-bdd.md +207 -0
- package/claude/commands/session/doc-feature.md +218 -0
- package/claude/commands/session/doc-read.md +225 -0
- package/claude/commands/session/execute.md +204 -0
- package/claude/commands/session/explain.md +202 -0
- package/claude/commands/session/fix-bug.md +210 -0
- package/claude/commands/session/fix-build.md +182 -0
- package/claude/commands/session/fix-test.md +189 -0
- package/claude/commands/session/pending.md +232 -0
- package/claude/commands/session/refine.md +188 -0
- package/claude/commands/session/resume.md +192 -0
- package/claude/commands/session/review.md +192 -0
- package/claude/commands/session/scope-change.md +181 -0
- package/claude/commands/session/start-blocks.md +347 -0
- package/claude/commands/session/start.md +604 -0
- package/claude/commands/session/status.md +169 -0
- package/claude/commands/session/test-fix.md +221 -0
- package/claude/commands/session/test-run.md +203 -0
- package/claude/commands/session/test-write.md +242 -0
- package/claude/commands/session/validate.md +162 -0
- package/claude/config/context.json +40 -0
- package/claude/config/github.json +69 -0
- package/claude/config/github.schema.json +106 -0
- package/claude/config/team.json +46 -0
- package/claude/config/team.schema.json +106 -0
- package/claude/config/workspace.json +43 -0
- package/claude/config/workspace.schema.json +75 -0
- package/claude/skills/README.md +228 -0
- package/claude/skills/accessibility/SKILL.md +573 -0
- package/claude/skills/api-bypass-layers/SKILL.md +550 -0
- package/claude/skills/asana-integration/SKILL.md +499 -0
- package/claude/skills/better-auth/SKILL.md +666 -0
- package/claude/skills/billing-subscriptions/SKILL.md +660 -0
- package/claude/skills/block-decision-matrix/SKILL.md +359 -0
- package/claude/skills/clickup-integration/SKILL.md +434 -0
- package/claude/skills/core-theme-responsibilities/SKILL.md +485 -0
- package/claude/skills/create-plugin/SKILL.md +425 -0
- package/claude/skills/create-theme/SKILL.md +331 -0
- package/claude/skills/cypress-api/SKILL.md +511 -0
- package/claude/skills/cypress-api/scripts/generate-api-controller.py +329 -0
- package/claude/skills/cypress-api/scripts/generate-api-test.py +930 -0
- package/claude/skills/cypress-e2e/SKILL.md +526 -0
- package/claude/skills/cypress-e2e/scripts/extract-selectors.py +383 -0
- package/claude/skills/cypress-e2e/scripts/generate-uat-test.py +788 -0
- package/claude/skills/cypress-selectors/SKILL.md +309 -0
- package/claude/skills/cypress-selectors/scripts/extract-missing.py +243 -0
- package/claude/skills/cypress-selectors/scripts/generate-block-selectors.py +283 -0
- package/claude/skills/cypress-selectors/scripts/validate-selectors.py +145 -0
- package/claude/skills/database-migrations/SKILL.md +335 -0
- package/claude/skills/database-migrations/scripts/generate-sample-data.py +284 -0
- package/claude/skills/database-migrations/scripts/validate-migration.py +323 -0
- package/claude/skills/design-system/SKILL.md +682 -0
- package/claude/skills/documentation/SKILL.md +540 -0
- package/claude/skills/entity-api/SKILL.md +482 -0
- package/claude/skills/entity-system/SKILL.md +635 -0
- package/claude/skills/entity-system/scripts/generate-child-migration.py +298 -0
- package/claude/skills/entity-system/scripts/generate-metas-migration.py +233 -0
- package/claude/skills/entity-system/scripts/generate-migration.py +382 -0
- package/claude/skills/entity-system/scripts/generate-sample-data.py +418 -0
- package/claude/skills/entity-system/scripts/scaffold-entity.py +661 -0
- package/claude/skills/github/SKILL.md +467 -0
- package/claude/skills/i18n-nextintl/SKILL.md +302 -0
- package/claude/skills/i18n-nextintl/scripts/add-translation.py +243 -0
- package/claude/skills/i18n-nextintl/scripts/extract-hardcoded.py +246 -0
- package/claude/skills/i18n-nextintl/scripts/validate-translations.py +260 -0
- package/claude/skills/impact-analysis/SKILL.md +203 -0
- package/claude/skills/jest-unit/SKILL.md +306 -0
- package/claude/skills/jest-unit/references/component-testing.md +371 -0
- package/claude/skills/jest-unit/references/mocking-patterns.md +380 -0
- package/claude/skills/jest-unit/references/service-hook-testing.md +454 -0
- package/claude/skills/jira-integration/SKILL.md +539 -0
- package/claude/skills/media-library/SKILL.md +743 -0
- package/claude/skills/mock-analysis/SKILL.md +276 -0
- package/claude/skills/monorepo-architecture/SKILL.md +162 -0
- package/claude/skills/nextjs-api-development/SKILL.md +364 -0
- package/claude/skills/nextjs-api-development/scripts/generate-crud-tests.py +456 -0
- package/claude/skills/nextjs-api-development/scripts/scaffold-endpoint.py +481 -0
- package/claude/skills/nextjs-api-development/scripts/validate-api.py +283 -0
- package/claude/skills/notion-integration/SKILL.md +641 -0
- package/claude/skills/npm-development-workflow/SKILL.md +480 -0
- package/claude/skills/page-builder-blocks/SKILL.md +530 -0
- package/claude/skills/page-builder-blocks/scripts/scaffold-block.py +444 -0
- package/claude/skills/permissions-system/SKILL.md +619 -0
- package/claude/skills/plugins/SKILL.md +340 -0
- package/claude/skills/plugins/references/plugin-templates.md +414 -0
- package/claude/skills/plugins/references/plugin-testing.md +353 -0
- package/claude/skills/plugins/references/plugin-types.md +198 -0
- package/claude/skills/plugins/scripts/scaffold-plugin.py +443 -0
- package/claude/skills/pom-patterns/SKILL.md +452 -0
- package/claude/skills/pom-patterns/scripts/generate-pom.py +392 -0
- package/claude/skills/rate-limiting/SKILL.md +342 -0
- package/claude/skills/react-best-practices/AGENTS.md +2410 -0
- package/claude/skills/react-best-practices/README.md +123 -0
- package/claude/skills/react-best-practices/SKILL.md +125 -0
- package/claude/skills/react-best-practices/metadata.json +15 -0
- package/claude/skills/react-best-practices/rules/_sections.md +46 -0
- package/claude/skills/react-best-practices/rules/_template.md +28 -0
- package/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/claude/skills/react-best-practices/rules/advanced-use-latest.md +49 -0
- package/claude/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/claude/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/claude/skills/react-best-practices/rules/async-dependencies.md +36 -0
- package/claude/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/claude/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/claude/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/claude/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/claude/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/claude/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/claude/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/claude/skills/react-best-practices/rules/js-batch-dom-css.md +82 -0
- package/claude/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/claude/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/claude/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/claude/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/claude/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/claude/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/claude/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/claude/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/claude/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/claude/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/claude/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/claude/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/claude/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/claude/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/claude/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/claude/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/claude/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/claude/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/claude/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/claude/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/claude/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/claude/skills/react-best-practices/rules/server-cache-react.md +76 -0
- package/claude/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/claude/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/claude/skills/react-patterns/SKILL.md +688 -0
- package/claude/skills/registry-system/SKILL.md +331 -0
- package/claude/skills/scheduled-actions/SKILL.md +671 -0
- package/claude/skills/scope-enforcement/SKILL.md +542 -0
- package/claude/skills/scope-enforcement/scripts/validate-scope.py +357 -0
- package/claude/skills/server-actions/SKILL.md +493 -0
- package/claude/skills/service-layer/SKILL.md +587 -0
- package/claude/skills/session-management/SKILL.md +266 -0
- package/claude/skills/session-management/scripts/create-session.py +166 -0
- package/claude/skills/session-management/scripts/iteration-close.sh +105 -0
- package/claude/skills/session-management/scripts/iteration-init.sh +180 -0
- package/claude/skills/session-management/scripts/session-archive.sh +87 -0
- package/claude/skills/session-management/scripts/session-close.sh +133 -0
- package/claude/skills/session-management/scripts/session-init.sh +225 -0
- package/claude/skills/session-management/scripts/session-list.sh +163 -0
- package/claude/skills/session-management/scripts/split-plan.sh +116 -0
- package/claude/skills/shadcn-components/SKILL.md +586 -0
- package/claude/skills/shadcn-theming/SKILL.md +446 -0
- package/claude/skills/suspense-loading/SKILL.md +280 -0
- package/claude/skills/tailwind-theming/SKILL.md +507 -0
- package/claude/skills/tanstack-query/SKILL.md +608 -0
- package/claude/skills/test-coverage/SKILL.md +239 -0
- package/claude/skills/web-design-guidelines/SKILL.md +39 -0
- package/claude/skills/zod-validation/SKILL.md +537 -0
- package/claude/templates/blocks/progress.md +86 -0
- package/claude/templates/iteration/changes.md +61 -0
- package/claude/templates/iteration/progress.md +55 -0
- package/claude/templates/log.md +31 -0
- package/claude/templates/story/context.md +77 -0
- package/claude/templates/story/pendings.md +37 -0
- package/claude/templates/story/plan.md +299 -0
- package/claude/templates/story/requirements.md +109 -0
- package/claude/templates/story/scope.json +10 -0
- package/claude/templates/story/tests.md +91 -0
- package/claude/templates/task/progress.md +58 -0
- package/claude/templates/task/requirements.md +54 -0
- package/claude/workflows/README.md +154 -0
- package/claude/workflows/blocks.md +614 -0
- package/claude/workflows/story.md +1207 -0
- package/claude/workflows/task.md +927 -0
- package/claude/workflows/tweak.md +527 -0
- package/cursor/.gitkeep +0 -0
- package/package.json +35 -0
- package/scripts/postinstall.mjs +198 -0
- package/scripts/setup.mjs +282 -0
- package/scripts/sync.mjs +209 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: billing-subscriptions
|
|
3
|
+
description: |
|
|
4
|
+
Billing and subscription system for this Next.js application.
|
|
5
|
+
Covers Stripe integration, plans configuration, checkout flow, customer portal, webhooks, and usage tracking.
|
|
6
|
+
Use this skill when implementing billing features or working with subscription management.
|
|
7
|
+
allowed-tools: Read, Glob, Grep
|
|
8
|
+
version: 1.0.0
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Billing & Subscriptions Skill
|
|
12
|
+
|
|
13
|
+
Patterns for working with the Stripe-based billing system, subscription management, and usage tracking.
|
|
14
|
+
|
|
15
|
+
## Architecture Overview
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
BILLING ARCHITECTURE:
|
|
19
|
+
|
|
20
|
+
Configuration Layer:
|
|
21
|
+
contents/themes/{theme}/config/billing.config.ts
|
|
22
|
+
├── provider: 'stripe' | 'paddle' | 'lemonsqueezy'
|
|
23
|
+
├── currency: 'usd' | 'eur' | ...
|
|
24
|
+
├── defaultPlan: 'free'
|
|
25
|
+
├── features: { featureSlug: FeatureDefinition }
|
|
26
|
+
├── limits: { limitSlug: LimitDefinition }
|
|
27
|
+
├── plans: PlanDefinition[]
|
|
28
|
+
└── actionMappings: ActionMappings
|
|
29
|
+
|
|
30
|
+
Core Library:
|
|
31
|
+
core/lib/billing/
|
|
32
|
+
├── config-types.ts # BillingConfig interface
|
|
33
|
+
├── types.ts # PlanType, SubscriptionStatus, etc.
|
|
34
|
+
├── schema.ts # Zod validation schemas
|
|
35
|
+
├── gateways/
|
|
36
|
+
│ └── stripe.ts # Stripe SDK wrapper
|
|
37
|
+
├── queries.ts # Database queries
|
|
38
|
+
├── enforcement.ts # Limit/feature enforcement
|
|
39
|
+
├── helpers.ts # Utility functions
|
|
40
|
+
└── jobs.ts # Background jobs
|
|
41
|
+
|
|
42
|
+
Services Layer:
|
|
43
|
+
core/lib/services/
|
|
44
|
+
├── subscription.service.ts # Subscription CRUD
|
|
45
|
+
├── plan.service.ts # Plan management
|
|
46
|
+
└── usage.service.ts # Usage tracking
|
|
47
|
+
|
|
48
|
+
API Endpoints:
|
|
49
|
+
app/api/v1/billing/
|
|
50
|
+
├── checkout/route.ts # Create Stripe checkout session
|
|
51
|
+
├── portal/route.ts # Customer portal access
|
|
52
|
+
├── plans/route.ts # List available plans
|
|
53
|
+
├── cancel/route.ts # Cancel subscription
|
|
54
|
+
├── change-plan/route.ts # Upgrade/downgrade
|
|
55
|
+
├── check-action/route.ts # Permission check
|
|
56
|
+
└── webhooks/stripe/route.ts # Stripe webhooks
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## When to Use This Skill
|
|
60
|
+
|
|
61
|
+
- Implementing billing features
|
|
62
|
+
- Working with subscription management
|
|
63
|
+
- Configuring plans and features
|
|
64
|
+
- Setting up Stripe webhooks
|
|
65
|
+
- Implementing usage limits
|
|
66
|
+
- Testing billing flows
|
|
67
|
+
|
|
68
|
+
## Three-Layer Permission Model
|
|
69
|
+
|
|
70
|
+
The billing system uses a three-layer model:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
RESULT = Permission (RBAC) AND Feature (Plan) AND Quota (Limits)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Layer 1: RBAC Permissions
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Team role-based access
|
|
80
|
+
actionMappings: {
|
|
81
|
+
permissions: {
|
|
82
|
+
'team.billing.manage': 'team.billing.manage',
|
|
83
|
+
'team.settings.edit': 'team.settings.edit',
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Layer 2: Plan Features
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// Feature access based on subscription plan
|
|
92
|
+
features: {
|
|
93
|
+
advanced_analytics: {
|
|
94
|
+
name: 'billing.features.advanced_analytics',
|
|
95
|
+
description: 'billing.features.advanced_analytics_description',
|
|
96
|
+
},
|
|
97
|
+
api_access: {
|
|
98
|
+
name: 'billing.features.api_access',
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Feature requirements per action
|
|
103
|
+
actionMappings: {
|
|
104
|
+
features: {
|
|
105
|
+
'analytics.view_advanced': 'advanced_analytics',
|
|
106
|
+
'api.generate_key': 'api_access',
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Layer 3: Usage Limits (Quotas)
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Limit definitions
|
|
115
|
+
limits: {
|
|
116
|
+
team_members: {
|
|
117
|
+
name: 'billing.limits.team_members',
|
|
118
|
+
unit: 'count',
|
|
119
|
+
resetPeriod: 'never',
|
|
120
|
+
},
|
|
121
|
+
api_calls: {
|
|
122
|
+
name: 'billing.limits.api_calls',
|
|
123
|
+
unit: 'calls',
|
|
124
|
+
resetPeriod: 'monthly',
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Limit consumption per action
|
|
129
|
+
actionMappings: {
|
|
130
|
+
limits: {
|
|
131
|
+
'team.members.invite': 'team_members',
|
|
132
|
+
'api.call': 'api_calls',
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Plans Configuration
|
|
138
|
+
|
|
139
|
+
### Plan Definition Structure
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// contents/themes/default/config/billing.config.ts
|
|
143
|
+
import type { BillingConfig } from '@/core/lib/billing/config-types'
|
|
144
|
+
|
|
145
|
+
export const billingConfig: BillingConfig = {
|
|
146
|
+
provider: 'stripe',
|
|
147
|
+
currency: 'usd',
|
|
148
|
+
defaultPlan: 'free',
|
|
149
|
+
|
|
150
|
+
plans: [
|
|
151
|
+
{
|
|
152
|
+
slug: 'free',
|
|
153
|
+
name: 'billing.plans.free.name', // i18n key
|
|
154
|
+
description: 'billing.plans.free.description',
|
|
155
|
+
type: 'free',
|
|
156
|
+
visibility: 'public',
|
|
157
|
+
price: { monthly: 0, yearly: 0 }, // in cents
|
|
158
|
+
trialDays: 0,
|
|
159
|
+
features: ['basic_analytics'],
|
|
160
|
+
limits: {
|
|
161
|
+
team_members: 3,
|
|
162
|
+
tasks: 50,
|
|
163
|
+
api_calls: 1000,
|
|
164
|
+
},
|
|
165
|
+
stripePriceIdMonthly: null,
|
|
166
|
+
stripePriceIdYearly: null,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
slug: 'pro',
|
|
170
|
+
name: 'billing.plans.pro.name',
|
|
171
|
+
type: 'paid',
|
|
172
|
+
visibility: 'public',
|
|
173
|
+
price: {
|
|
174
|
+
monthly: 2900, // $29.00
|
|
175
|
+
yearly: 29000, // $290.00 (16% savings)
|
|
176
|
+
},
|
|
177
|
+
trialDays: 14,
|
|
178
|
+
features: [
|
|
179
|
+
'basic_analytics',
|
|
180
|
+
'advanced_analytics',
|
|
181
|
+
'api_access',
|
|
182
|
+
'priority_support',
|
|
183
|
+
],
|
|
184
|
+
limits: {
|
|
185
|
+
team_members: 15,
|
|
186
|
+
tasks: 1000,
|
|
187
|
+
api_calls: 100000,
|
|
188
|
+
},
|
|
189
|
+
stripePriceIdMonthly: 'price_pro_monthly',
|
|
190
|
+
stripePriceIdYearly: 'price_pro_yearly',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
slug: 'enterprise',
|
|
194
|
+
name: 'billing.plans.enterprise.name',
|
|
195
|
+
type: 'enterprise',
|
|
196
|
+
visibility: 'hidden',
|
|
197
|
+
features: ['*'], // All features
|
|
198
|
+
limits: {
|
|
199
|
+
team_members: -1, // Unlimited
|
|
200
|
+
tasks: -1,
|
|
201
|
+
api_calls: -1,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Plan Types
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
type PlanType = 'free' | 'paid' | 'enterprise'
|
|
212
|
+
type PlanVisibility = 'public' | 'hidden'
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Team-Based Subscriptions
|
|
216
|
+
|
|
217
|
+
Subscriptions are tied to teams, not users:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// Database schema: subscriptions table
|
|
221
|
+
{
|
|
222
|
+
id: string
|
|
223
|
+
teamId: string // Subscription belongs to team
|
|
224
|
+
planId: string // Current plan
|
|
225
|
+
status: SubscriptionStatus
|
|
226
|
+
billingInterval: 'monthly' | 'yearly'
|
|
227
|
+
externalSubscriptionId?: string // Stripe subscription ID
|
|
228
|
+
externalCustomerId?: string // Stripe customer ID
|
|
229
|
+
currentPeriodStart: Date
|
|
230
|
+
currentPeriodEnd: Date
|
|
231
|
+
cancelAtPeriodEnd: boolean
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Status values
|
|
235
|
+
type SubscriptionStatus =
|
|
236
|
+
| 'active'
|
|
237
|
+
| 'trialing'
|
|
238
|
+
| 'past_due'
|
|
239
|
+
| 'canceled'
|
|
240
|
+
| 'expired'
|
|
241
|
+
| 'paused'
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Checkout Flow
|
|
245
|
+
|
|
246
|
+
### Create Checkout Session
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
// app/api/v1/billing/checkout/route.ts
|
|
250
|
+
import { createCheckoutSession } from '@/core/lib/billing/gateways/stripe'
|
|
251
|
+
|
|
252
|
+
export async function POST(request: NextRequest) {
|
|
253
|
+
// 1. Authenticate request
|
|
254
|
+
const authResult = await authenticateRequest(request)
|
|
255
|
+
|
|
256
|
+
// 2. Validate request body
|
|
257
|
+
const { planSlug, billingPeriod } = checkoutSchema.parse(body)
|
|
258
|
+
|
|
259
|
+
// 3. Check permissions (team.billing.manage)
|
|
260
|
+
const membership = await MembershipService.get(userId, teamId)
|
|
261
|
+
const actionResult = membership.canPerformAction('billing.checkout')
|
|
262
|
+
|
|
263
|
+
if (!actionResult.allowed) {
|
|
264
|
+
return Response.json({ error: actionResult.message }, { status: 403 })
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 4. Create Stripe checkout session
|
|
268
|
+
const session = await createCheckoutSession({
|
|
269
|
+
teamId,
|
|
270
|
+
planSlug,
|
|
271
|
+
billingPeriod,
|
|
272
|
+
successUrl: `${appUrl}/dashboard/settings/billing?success=true`,
|
|
273
|
+
cancelUrl: `${appUrl}/dashboard/settings/billing?canceled=true`,
|
|
274
|
+
customerEmail: user.email,
|
|
275
|
+
customerId: existingCustomerId,
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
return Response.json({
|
|
279
|
+
success: true,
|
|
280
|
+
data: { url: session.url, sessionId: session.id }
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Stripe Checkout Session
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// core/lib/billing/gateways/stripe.ts
|
|
289
|
+
import Stripe from 'stripe'
|
|
290
|
+
|
|
291
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
|
|
292
|
+
|
|
293
|
+
export async function createCheckoutSession(params: CheckoutParams) {
|
|
294
|
+
const { teamId, planSlug, billingPeriod, successUrl, cancelUrl } = params
|
|
295
|
+
|
|
296
|
+
// Get plan from registry
|
|
297
|
+
const plan = BILLING_REGISTRY.getPlan(planSlug)
|
|
298
|
+
const priceId = billingPeriod === 'yearly'
|
|
299
|
+
? plan.stripePriceIdYearly
|
|
300
|
+
: plan.stripePriceIdMonthly
|
|
301
|
+
|
|
302
|
+
return stripe.checkout.sessions.create({
|
|
303
|
+
mode: 'subscription',
|
|
304
|
+
payment_method_types: ['card'],
|
|
305
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
306
|
+
success_url: successUrl,
|
|
307
|
+
cancel_url: cancelUrl,
|
|
308
|
+
client_reference_id: teamId,
|
|
309
|
+
metadata: { teamId, planSlug, billingPeriod },
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Customer Portal
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// app/api/v1/billing/portal/route.ts
|
|
318
|
+
import { createPortalSession } from '@/core/lib/billing/gateways/stripe'
|
|
319
|
+
|
|
320
|
+
export async function POST(request: NextRequest) {
|
|
321
|
+
// 1. Authenticate and get team subscription
|
|
322
|
+
const subscription = await SubscriptionService.getActive(teamId)
|
|
323
|
+
|
|
324
|
+
// 2. Create portal session
|
|
325
|
+
const portalSession = await createPortalSession({
|
|
326
|
+
customerId: subscription.externalCustomerId,
|
|
327
|
+
returnUrl: `${appUrl}/dashboard/settings/billing`,
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
return Response.json({
|
|
331
|
+
success: true,
|
|
332
|
+
data: { url: portalSession.url }
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Webhook Handling
|
|
338
|
+
|
|
339
|
+
### Stripe Webhook Endpoint
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
// app/api/v1/billing/webhooks/stripe/route.ts
|
|
343
|
+
import { verifyWebhookSignature } from '@/core/lib/billing/gateways/stripe'
|
|
344
|
+
|
|
345
|
+
export async function POST(request: NextRequest) {
|
|
346
|
+
// 1. Verify webhook signature (MANDATORY)
|
|
347
|
+
const payload = await request.text()
|
|
348
|
+
const signature = request.headers.get('stripe-signature')
|
|
349
|
+
const event = verifyWebhookSignature(payload, signature!)
|
|
350
|
+
|
|
351
|
+
// 2. Check for duplicate events (idempotency)
|
|
352
|
+
const existing = await queryOne(
|
|
353
|
+
`SELECT id FROM billing_events WHERE metadata->>'stripeEventId' = $1`,
|
|
354
|
+
[event.id]
|
|
355
|
+
)
|
|
356
|
+
if (existing) return Response.json({ received: true, status: 'duplicate' })
|
|
357
|
+
|
|
358
|
+
// 3. Handle events
|
|
359
|
+
switch (event.type) {
|
|
360
|
+
case 'checkout.session.completed':
|
|
361
|
+
await handleCheckoutCompleted(event.data.object)
|
|
362
|
+
break
|
|
363
|
+
case 'invoice.paid':
|
|
364
|
+
await handleInvoicePaid(event.data.object)
|
|
365
|
+
break
|
|
366
|
+
case 'invoice.payment_failed':
|
|
367
|
+
await handlePaymentFailed(event.data.object)
|
|
368
|
+
break
|
|
369
|
+
case 'customer.subscription.updated':
|
|
370
|
+
await handleSubscriptionUpdated(event.data.object)
|
|
371
|
+
break
|
|
372
|
+
case 'customer.subscription.deleted':
|
|
373
|
+
await handleSubscriptionDeleted(event.data.object)
|
|
374
|
+
break
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return Response.json({ received: true })
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Key Webhook Events
|
|
382
|
+
|
|
383
|
+
| Event | Action |
|
|
384
|
+
|-------|--------|
|
|
385
|
+
| `checkout.session.completed` | Create/update subscription with Stripe IDs |
|
|
386
|
+
| `invoice.paid` | Update period dates, sync invoice |
|
|
387
|
+
| `invoice.payment_failed` | Mark subscription as `past_due` |
|
|
388
|
+
| `customer.subscription.updated` | Sync status and plan changes |
|
|
389
|
+
| `customer.subscription.deleted` | Mark subscription as `canceled` |
|
|
390
|
+
|
|
391
|
+
### Webhook Security
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
// CRITICAL: Always verify webhook signatures
|
|
395
|
+
export function verifyWebhookSignature(
|
|
396
|
+
payload: string,
|
|
397
|
+
signature: string
|
|
398
|
+
): Stripe.Event {
|
|
399
|
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
|
|
400
|
+
return stripe.webhooks.constructEvent(payload, signature, webhookSecret)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// NOTE: Webhooks bypass RLS (no user context)
|
|
404
|
+
// Use direct query() calls, not queryWithRLS()
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Usage Tracking
|
|
408
|
+
|
|
409
|
+
### Usage Service
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
// core/lib/services/usage.service.ts
|
|
413
|
+
export class UsageService {
|
|
414
|
+
static async trackUsage(
|
|
415
|
+
teamId: string,
|
|
416
|
+
limitSlug: string,
|
|
417
|
+
amount: number = 1
|
|
418
|
+
): Promise<void> {
|
|
419
|
+
await query(
|
|
420
|
+
`INSERT INTO billing_usage (team_id, limit_slug, amount, period)
|
|
421
|
+
VALUES ($1, $2, $3, date_trunc('month', NOW()))
|
|
422
|
+
ON CONFLICT (team_id, limit_slug, period)
|
|
423
|
+
DO UPDATE SET amount = billing_usage.amount + $3`,
|
|
424
|
+
[teamId, limitSlug, amount]
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
static async getUsage(teamId: string, limitSlug: string): Promise<number> {
|
|
429
|
+
const result = await queryOne<{ amount: number }>(
|
|
430
|
+
`SELECT COALESCE(SUM(amount), 0) as amount
|
|
431
|
+
FROM billing_usage
|
|
432
|
+
WHERE team_id = $1 AND limit_slug = $2
|
|
433
|
+
AND period = date_trunc('month', NOW())`,
|
|
434
|
+
[teamId, limitSlug]
|
|
435
|
+
)
|
|
436
|
+
return result?.amount || 0
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
static async checkLimit(
|
|
440
|
+
teamId: string,
|
|
441
|
+
limitSlug: string
|
|
442
|
+
): Promise<{ allowed: boolean; current: number; limit: number }> {
|
|
443
|
+
const current = await this.getUsage(teamId, limitSlug)
|
|
444
|
+
const limit = await this.getLimitForTeam(teamId, limitSlug)
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
allowed: limit === -1 || current < limit, // -1 = unlimited
|
|
448
|
+
current,
|
|
449
|
+
limit,
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Check Action Before Performing
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
// app/api/v1/billing/check-action/route.ts
|
|
459
|
+
export async function POST(request: NextRequest) {
|
|
460
|
+
const { action } = await request.json()
|
|
461
|
+
|
|
462
|
+
const membership = await MembershipService.get(userId, teamId)
|
|
463
|
+
const result = membership.canPerformAction(action)
|
|
464
|
+
|
|
465
|
+
return Response.json({
|
|
466
|
+
success: true,
|
|
467
|
+
data: {
|
|
468
|
+
allowed: result.allowed,
|
|
469
|
+
reason: result.reason,
|
|
470
|
+
meta: result.meta, // { current, limit, limitSlug }
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Database Schema
|
|
477
|
+
|
|
478
|
+
### Why Plans/Subscriptions Use Inline JSONB (NOT Separate Meta Tables)
|
|
479
|
+
|
|
480
|
+
Unlike regular entities that use `{entity}_metas` tables, billing entities store metadata **inline** as JSONB columns:
|
|
481
|
+
|
|
482
|
+
| Entity Pattern | Storage | Why |
|
|
483
|
+
|----------------|---------|-----|
|
|
484
|
+
| Regular entities (`products`, `tasks`) | Separate `{entity}_metas` table | Dynamic, user-extensible, plugin-specific data |
|
|
485
|
+
| `plans` | Inline `features JSONB`, `limits JSONB` | Fixed structure, defined in config, read-heavy |
|
|
486
|
+
| `subscriptions` | No metas needed | All data is structured and predefined |
|
|
487
|
+
|
|
488
|
+
**Reasons for inline JSONB in billing:**
|
|
489
|
+
|
|
490
|
+
1. **Performance:** Plan lookups are extremely frequent (every permission check). Separate table = extra JOIN.
|
|
491
|
+
2. **Fixed schema:** Features/limits are defined in `billing.config.ts`, not user-extensible.
|
|
492
|
+
3. **Config-driven:** Plans are synced from config to DB, not created dynamically.
|
|
493
|
+
4. **Read-heavy:** Plans are read thousands of times per write. Inline is 10x faster.
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
// CORRECT for plans: Inline JSONB
|
|
497
|
+
features: JSONB DEFAULT '[]' // Array of feature slugs
|
|
498
|
+
limits: JSONB DEFAULT '{}' // { limitSlug: value }
|
|
499
|
+
|
|
500
|
+
// WRONG for plans: Separate meta table
|
|
501
|
+
// plans_metas table would add unnecessary complexity
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**When to use separate metas vs inline JSONB:**
|
|
505
|
+
|
|
506
|
+
| Use Separate `{entity}_metas` | Use Inline JSONB |
|
|
507
|
+
|-------------------------------|------------------|
|
|
508
|
+
| User can add arbitrary keys | Fixed, known keys |
|
|
509
|
+
| Plugins need to store data | Core-only data |
|
|
510
|
+
| Searchable by meta key/value | Rarely searched by meta |
|
|
511
|
+
| Low read frequency | Very high read frequency |
|
|
512
|
+
|
|
513
|
+
### Migrations
|
|
514
|
+
|
|
515
|
+
```sql
|
|
516
|
+
-- 012_billing_plans.sql
|
|
517
|
+
CREATE TABLE plans (
|
|
518
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
519
|
+
slug TEXT UNIQUE NOT NULL,
|
|
520
|
+
name TEXT NOT NULL,
|
|
521
|
+
type plan_type NOT NULL DEFAULT 'free',
|
|
522
|
+
visibility plan_visibility NOT NULL DEFAULT 'public',
|
|
523
|
+
"priceMonthly" INTEGER DEFAULT 0,
|
|
524
|
+
"priceYearly" INTEGER DEFAULT 0,
|
|
525
|
+
"trialDays" INTEGER DEFAULT 0,
|
|
526
|
+
features JSONB DEFAULT '[]', -- Inline meta: array of feature slugs
|
|
527
|
+
limits JSONB DEFAULT '{}', -- Inline meta: { limitSlug: number }
|
|
528
|
+
"stripePriceIdMonthly" TEXT,
|
|
529
|
+
"stripePriceIdYearly" TEXT,
|
|
530
|
+
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
531
|
+
"updatedAt" TIMESTAMPTZ DEFAULT NOW()
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
-- 013_billing_subscriptions.sql
|
|
535
|
+
CREATE TABLE subscriptions (
|
|
536
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
537
|
+
"teamId" TEXT REFERENCES teams(id) ON DELETE CASCADE,
|
|
538
|
+
"planId" TEXT REFERENCES plans(id),
|
|
539
|
+
status subscription_status NOT NULL DEFAULT 'active',
|
|
540
|
+
"billingInterval" TEXT DEFAULT 'monthly',
|
|
541
|
+
"externalSubscriptionId" TEXT,
|
|
542
|
+
"externalCustomerId" TEXT,
|
|
543
|
+
"currentPeriodStart" TIMESTAMPTZ,
|
|
544
|
+
"currentPeriodEnd" TIMESTAMPTZ,
|
|
545
|
+
"cancelAtPeriodEnd" BOOLEAN DEFAULT false,
|
|
546
|
+
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
547
|
+
"updatedAt" TIMESTAMPTZ DEFAULT NOW()
|
|
548
|
+
);
|
|
549
|
+
-- NOTE: No subscriptions_metas table - all data is structured
|
|
550
|
+
|
|
551
|
+
-- 014_billing_usage.sql
|
|
552
|
+
CREATE TABLE billing_usage (
|
|
553
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
554
|
+
"teamId" TEXT REFERENCES teams(id) ON DELETE CASCADE,
|
|
555
|
+
"limitSlug" TEXT NOT NULL,
|
|
556
|
+
amount INTEGER DEFAULT 0,
|
|
557
|
+
period DATE NOT NULL,
|
|
558
|
+
UNIQUE("teamId", "limitSlug", period)
|
|
559
|
+
);
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
## Environment Variables
|
|
563
|
+
|
|
564
|
+
```env
|
|
565
|
+
# Stripe Configuration
|
|
566
|
+
STRIPE_SECRET_KEY=sk_test_...
|
|
567
|
+
STRIPE_PUBLISHABLE_KEY=pk_test_...
|
|
568
|
+
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
569
|
+
|
|
570
|
+
# Stripe Price IDs (from billing.config.ts)
|
|
571
|
+
# These are set in the plan definitions, not env vars
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
## Registry Integration
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
// core/lib/registries/billing-registry.ts
|
|
578
|
+
// Auto-generated - DO NOT EDIT
|
|
579
|
+
|
|
580
|
+
export const BILLING_REGISTRY = {
|
|
581
|
+
config: billingConfig,
|
|
582
|
+
|
|
583
|
+
getPlan(slug: string): PlanDefinition | undefined,
|
|
584
|
+
getFeature(slug: string): FeatureDefinition | undefined,
|
|
585
|
+
getLimit(slug: string): LimitDefinition | undefined,
|
|
586
|
+
|
|
587
|
+
// Pre-computed matrices for O(1) lookups
|
|
588
|
+
planFeatureMatrix: Map<string, Set<string>>,
|
|
589
|
+
planLimitMatrix: Map<string, Map<string, number>>,
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
## Anti-Patterns
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
// NEVER: Hardcode plan prices in frontend
|
|
597
|
+
const price = '$29.00'
|
|
598
|
+
|
|
599
|
+
// CORRECT: Use plan config
|
|
600
|
+
const plan = BILLING_REGISTRY.getPlan('pro')
|
|
601
|
+
const price = formatCurrency(plan.price.monthly / 100)
|
|
602
|
+
|
|
603
|
+
// NEVER: Check plan features manually
|
|
604
|
+
if (plan.slug === 'pro' || plan.slug === 'business')
|
|
605
|
+
|
|
606
|
+
// CORRECT: Use feature checks
|
|
607
|
+
const hasFeature = membership.hasFeature('advanced_analytics')
|
|
608
|
+
|
|
609
|
+
// NEVER: Skip webhook signature verification
|
|
610
|
+
const event = JSON.parse(payload) // UNSAFE!
|
|
611
|
+
|
|
612
|
+
// CORRECT: Always verify signatures
|
|
613
|
+
const event = verifyWebhookSignature(payload, signature)
|
|
614
|
+
|
|
615
|
+
// NEVER: Use RLS queries in webhooks (no user context)
|
|
616
|
+
await queryWithRLS(userId, teamId, sql)
|
|
617
|
+
|
|
618
|
+
// CORRECT: Use direct queries in webhooks
|
|
619
|
+
await query(sql, params)
|
|
620
|
+
|
|
621
|
+
// NEVER: Store prices in dollars (use cents)
|
|
622
|
+
price: { monthly: 29.00 } // Wrong!
|
|
623
|
+
|
|
624
|
+
// CORRECT: Store prices in cents
|
|
625
|
+
price: { monthly: 2900 } // $29.00
|
|
626
|
+
|
|
627
|
+
// NEVER: Forget to handle -1 (unlimited)
|
|
628
|
+
if (current >= limit) return false
|
|
629
|
+
|
|
630
|
+
// CORRECT: Check for unlimited
|
|
631
|
+
if (limit === -1) return true
|
|
632
|
+
if (current >= limit) return false
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
## Checklist
|
|
636
|
+
|
|
637
|
+
Before finalizing billing implementation:
|
|
638
|
+
|
|
639
|
+
- [ ] Stripe API keys configured in environment
|
|
640
|
+
- [ ] Webhook endpoint configured in Stripe dashboard
|
|
641
|
+
- [ ] Webhook secret configured in environment
|
|
642
|
+
- [ ] Plans defined in `billing.config.ts` with Stripe price IDs
|
|
643
|
+
- [ ] Features and limits defined
|
|
644
|
+
- [ ] Action mappings configured (permissions, features, limits)
|
|
645
|
+
- [ ] Team-based subscription created on team creation
|
|
646
|
+
- [ ] Checkout flow tested (monthly and yearly)
|
|
647
|
+
- [ ] Portal flow tested
|
|
648
|
+
- [ ] All webhook events handled and tested
|
|
649
|
+
- [ ] Usage tracking implemented for metered limits
|
|
650
|
+
- [ ] Limit enforcement working
|
|
651
|
+
- [ ] Invoice sync working
|
|
652
|
+
- [ ] Translations added for plan names/descriptions
|
|
653
|
+
- [ ] Error handling for failed payments
|
|
654
|
+
|
|
655
|
+
## Related Skills
|
|
656
|
+
|
|
657
|
+
- `permissions-system` - RBAC integration
|
|
658
|
+
- `entity-api` - API patterns for billing endpoints
|
|
659
|
+
- `service-layer` - Service class patterns
|
|
660
|
+
- `database-migrations` - Billing table migrations
|