Small, independent hardening fixes in the billing and auth stack modules. They were found by formal models and property tests of the billing and signup flows. Each one is a rare edge case whose correct behaviour costs a few lines. No new persisted field, collection or flag.
Fixes (one commit each; red test first, then green):
- Checkout webhook: when
stripe.subscriptions.retrieve fails in the checkout.session.completed handler (billing.webhook.service.js, around the retrieve catch), throw instead of return. Stripe then redelivers the event, instead of it being recorded as processed while the subscription row stays unlinked on the free plan. Update the unit test that pins the old behaviour.
- Invoice webhooks: in the
invoice.payment_failed and invoice.payment_succeeded handlers, right after findByStripeSubscriptionId, add if (existing.status === 'canceled') return;. A late invoice event must not bring a canceled subscription back to active or past_due.
- Metering:
incrementMeter (billing.usage.service.js) must bill against the free plan when the subscription status is fail-closed (unpaid / paused / incomplete, the same list the quota gate uses in billing.quota.service.js). Today the gate treats these as free while the meter still consumes the paid quota. Widen the findPlan projection to include status and reuse the gate's list; don't duplicate it.
- Extras debit: wrap
BillingExtraService.debit in the existing retryWithBackoff (modules/billing/lib/billing.retry.js). The debit is idempotent by key, so a retry cannot double-charge, and a transient DB error no longer makes the overflow free.
- Grant idempotency across orgs: in
creditGrant (billing.extraBalance.repository.js), before the write, return { applied: false, reason: 'duplicate_grant' } when exists({ 'ledger.refId': idempotencyKey }) holds in any org. Today the guard is per org, so a grant whose target org changed between retries is credited twice.
- Email verification race: in
verifyEmail (auth controller), consume the token with one atomic findOneAndUpdate on the unexpired token that sets it to null and marks the email verified. A null result returns the existing 400. Two concurrent verifications must not provision two workspaces.
- Runbook: add one line to
modules/billing/RUNBOOKS.md: "customer paid but no credit → POST /api/admin/billing/webhook/replay {eventId}" (an event lost mid-handler is replayed by hand).
Tests: one unit or integration test per fix, reproducing the edge case (red before the fix, green after).
Scope: validated 2026-09-28
Small, independent hardening fixes in the billing and auth stack modules. They were found by formal models and property tests of the billing and signup flows. Each one is a rare edge case whose correct behaviour costs a few lines. No new persisted field, collection or flag.
Fixes (one commit each; red test first, then green):
stripe.subscriptions.retrievefails in thecheckout.session.completedhandler (billing.webhook.service.js, around the retrievecatch), throw instead ofreturn. Stripe then redelivers the event, instead of it being recorded as processed while the subscription row stays unlinked on the free plan. Update the unit test that pins the old behaviour.invoice.payment_failedandinvoice.payment_succeededhandlers, right afterfindByStripeSubscriptionId, addif (existing.status === 'canceled') return;. A late invoice event must not bring a canceled subscription back toactiveorpast_due.incrementMeter(billing.usage.service.js) must bill against the free plan when the subscription status is fail-closed (unpaid/paused/incomplete, the same list the quota gate uses inbilling.quota.service.js). Today the gate treats these as free while the meter still consumes the paid quota. Widen thefindPlanprojection to includestatusand reuse the gate's list; don't duplicate it.BillingExtraService.debitin the existingretryWithBackoff(modules/billing/lib/billing.retry.js). The debit is idempotent by key, so a retry cannot double-charge, and a transient DB error no longer makes the overflow free.creditGrant(billing.extraBalance.repository.js), before the write, return{ applied: false, reason: 'duplicate_grant' }whenexists({ 'ledger.refId': idempotencyKey })holds in any org. Today the guard is per org, so a grant whose target org changed between retries is credited twice.verifyEmail(auth controller), consume the token with one atomicfindOneAndUpdateon the unexpired token that sets it to null and marks the email verified. A null result returns the existing 400. Two concurrent verifications must not provision two workspaces.modules/billing/RUNBOOKS.md: "customer paid but no credit →POST /api/admin/billing/webhook/replay {eventId}" (an event lost mid-handler is replayed by hand).Tests: one unit or integration test per fix, reproducing the edge case (red before the fix, green after).
Scope: validated 2026-09-28