Skip to content

Commit 3deb607

Browse files
Merge remote-tracking branch 'origin/master' into billing-5-get-plans
2 parents 56dca78 + 9836a8e commit 3deb607

10 files changed

Lines changed: 423 additions & 225 deletions

File tree

.claude/skills/create-module/SKILL.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ Files to check in `modules/{new-module-name}/`:
5151
- Policy function names
5252
- Test descriptions and fixture data
5353

54+
### 4b. Config-driven enums
55+
56+
Business values (plans, roles, statuses) must be driven by the module config, never hardcoded in models or schemas.
57+
58+
Pattern:
59+
- Define values in `modules/{module}/config/{module}.development.config.js`
60+
- Reference in Mongoose model: `enum: config.{section}.{enumName}`
61+
- Reference in Zod schema: `z.enum(config.{section}.{enumName})`
62+
63+
Example: `config.billing.plans` used in both `billing.subscription.model.mongoose.js` and `billing.subscription.schema.js`.
64+
65+
> Existing reference: `users.schema.js` uses `z.enum(config.whitelists.users.roles)`.
66+
5467
### 5. Apply renames carefully
5568

5669
- Case-sensitive, whole-word matches where possible

.claude/skills/naming/SKILL.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ Audit or apply the project's file and folder naming conventions.
3434

3535
> `[.{name}]` is optional — the reference `tasks` module uses the short form (e.g., `tasks.controller.js`, `tasks.service.js`).
3636
37+
### Multi-entity modules
38+
39+
When a module contains multiple entities (e.g., `billing` with `subscription`, `organizations` with `membership`), files are still prefixed by the **module name**, not the entity:
40+
41+
| Correct | Wrong |
42+
| --- | --- |
43+
| `billing.subscription.model.mongoose.js` | `subscription.model.mongoose.js` |
44+
| `billing.subscription.schema.js` | `subscription.schema.js` |
45+
| `organizations.membership.model.mongoose.js` | `membership.model.mongoose.js` |
46+
47+
Pattern: `{module}.{entity}[.{name}].{type}.js`
48+
3749
### Naming Tokens
3850

3951
From a module name (e.g., `my-feature`):

modules/billing/config/billing.development.config.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
const config = {
2+
billing: {
3+
// Plans available for subscriptions — extend as needed
4+
plans: ['free', 'starter', 'pro', 'enterprise'],
5+
// Stripe subscription statuses — see https://docs.stripe.com/api/subscriptions/object#subscription_object-status
6+
statuses: [
7+
'incomplete',
8+
'incomplete_expired',
9+
'trialing',
10+
'active',
11+
'past_due',
12+
'canceled',
13+
'unpaid',
14+
'paused',
15+
],
16+
},
217
stripe: {
318
secretKey: process.env.DEVKIT_NODE_stripe_secretKey ?? '',
419
webhookSecret: process.env.DEVKIT_NODE_stripe_webhookSecret ?? '',

modules/billing/models/billing.subscription.model.mongoose.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Module dependencies
33
*/
44
import mongoose from 'mongoose';
5+
import config from '../../../config/index.js';
56

67
const Schema = mongoose.Schema;
78

@@ -28,12 +29,12 @@ const SubscriptionMongoose = new Schema(
2829
},
2930
plan: {
3031
type: String,
31-
enum: ['free', 'starter', 'pro'],
32+
enum: config.billing.plans,
3233
default: 'free',
3334
},
3435
status: {
3536
type: String,
36-
enum: ['active', 'past_due', 'canceled', 'trialing', 'incomplete'],
37+
enum: config.billing.statuses,
3738
default: 'active',
3839
},
3940
currentPeriodEnd: {

modules/billing/models/billing.subscription.schema.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Module dependencies
33
*/
44
import { z } from 'zod';
5+
import config from '../../../config/index.js';
56

67
/**
78
* Data Schema
@@ -14,9 +15,6 @@ const optionalStripeId = z
1415
.optional()
1516
.transform((val) => (val === '' ? undefined : val));
1617

17-
const plans = ['free', 'starter', 'pro'];
18-
const statuses = ['active', 'past_due', 'canceled', 'trialing', 'incomplete'];
19-
2018
const baseShape = {
2119
organization: z.string().trim().regex(objectIdRegex, 'organization must be a valid ObjectId'),
2220
stripeCustomerId: optionalStripeId,
@@ -26,8 +24,8 @@ const baseShape = {
2624

2725
const Subscription = z.object({
2826
...baseShape,
29-
plan: z.enum(plans).default('free'),
30-
status: z.enum(statuses).default('active'),
27+
plan: z.enum(config.billing.plans).default('free'),
28+
status: z.enum(config.billing.statuses).default('active'),
3129
cancelAtPeriodEnd: z.boolean().default(false),
3230
});
3331

@@ -36,8 +34,8 @@ const Subscription = z.object({
3634
*/
3735
const SubscriptionCore = z.object({
3836
...baseShape,
39-
plan: z.enum(plans),
40-
status: z.enum(statuses),
37+
plan: z.enum(config.billing.plans),
38+
status: z.enum(config.billing.statuses),
4139
cancelAtPeriodEnd: z.boolean(),
4240
});
4341

modules/billing/tests/billing.unit.tests.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* Module dependencies.
33
*/
4+
import config from '../../../config/index.js';
45
import schema from '../models/billing.subscription.schema.js';
56

67
/**
@@ -82,7 +83,7 @@ describe('Billing unit tests:', () => {
8283
});
8384

8485
test('should accept all valid plan values', (done) => {
85-
for (const plan of ['free', 'starter', 'pro']) {
86+
for (const plan of config.billing.plans) {
8687
subscription.plan = plan;
8788
const result = schema.Subscription.safeParse(subscription);
8889
expect(result.error).toBeFalsy();
@@ -91,7 +92,7 @@ describe('Billing unit tests:', () => {
9192
});
9293

9394
test('should accept all valid status values', (done) => {
94-
for (const status of ['active', 'past_due', 'canceled', 'trialing', 'incomplete']) {
95+
for (const status of config.billing.statuses) {
9596
subscription.status = status;
9697
const result = schema.Subscription.safeParse(subscription);
9798
expect(result.error).toBeFalsy();

modules/organizations/models/organizations.model.mongoose.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
*/
44
import mongoose from 'mongoose';
55

6+
import config from '../../../config/index.js';
7+
68
const Schema = mongoose.Schema;
79

810
/**
@@ -34,7 +36,7 @@ const OrganizationMongoose = new Schema(
3436
},
3537
plan: {
3638
type: String,
37-
enum: ['free', 'starter', 'pro', 'enterprise'],
39+
enum: config.billing.plans,
3840
default: 'free',
3941
},
4042
createdBy: {

modules/organizations/models/organizations.schema.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
*/
44
import { z } from 'zod';
55

6+
import config from '../../../config/index.js';
7+
68
/**
79
* Data Schema
810
*/
@@ -11,7 +13,7 @@ const Organization = z.object({
1113
description: z.string().trim().default(''),
1214
slug: z.string().trim().min(1).toLowerCase().optional(),
1315
domain: z.string().trim().default(''),
14-
plan: z.enum(['free', 'starter', 'pro', 'enterprise']).default('free'),
16+
plan: z.enum(config.billing.plans).default('free'),
1517
});
1618

1719
const OrganizationUpdate = Organization.partial();

0 commit comments

Comments
 (0)