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:
Moheeb Zara
2026-05-18 18:34:03 -07:00
parent 705a93291b
commit d2b7aa6897
3 changed files with 49 additions and 1 deletions
+17 -1
View File
@@ -21,6 +21,7 @@ import {
memberStatusPatch,
stripeInvoiceToPaymentRow,
minorToMajor,
laterPeriodEnd,
} from '@/lib/stripe/webhook-logic'
import {
renderDuesEmail,
@@ -193,7 +194,22 @@ export async function POST(
customerId,
)
if (!memberId) return
const periodEnd = subscriptionPeriodEnd(sub)
const incomingPeriodEnd = subscriptionPeriodEnd(sub)
// Never rewind the period on a stale/out-of-order event. Status still
// reflects the latest sub.status, but graceExceeded is computed from the
// monotonic (non-rewound) period so a late event can't false-lapse a
// paid member.
const { data: existingBilling } = await admin
.from('member_billing')
.select('current_period_end')
.eq('space_id', spaceId)
.eq('member_id', memberId)
.maybeSingle()
const periodEnd = laterPeriodEnd(
(existingBilling?.current_period_end as string | null) ?? null,
incomingPeriodEnd,
)
await admin.from('member_billing').upsert(
{