Previously `apply_subscription_event` committed the StripeEvent idempotency row before invoking the handler, then the handlers each committed their own mutations. If a handler raised mid-flight (transient DB error, network blip, race), the idempotency mark was already persisted — Stripe's retry would hit the IntegrityError branch and silently return False, and the subscription state would permanently desync from Stripe. Switch to a single atomic transaction: - Insert the StripeEvent + flush (catch IntegrityError on duplicate event_id). - Run the handler. - Commit on success; roll back the entire transaction on failure and re-raise. Drop the four `db.commit()` calls inside `_handle_*` so the outer caller owns commit. The webhook endpoint already lets exceptions propagate, so a 500 response now correctly tells Stripe to retry. Tests: three new regression cases in test_stripe_webhook_handler.py covering handler failure (no idempotency mark persisted), retry-after-failure success, and duplicate-event-id skip. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
11 KiB
11 KiB