Skip to content

Commit 73e4062

Browse files
fix(billing): reconcile trialing subscriptions (#3626)
* fix(billing): include trialing subscriptions in reconciliation sweep Add 'trialing' to RECONCILE_STATUSES so orgs on a trial period are included in the weekly Stripe↔DB reconciliation. Previously they were invisible until transitioning to active/past_due (7-14 day blind spot). * docs(billing): update runReconciliation JSDoc to reflect trialing status
1 parent 0005b45 commit 73e4062

2 files changed

Lines changed: 21 additions & 2 deletions

File tree

modules/billing/services/billing.reconcile.service.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const RECONCILE_PAGE_SIZE = 100;
1515
/**
1616
* Statuses reconciled against Stripe.
1717
*/
18-
const RECONCILE_STATUSES = ['active', 'past_due'];
18+
const RECONCILE_STATUSES = ['active', 'past_due', 'trialing'];
1919

2020
/**
2121
* Valid plan names from config.
@@ -37,7 +37,7 @@ const resolveStripePlan = (subscription) => {
3737

3838
/**
3939
* @function runReconciliation
40-
* @description Paginate all active|past_due subscriptions, fetch live Stripe status for each,
40+
* @description Paginate all active|past_due|trialing subscriptions, fetch live Stripe status for each,
4141
* and emit billing.reconciliation.divergence when status or plan diverges.
4242
*
4343
* LOG-ONLY policy: this function NEVER writes to the DB or Stripe.

modules/billing/tests/billing.reconcile.service.unit.tests.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,25 @@ describe('BillingReconcileService.runReconciliation unit tests:', () => {
182182
);
183183
});
184184

185+
test('detects plan drift on trialing sub — emits billing.reconciliation.divergence (C4)', async () => {
186+
const sub = makeDbSub({ status: 'trialing', plan: 'pro' });
187+
mockSubscriptionRepository.findPageForReconciliation
188+
.mockResolvedValueOnce([sub])
189+
.mockResolvedValue([]);
190+
// Stripe has plan: starter — simulates Dashboard bump with lost webhook
191+
mockStripeInstance.subscriptions.retrieve.mockResolvedValue(
192+
makeStripeSub({ status: 'trialing', items: { data: [{ price: { metadata: { planId: 'starter' } } }] } }),
193+
);
194+
195+
const result = await BillingReconcileService.runReconciliation();
196+
197+
expect(result).toMatchObject({ checked: 1, divergences: 1, errors: 0 });
198+
expect(mockEvents.emit).toHaveBeenCalledWith(
199+
'billing.reconciliation.divergence',
200+
expect.objectContaining({ organizationId: orgId, planMismatch: true }),
201+
);
202+
});
203+
185204
test('counts errors and continues on individual Stripe retrieve failure', async () => {
186205
const subs = [makeDbSub(), makeDbSub({ _id: 'sub_doc_002', stripeSubscriptionId: 'sub_test_002' })];
187206
mockSubscriptionRepository.findPageForReconciliation

0 commit comments

Comments
 (0)