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>
81 lines
4.0 KiB
Plaintext
81 lines
4.0 KiB
Plaintext
###############################################################################
|
||
# Stripe — Checkout + Subscriptions (B-2.7 / B-2.8)
|
||
###############################################################################
|
||
#
|
||
# Required for the /checkout/<plan> flow and the /webhooks/stripe receiver.
|
||
# The application will boot without these — billing routes will redirect to
|
||
# /tarifs with a "contact info@dictia.ca" message until the keys are set.
|
||
#
|
||
# Get these from https://dashboard.stripe.com (CAD account)
|
||
# - Use sk_test_/pk_test_/whsec_test_ keys against the Stripe test mode for
|
||
# pre-prod. Switch to live keys ONLY after end-to-end CAD/TVQ rehearsal.
|
||
|
||
# STRIPE_SECRET_KEY=sk_test_... # or sk_live_...
|
||
# STRIPE_PUBLISHABLE_KEY=pk_test_... # used client-side; not strictly needed for hosted Checkout
|
||
# STRIPE_WEBHOOK_SECRET=whsec_... # for B-2.8 webhook signature verification
|
||
|
||
###############################################################################
|
||
# Price IDs — one per plan, period, and (for hardware plans) setup fee.
|
||
###############################################################################
|
||
#
|
||
# Format: price_xxxxxxxxxxxxxxxxxxxxxxxxxx
|
||
# Naming convention in this codebase: STRIPE_<PLAN>_<TYPE>
|
||
# PLAN = DICTIA_8 | DICTIA_16 | DICTIA_CLOUD
|
||
# TYPE = SETUP (one-time, hardware only) | MONTHLY | YEARLY
|
||
#
|
||
# Yearly Price = Monthly Price × 12 × 0.85 (15 % discount). Configure both
|
||
# Prices in the Stripe Dashboard for each plan.
|
||
|
||
# DictIA 8 (8-channel hardware bundle): 3 450 $ setup + 173 $/mo
|
||
# STRIPE_DICTIA_8_SETUP=price_xxx
|
||
# STRIPE_DICTIA_8_MONTHLY=price_xxx
|
||
# STRIPE_DICTIA_8_YEARLY=price_xxx
|
||
|
||
# DictIA 16 (16-channel hardware bundle): 5 750 $ setup + 201 $/mo
|
||
# STRIPE_DICTIA_16_SETUP=price_xxx
|
||
# STRIPE_DICTIA_16_MONTHLY=price_xxx
|
||
# STRIPE_DICTIA_16_YEARLY=price_xxx
|
||
|
||
# DictIA Cloud (SaaS-only, no hardware): 369 $/mo
|
||
# STRIPE_DICTIA_CLOUD_MONTHLY=price_xxx
|
||
# STRIPE_DICTIA_CLOUD_YEARLY=price_xxx
|
||
|
||
###############################################################################
|
||
# Required Stripe Dashboard configuration
|
||
###############################################################################
|
||
#
|
||
# 1. Activate CAD currency on the account (Settings → Account → Currencies).
|
||
#
|
||
# 2. Enable Stripe Tax with TPS (5 %) and TVQ (9.975 %) for Quebec
|
||
# (Tax → Settings → Tax registrations → Canada → Quebec).
|
||
# All Checkout Sessions are created with `automatic_tax: { enabled: true }`
|
||
# and `billing_address_collection: required` so Stripe computes taxes.
|
||
#
|
||
# 3. Enable Apple Pay + Google Pay
|
||
# (Settings → Payment methods → Apple Pay, Google Pay).
|
||
# Apple Pay requires verifying the dictia.ca domain via the Stripe-hosted
|
||
# `.well-known/apple-developer-merchantid-domain-association` file.
|
||
#
|
||
# 4. For each plan, create:
|
||
# - One recurring monthly Price (CAD, billing_scheme=per_unit)
|
||
# - 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://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).
|