-
-
Notifications
You must be signed in to change notification settings - Fork 10
feat(billing): compute layer foundation - PR-N1 (no behavior change) #3535
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
eb60d45
feat(billing): compute layer foundation - pr-n1 (no behavior change)
PierreBrisorgueil 20ac494
fix(billing): remove duplicate index creation from migrations
PierreBrisorgueil 0878194
fix(billing): make migrations pure no-ops - all indexes are Mongoose-…
PierreBrisorgueil 4a3eeec
test(billing): remove done callbacks from synchronous Zod schema tests
PierreBrisorgueil 615cac6
fix(billing): address CodeRabbit major + minor findings on billing.pl…
PierreBrisorgueil 0095604
fix(billing): suppress Codacy false-positive useQwikValidLexicalScope…
PierreBrisorgueil 830b425
refactor(billing): apply critical-review fixes — repository pattern +…
PierreBrisorgueil 3e9c010
fix(billing): add biome-ignore suppressions for useQwikValidLexicalSc…
PierreBrisorgueil 0b04e3e
fix(billing): address CodeRabbit Major/Critical findings on indexes +…
PierreBrisorgueil b9c005e
test(billing): add unit tests for billing.plan.repository.js
PierreBrisorgueil 6fd7fd9
docs(billing): align stale 'Compute fields' comment with meter rename
PierreBrisorgueil File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
modules/billing/migrations/20260501000000-add-compute-fields.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /** | ||
| * Migration: Add compute fields to billing_usages collection | ||
| * | ||
| * Adds weekKey, computeUsed, computeQuota, planVersion, computeBreakdown, | ||
| * resetAt, alertedAt80, alertedAt100, consumedHistoryIds to existing documents. | ||
| * | ||
| * Additive only — no data backfill (existing documents keep legacy values). | ||
| * Idempotent: safe to run multiple times. | ||
| * | ||
| * @returns {Promise<void>} | ||
| */ | ||
| export async function up() { | ||
| const { db } = await import('mongoose').then((m) => ({ db: m.default.connection.db })); | ||
| const collection = db.collection('billingusages'); | ||
|
|
||
| // Add sparse index for weekKey — only indexes docs where weekKey exists. | ||
| // createIndex is idempotent: no-op if the index already exists. | ||
| await collection.createIndex( | ||
| { organizationId: 1, weekKey: 1 }, | ||
| { unique: true, sparse: true, name: 'organizationId_weekKey_unique_sparse' }, | ||
| ); | ||
|
|
||
| // No document backfill: new fields have defaults in the Mongoose schema | ||
| // (computeUsed: 0, computeQuota: 0, computeBreakdown: {}, consumedHistoryIds: []). | ||
| // Existing documents without these fields will use Mongoose defaults on read. | ||
| } | ||
|
|
||
| /** | ||
| * Down: remove the weekKey sparse index. | ||
| * Does NOT remove fields (additive migrations are one-way safe). | ||
| * @returns {Promise<void>} | ||
| */ | ||
| export async function down() { | ||
| const { db } = await import('mongoose').then((m) => ({ db: m.default.connection.db })); | ||
| const collection = db.collection('billingusages'); | ||
|
|
||
| try { | ||
| await collection.dropIndex('organizationId_weekKey_unique_sparse'); | ||
| } catch (err) { | ||
| // Index may not exist — ignore "index not found" errors | ||
| if (err?.codeName !== 'IndexNotFound' && err?.code !== 27) throw err; | ||
| } | ||
| } |
55 changes: 55 additions & 0 deletions
55
modules/billing/migrations/20260501000100-add-plan-version-and-period-start.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| /** | ||
| * Migration: Add planVersion and currentPeriodStart to subscriptions collection | ||
| * | ||
| * Also ensures the billingplans and processedstripeevents collections have | ||
| * their required indexes. | ||
| * | ||
| * Additive only — no data backfill. | ||
| * Idempotent: safe to run multiple times. | ||
| * | ||
| * @returns {Promise<void>} | ||
| */ | ||
| export async function up() { | ||
| const { db } = await import('mongoose').then((m) => ({ db: m.default.connection.db })); | ||
|
|
||
| // ── subscriptions: sparse index for planVersion ───────────────────────── | ||
| const subscriptions = db.collection('subscriptions'); | ||
| await subscriptions.createIndex( | ||
| { planVersion: 1 }, | ||
| { sparse: true, name: 'planVersion_sparse' }, | ||
| ); | ||
|
|
||
| // ── billingplans: create indexes if collection doesn't already have them ── | ||
| const billingplans = db.collection('billingplans'); | ||
| await billingplans.createIndex( | ||
| { planId: 1, version: 1 }, | ||
| { unique: true, name: 'planId_version_unique' }, | ||
| ); | ||
| await billingplans.createIndex( | ||
| { planId: 1, active: 1, effectiveUntil: 1 }, | ||
| { name: 'planId_active_effectiveUntil' }, | ||
| ); | ||
|
|
||
| // ── processedstripeevents: TTL index ───────────────────────────────────── | ||
| const events = db.collection('processedstripeevents'); | ||
| await events.createIndex( | ||
| { processedAt: 1 }, | ||
| { expireAfterSeconds: 30 * 24 * 60 * 60, name: 'processedAt_ttl_30d' }, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Down: remove the added sparse index from subscriptions. | ||
| * Does NOT remove fields or drop the new collections. | ||
| * @returns {Promise<void>} | ||
| */ | ||
| export async function down() { | ||
| const { db } = await import('mongoose').then((m) => ({ db: m.default.connection.db })); | ||
| const subscriptions = db.collection('subscriptions'); | ||
|
|
||
| try { | ||
| await subscriptions.dropIndex('planVersion_sparse'); | ||
| } catch (err) { | ||
| if (err?.codeName !== 'IndexNotFound' && err?.code !== 27) throw err; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| /** | ||
| * Module dependencies | ||
| */ | ||
| import mongoose from 'mongoose'; | ||
|
|
||
| const Schema = mongoose.Schema; | ||
|
|
||
| /** | ||
| * BillingPlan Data Model Mongoose | ||
| * | ||
| * Versioned plan definitions for compute-based pricing. | ||
| * Each (planId, version) pair is immutable after creation. | ||
| * Use bumpVersion to create a new version and deactivate the previous one. | ||
| */ | ||
| const BillingPlanMongoose = new Schema( | ||
| { | ||
| planId: { | ||
| type: String, | ||
| required: true, | ||
| trim: true, | ||
| }, | ||
| version: { | ||
| type: String, | ||
| required: true, | ||
| trim: true, | ||
| }, | ||
| computeQuota: { | ||
| type: Number, | ||
| required: true, | ||
| min: 0, | ||
| }, | ||
| stripePriceMonthly: { | ||
| type: String, | ||
| trim: true, | ||
| sparse: true, | ||
| }, | ||
| stripePriceAnnual: { | ||
| type: String, | ||
| trim: true, | ||
| sparse: true, | ||
| }, | ||
| /** | ||
| * Flexible ratio map for compute unit attribution. | ||
| * Example: { scrap: 1, autofix: 2, wizard: 5 } | ||
| */ | ||
| ratios: { | ||
| type: Schema.Types.Mixed, | ||
| default: () => ({}), | ||
| }, | ||
|
PierreBrisorgueil marked this conversation as resolved.
|
||
| effectiveFrom: { | ||
| type: Date, | ||
| required: true, | ||
| }, | ||
| effectiveUntil: { | ||
| type: Date, | ||
| default: null, | ||
| }, | ||
| active: { | ||
| type: Boolean, | ||
| default: true, | ||
| index: true, | ||
| }, | ||
| }, | ||
| { | ||
| timestamps: true, | ||
| }, | ||
| ); | ||
|
|
||
| /** | ||
| * Unique index per (planId, version) — immutable identity | ||
| */ | ||
| BillingPlanMongoose.index({ planId: 1, version: 1 }, { unique: true }); | ||
|
PierreBrisorgueil marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Compound index to look up the active plan for a given planId efficiently | ||
| */ | ||
| BillingPlanMongoose.index({ planId: 1, active: 1, effectiveUntil: 1 }); | ||
|
PierreBrisorgueil marked this conversation as resolved.
Outdated
PierreBrisorgueil marked this conversation as resolved.
Outdated
PierreBrisorgueil marked this conversation as resolved.
Outdated
|
||
|
|
||
| /** | ||
| * Returns the hex string representation of the document ObjectId. | ||
| * @returns {string} Hex string of the ObjectId. | ||
| */ | ||
| function addID() { | ||
| return this._id.toHexString(); | ||
| } | ||
|
|
||
| /** | ||
| * Model configuration | ||
| */ | ||
| BillingPlanMongoose.virtual('id').get(addID); | ||
| BillingPlanMongoose.set('toJSON', { | ||
| virtuals: true, | ||
| }); | ||
|
|
||
| mongoose.model('BillingPlan', BillingPlanMongoose); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| /** | ||
| * Module dependencies | ||
| */ | ||
| import { z } from 'zod'; | ||
|
|
||
| /** | ||
| * BillingPlan Zod schema — mirrors billing.plan.model.mongoose.js | ||
| */ | ||
|
|
||
| const BillingPlan = z.object({ | ||
| planId: z.string().trim().min(1, 'planId is required'), | ||
| version: z.string().trim().min(1, 'version is required'), | ||
| computeQuota: z.number().int().min(0, 'computeQuota must be >= 0'), | ||
| stripePriceMonthly: z.string().trim().optional().nullable(), | ||
| stripePriceAnnual: z.string().trim().optional().nullable(), | ||
| ratios: z.record(z.string(), z.number()).default(() => ({})), | ||
| effectiveFrom: z.coerce.date(), | ||
| effectiveUntil: z.coerce.date().nullable().optional(), | ||
| active: z.boolean().default(true), | ||
| }); | ||
|
|
||
| /** | ||
| * Schema for bumping to a new plan version. | ||
| * computeQuota and ratios are required; other fields are optional overrides. | ||
|
PierreBrisorgueil marked this conversation as resolved.
Outdated
|
||
| */ | ||
| const BillingPlanBump = z | ||
| .object({ | ||
| computeQuota: z.number().int().min(0), | ||
| ratios: z.record(z.string(), z.number()).optional(), | ||
| stripePriceMonthly: z.string().trim().optional().nullable(), | ||
| stripePriceAnnual: z.string().trim().optional().nullable(), | ||
| }) | ||
| .strict(); | ||
|
PierreBrisorgueil marked this conversation as resolved.
|
||
|
|
||
| export default { | ||
| BillingPlan, | ||
| BillingPlanBump, | ||
| }; | ||
61 changes: 61 additions & 0 deletions
61
modules/billing/models/billing.processedStripeEvent.model.mongoose.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /** | ||
| * Module dependencies | ||
| */ | ||
| import mongoose from 'mongoose'; | ||
|
|
||
| const Schema = mongoose.Schema; | ||
|
|
||
| /** | ||
| * ProcessedStripeEvent Data Model Mongoose | ||
| * | ||
| * Idempotency store for Stripe webhook events. | ||
| * TTL of 30 days — events older than that can safely be re-processed | ||
| * (Stripe's webhook retry window is 3 days). | ||
| */ | ||
| const ProcessedStripeEventMongoose = new Schema( | ||
| { | ||
| eventId: { | ||
| type: String, | ||
| required: true, | ||
| unique: true, | ||
| trim: true, | ||
| }, | ||
| type: { | ||
| type: String, | ||
| required: true, | ||
| trim: true, | ||
| }, | ||
| processedAt: { | ||
| type: Date, | ||
| required: true, | ||
| default: () => new Date(), | ||
| }, | ||
| }, | ||
| { | ||
| timestamps: false, | ||
| // Disable _id virtuals to keep the document lean | ||
| }, | ||
| ); | ||
|
|
||
| /** | ||
| * TTL index: automatically remove documents 30 days after processedAt | ||
| */ | ||
| ProcessedStripeEventMongoose.index({ processedAt: 1 }, { expireAfterSeconds: 30 * 24 * 60 * 60 }); | ||
|
PierreBrisorgueil marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Returns the hex string representation of the document ObjectId. | ||
| * @returns {string} Hex string of the ObjectId. | ||
| */ | ||
| function addID() { | ||
| return this._id.toHexString(); | ||
| } | ||
|
|
||
| /** | ||
| * Model configuration | ||
| */ | ||
| ProcessedStripeEventMongoose.virtual('id').get(addID); | ||
| ProcessedStripeEventMongoose.set('toJSON', { | ||
| virtuals: true, | ||
| }); | ||
|
|
||
| mongoose.model('ProcessedStripeEvent', ProcessedStripeEventMongoose); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.