Skip to content

Commit b5cfb57

Browse files
fix(billing): include error stacks + cover syncOrganizationPlan failure path
Add stack: err?.stack to both logger.error calls in the syncOrganizationPlan catch block to match the convention used throughout billing.webhook.service.js. Add unit test covering the dunning recovery setPlan failure path: asserts non-fatal behaviour, logger.error call, and billing.organization.sync_failed emit.
1 parent e76019b commit b5cfb57

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

modules/billing/services/billing.webhook.service.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,12 +611,14 @@ const handleInvoicePaymentSucceeded = async (invoice, event) => {
611611
logger.error('[billing.webhook] syncOrganizationPlan failed (non-fatal)', {
612612
organizationId,
613613
error: syncErr?.message ?? String(syncErr),
614+
stack: syncErr?.stack,
614615
});
615616
try {
616617
billingEvents.emit('billing.organization.sync_failed', { organizationId, source: 'dunning_recovery' });
617618
} catch (evtErr) {
618619
logger.error('[billing.webhook] billing.organization.sync_failed listener error (non-fatal)', {
619620
error: evtErr?.message ?? String(evtErr),
621+
stack: evtErr?.stack,
620622
});
621623
}
622624
}

modules/billing/tests/billing.webhook.subscription.unit.tests.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,40 @@ describe('Billing webhook subscription unit tests:', () => {
600600
'invoice',
601601
);
602602
});
603+
604+
// V8.1 — syncOrganizationPlan failure path coverage
605+
test('V8.1 — syncOrganizationPlan failure: non-fatal, logs error + emits sync_failed', async () => {
606+
const existing = {
607+
_id: subId,
608+
organization: orgId,
609+
pastDueSince: new Date('2026-04-01'),
610+
status: 'unpaid',
611+
plan: 'free',
612+
};
613+
mockSubscriptionRepository.findByStripeSubscriptionId.mockResolvedValue(existing);
614+
mockStripe.subscriptions.retrieve.mockResolvedValue({
615+
items: { data: [{ price: { metadata: { planId: 'pro' } } }] },
616+
});
617+
mockOrganizationRepository.setPlan.mockRejectedValue(new Error('DB write failed'));
618+
619+
// The logger mock is registered in beforeEach via jest.unstable_mockModule.
620+
// Import it here (after BillingWebhookService) to get the same mocked instance
621+
// that the service module captured at load time.
622+
const { default: mockLogger } = await import('../../../lib/services/logger.js');
623+
624+
await expect(
625+
BillingWebhookService.handleInvoicePaymentSucceeded({ subscription: 'sub_456' }, makeEvent()),
626+
).resolves.not.toThrow();
627+
628+
expect(mockLogger.error).toHaveBeenCalledWith(
629+
'[billing.webhook] syncOrganizationPlan failed (non-fatal)',
630+
expect.objectContaining({ organizationId: orgId }),
631+
);
632+
expect(mockEvents.emit).toHaveBeenCalledWith(
633+
'billing.organization.sync_failed',
634+
expect.objectContaining({ organizationId: orgId, source: 'dunning_recovery' }),
635+
);
636+
});
603637
});
604638

605639
describe('handleInvoicePaymentFailed', () => {

0 commit comments

Comments
 (0)