Files
dictia-public/config/env.stripe.example
Allison 64738bfd1f 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>
2026-04-28 08:41:03 -04:00

81 lines
4.0 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
###############################################################################
# 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).