Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions modules/billing/services/billing.admin.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,11 @@ const syncOrgFromStripe = async (orgId) => {
const stripeSub = await stripe.subscriptions.retrieve(existing.stripeSubscriptionId);
const newPlan = resolveStripePlan(stripeSub);
const newStatus = stripeSub.status;
const newPeriodStart = stripeSub.current_period_start
? new Date(stripeSub.current_period_start * 1000)
// Stripe API ≥ 2025-08-27 moved current_period_start to items.data[0].
// Read from items first, fall back to top-level for older API versions (mirrors webhook handler).
const rawPeriodStart = stripeSub.items?.data?.[0]?.current_period_start ?? stripeSub.current_period_start;
const newPeriodStart = rawPeriodStart
? new Date(rawPeriodStart * 1000)
: null;

const previous = { plan: existing.plan, status: existing.status };
Expand Down
39 changes: 39 additions & 0 deletions modules/billing/tests/billing.admin.service.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,45 @@ describe('BillingAdminService unit tests:', () => {
test('throws 422 for invalid orgId format', async () => {
await expect(BillingAdminService.syncOrgFromStripe('bad-id')).rejects.toMatchObject({ status: 422 });
});

test('writes non-null currentPeriodStart when period is only in items.data[0] (Stripe API ≥ 2025-08-27)', async () => {
// Simulate Stripe API ≥ 2025-08-27: top-level current_period_start is absent,
// period lives in items.data[0].current_period_start only.
const periodStart = 1750000000;
mockStripeInstance.subscriptions.retrieve.mockResolvedValue({
id: stripeSubId,
status: 'active',
current_period_start: undefined, // absent on new API
items: {
data: [{
current_period_start: periodStart,
price: { metadata: { planId: 'pro' } },
}],
},
});

await BillingAdminService.syncOrgFromStripe(orgId);

const updateCall = mockSubscriptionRepository.update.mock.calls[0][0];
expect(updateCall.currentPeriodStart).toEqual(new Date(periodStart * 1000));
});

test('falls back to top-level current_period_start when items.data[0] has none (legacy API)', async () => {
const periodStart = 1699000000;
mockStripeInstance.subscriptions.retrieve.mockResolvedValue({
id: stripeSubId,
status: 'active',
current_period_start: periodStart,
items: {
data: [{ price: { metadata: { planId: 'pro' } } }], // no current_period_start on item
},
});

await BillingAdminService.syncOrgFromStripe(orgId);

const updateCall = mockSubscriptionRepository.update.mock.calls[0][0];
expect(updateCall.currentPeriodStart).toEqual(new Date(periodStart * 1000));
});
});

// ─────────────────────────────────────────────────────────────────────────────
Expand Down
Loading