fix(stripe): never rewind billing period on out-of-order webhooks (D5)
Stripe doesn't guarantee event order. A late customer.subscription. updated carrying an older period overwrote a newer current_period_end, so graceExceeded flipped a paid member to 'late' and fired a false 'dues lapsed' email + access loss. applySubscription now reads the stored period and keeps the later of stored vs incoming (laterPeriodEnd, pure + tested); status still tracks latest sub.status but grace is computed from the monotonic period.
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
memberStatusPatch,
|
||||
stripeInvoiceToPaymentRow,
|
||||
minorToMajor,
|
||||
laterPeriodEnd,
|
||||
} from '@/lib/stripe/webhook-logic'
|
||||
|
||||
describe('isoFromUnix', () => {
|
||||
@@ -123,6 +124,22 @@ describe('stripeInvoiceToPaymentRow', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('laterPeriodEnd (never rewind on out-of-order events)', () => {
|
||||
const older = '2026-05-01T00:00:00.000Z'
|
||||
const newer = '2026-06-01T00:00:00.000Z'
|
||||
it('keeps the later of stored vs incoming regardless of arrival order', () => {
|
||||
expect(laterPeriodEnd(newer, older)).toBe(newer) // stale event arrives late
|
||||
expect(laterPeriodEnd(older, newer)).toBe(newer) // normal advance
|
||||
expect(laterPeriodEnd(newer, newer)).toBe(newer)
|
||||
})
|
||||
it('handles nulls (first event / canceled sub with no period)', () => {
|
||||
expect(laterPeriodEnd(null, newer)).toBe(newer)
|
||||
expect(laterPeriodEnd(newer, null)).toBe(newer)
|
||||
expect(laterPeriodEnd(null, null)).toBeNull()
|
||||
expect(laterPeriodEnd(undefined, undefined)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('minorToMajor', () => {
|
||||
it('divides by 100, treats nullish as 0', () => {
|
||||
expect(minorToMajor(2500)).toBe(25)
|
||||
|
||||
Reference in New Issue
Block a user