feat(billing): B-2.8 Stripe webhook handler (subscription lifecycle + idempotency)
Endpoint: POST /checkout/webhooks/stripe (CSRF-exempt; signature-verified) Handles 5 Stripe events: - checkout.session.completed -> create Subscription, activate user - customer.subscription.updated -> sync status + current_period_end - customer.subscription.deleted -> mark canceled - invoice.payment_succeeded -> recover from past_due if applicable - invoice.payment_failed -> mark past_due Idempotency via WebhookEvent table (Stripe ID dedup) and Subscription unique constraint on stripe_subscription_id (defends against duplicate deliveries with distinct event IDs). User resolution prefers stripe_customer_id (server-set, anti-tamper) over event metadata.dictia_user_id over customer_email (per B-2.7 review note). New tables created via db.create_all(): - subscription (FK user.id ondelete=SET NULL for Loi 25 art. 28.1) - webhook_event (idempotency ledger) CSRF exemption wired via src/billing/exempt_webhook_csrf(csrf) called from src/app.py after billing_bp registration. Tests: 17/17 pass via tests/_run_stripe_webhook_windows.py. Existing 25 B-2.7 + 21 TOTP + 22 WebAuthn + 21 OAuth + 16 email tests unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -61,8 +61,20 @@
|
||||
# - One recurring yearly Price (CAD, = monthly × 12 × 0.85)
|
||||
# For DictIA 8 and DictIA 16, also create a one-time Price for the setup fee.
|
||||
#
|
||||
# 5. Create a webhook endpoint (B-2.8) pointing at https://dictia.ca/webhooks/stripe
|
||||
# with at least the events: checkout.session.completed,
|
||||
# customer.subscription.created, customer.subscription.updated,
|
||||
# customer.subscription.deleted, invoice.payment_failed.
|
||||
# Copy the signing secret into STRIPE_WEBHOOK_SECRET above.
|
||||
# 5. Create a webhook endpoint (B-2.8) pointing at
|
||||
# https://your-domain.example/checkout/webhooks/stripe
|
||||
# (the route lives under the /checkout/* prefix; CSRF-exempt; signature-
|
||||
# verified via STRIPE_WEBHOOK_SECRET below).
|
||||
#
|
||||
# Subscribe at minimum to these 5 events (the only ones the handler
|
||||
# processes; all others are acknowledged with 200 + ignored):
|
||||
# - checkout.session.completed (creates Subscription row, sets
|
||||
# User.subscription_status='active')
|
||||
# - customer.subscription.updated (status / current_period_end sync)
|
||||
# - customer.subscription.deleted (marks status='canceled')
|
||||
# - invoice.payment_succeeded (renewal touch; recovers past_due)
|
||||
# - invoice.payment_failed (marks status='past_due')
|
||||
#
|
||||
# Copy the signing secret (whsec_...) into STRIPE_WEBHOOK_SECRET above.
|
||||
# Without that secret, the webhook endpoint returns 400 invalid_signature
|
||||
# on every delivery (Stripe will retry for up to 30 days).
|
||||
|
||||
Reference in New Issue
Block a user