The payment system uses the Strategy Pattern with a Factory for provider instantiation. This decouples the payment router from specific provider implementations, making it easy to add new providers.
Router (payment.py)
└── PaymentService (payment.py)
└── PaymentProviderFactory (factory.py)
├── StripeProvider
├── PayPalProvider
├── MobilePayProvider (placeholder)
├── GooglePayProvider (placeholder)
└── ApplePayProvider (placeholder)
PAYMENT_PLANS in config.py.can_handle_payment_id(), so the system can auto-detect which provider handles a given payment ID without hardcoded logic.cpt:payment:{id}).All providers extend BasePaymentProvider (services/payment_providers/base.py):
| Method | Purpose |
|---|---|
create_payment_intent(plan, amount_cents, currency) |
Create payment session |
verify_payment(payment_id) |
Check payment status with provider API |
is_configured() |
Whether required env vars are set |
get_provider_name() |
Returns provider identifier string |
can_handle_payment_id(payment_id) |
Self-identification by payment ID format |
stripe_provider.py)pi_* (Payment Intent), cs_* (Checkout Session), pay_*payment_intent.succeeded, payment_intent.payment_failed, payment_intent.canceledstripe.Webhook.construct_event() with STRIPE_WEBHOOK_SECRETpaypal_provider.py)5O*, 3L*)sandbox (api.sandbox.paypal.com) or live (api.paypal.com)PAYMENT.CAPTURE.COMPLETED, CHECKOUT.ORDER.COMPLETED, CHECKOUT.ORDER.APPROVEDMobilePay (mobilepay_provider.py): is_configured() returns False. Payment ID prefix: mp_*.
Google Pay (googlepay_provider.py): is_configured() returns False. Payment ID prefix: gp_*.
Apple Pay (applepay_provider.py): is_configured() returns False. Payment ID prefix: ap_*.
1. POST /api/v1/payment/create-intent
├── Validate plan server-side (ignore client amount)
├── Get provider from PaymentProviderFactory
├── Provider creates payment intent
├── Store payment in Redis
└── Return: payment_id, client_secret/redirect_url
2. User completes payment on provider's page (Stripe Checkout / PayPal)
3. POST /api/v1/payment/verify
├── Find provider via can_handle_payment_id()
├── Provider verifies with its API
├── Update payment status in Redis
├── If succeeded: auto-generate receipt PDF
└── Return: status, plan, amount, receipt_url
4. GET /api/v1/payment/receipt/{payment_id}/pdf
├── Verify payment is succeeded
├── Check if PDF exists on disk (cache)
├── Generate PDF if not cached (WeasyPrint + Jinja2)
└── Return: PDF file download
| Endpoint | Method | Auth | Rate Limit | Purpose |
|---|---|---|---|---|
/api/v1/payment/create-intent |
POST | Optional | 10/min | Create payment |
/api/v1/payment/verify |
POST | Optional | 30/min | Verify status |
/api/v1/payment/receipt/{id} |
GET | Required | 30/min | Receipt metadata |
/api/v1/payment/receipt/{id}/pdf |
GET | Required | 10/min | Download PDF |
/api/v1/payment/webhook/stripe |
POST | None* | 100/min | Stripe webhooks |
/api/v1/payment/webhook/paypal |
POST | None | 100/min | PayPal webhooks |
/api/v1/payment/capture/paypal/{id} |
POST | Optional | 10/min | Capture PayPal |
*Stripe webhooks are verified via signature header, not JWT.
JWT_ACCESS_TOKEN_EXPIRE_MINUTES (default: 30)JWT_SECRET_KEY (generate with openssl rand -hex 32)slowapi with get_remote_address key functionevent = stripe.Webhook.construct_event(
payload=raw_body,
sig_header=stripe_signature_header,
secret=STRIPE_WEBHOOK_SECRET
)
Client cannot manipulate payment amounts. The server looks up the plan in PAYMENT_PLANS:
PAYMENT_PLANS = {
"basic": {"amount": 1900, "currency": "eur"}, # €19.00
"pro": {"amount": 2900, "currency": "eur"}, # €29.00
"expert": {"amount": 14900, "currency": "eur"} # €149.00
}
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
PAYPAL_MODE=sandbox # sandbox or live
PAYPAL_WEBHOOK_ID=... # Optional: for webhook verification
JWT_SECRET_KEY=... # openssl rand -hex 32
JWT_ALGORITHM=HS256
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
MOBILEPAY_CLIENT_ID= # GUID
MOBILEPAY_CLIENT_SECRET= # Base64
MOBILEPAY_SUBSCRIPTION_KEY= # Ocp-Apim-Subscription-Key
MOBILEPAY_MERCHANT_SALES_NUMBER=
GOOGLE_PAY_MERCHANT_ID= # From Google Pay Console
GOOGLE_PAY_ENVIRONMENT=TEST # TEST or PRODUCTION
APPLE_PAY_MERCHANT_ID= # merchant.com.yourdomain.app
APPLE_PAY_DOMAIN_NAME=
APPLE_PAY_PAYMENT_PROCESSING_CERT_PATH=
APPLE_PAY_MERCHANT_IDENTITY_CERT_PATH=
APPLE_PAY_PRIVATE_KEY_PATH=
app/templates/receipt.htmlstorage/receipts/receipt-{method}-{payment_id}.pdfverify_payment()services/payment_providers/new_provider.py extending BasePaymentProvidercreate_payment_intent, verify_payment, is_configured, get_provider_name, can_handle_payment_id)PaymentProviderFactory._providers in factory.pyPaymentMethod enum in models/payment.pyconfig.pyrouters/payment.py if needed