04-Integrations / 04.02.Payment-Integration-API

04.02.Payment Integration API

04.02. Payment Integration API

Architecture Overview

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)

Key Design Decisions

Base Provider Interface

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

Provider Implementations

Stripe (stripe_provider.py)

PayPal (paypal_provider.py)

Placeholder Providers

MobilePay (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_*.

Payment Flow

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

API Endpoints

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.

Security

Authentication

Rate Limiting

Stripe Webhook Verification

event = stripe.Webhook.construct_event(
    payload=raw_body,
    sig_header=stripe_signature_header,
    secret=STRIPE_WEBHOOK_SECRET
)

Server-Side Amount Validation

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
}

Environment Variables

Required for Stripe

STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

Required for PayPal

PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
PAYPAL_MODE=sandbox          # sandbox or live
PAYPAL_WEBHOOK_ID=...        # Optional: for webhook verification

Required for JWT

JWT_SECRET_KEY=...           # openssl rand -hex 32
JWT_ALGORITHM=HS256
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30

Placeholder Providers (not yet active)

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=

Receipt PDF Generation

Adding a New Payment Provider

  1. Create services/payment_providers/new_provider.py extending BasePaymentProvider
  2. Implement all abstract methods (create_payment_intent, verify_payment, is_configured, get_provider_name, can_handle_payment_id)
  3. Add the provider to PaymentProviderFactory._providers in factory.py
  4. Add the payment method to PaymentMethod enum in models/payment.py
  5. Add environment variables to config.py
  6. Add webhook endpoint to routers/payment.py if needed

Current Limitations