104 lines
3.1 KiB
Python
104 lines
3.1 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""
|
|
Stripe Checkout service for billing credit top-ups.
|
|
Creates Checkout Sessions for redirect-based payment flow.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from modules.shared.configuration import APP_CONFIG
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Server-side allowed amounts in CHF - never trust client
|
|
ALLOWED_AMOUNTS_CHF = [10, 25, 50, 100, 250, 500]
|
|
|
|
|
|
def create_checkout_session(
|
|
mandate_id: str,
|
|
user_id: Optional[str],
|
|
amount_chf: float
|
|
) -> str:
|
|
"""
|
|
Create a Stripe Checkout Session for credit top-up.
|
|
|
|
Amount and currency are validated server-side. The client-provided amount
|
|
must match an allowed preset.
|
|
|
|
Args:
|
|
mandate_id: Target mandate ID
|
|
user_id: Target user ID (for PREPAY_USER) or None (for mandate pool)
|
|
amount_chf: Amount in CHF (must be in ALLOWED_AMOUNTS_CHF)
|
|
|
|
Returns:
|
|
Stripe Checkout Session URL for redirect
|
|
|
|
Raises:
|
|
ValueError: If amount is invalid
|
|
"""
|
|
import stripe
|
|
|
|
# Validate amount server-side
|
|
if amount_chf not in ALLOWED_AMOUNTS_CHF:
|
|
raise ValueError(
|
|
f"Invalid amount {amount_chf} CHF. Allowed: {ALLOWED_AMOUNTS_CHF}"
|
|
)
|
|
|
|
# Pin API version from config (match Stripe Dashboard)
|
|
api_version = APP_CONFIG.get("STRIPE_API_VERSION")
|
|
if api_version:
|
|
stripe.api_version = api_version
|
|
|
|
# Get secrets
|
|
secret_key = APP_CONFIG.get("STRIPE_SECRET_KEY") or APP_CONFIG.get("STRIPE_SECRET_KEY_SECRET")
|
|
if not secret_key:
|
|
raise ValueError("STRIPE_SECRET_KEY or STRIPE_SECRET_KEY_SECRET not configured")
|
|
|
|
stripe.api_key = secret_key
|
|
|
|
frontend_url = APP_CONFIG.get("APP_FRONTEND_URL", "https://nyla-int.poweron-center.net")
|
|
base_path = "/admin/billing"
|
|
success_url = f"{frontend_url.rstrip('/')}{base_path}?success=true&session_id={{CHECKOUT_SESSION_ID}}"
|
|
cancel_url = f"{frontend_url.rstrip('/')}{base_path}?canceled=true"
|
|
|
|
# Amount in cents for Stripe (CHF uses 2 decimal places)
|
|
amount_cents = int(round(amount_chf * 100))
|
|
|
|
metadata = {
|
|
"mandateId": mandate_id,
|
|
"amountChf": str(amount_chf),
|
|
}
|
|
if user_id:
|
|
metadata["userId"] = user_id
|
|
|
|
session = stripe.checkout.Session.create(
|
|
mode="payment",
|
|
line_items=[
|
|
{
|
|
"price_data": {
|
|
"currency": "chf",
|
|
"unit_amount": amount_cents,
|
|
"product_data": {
|
|
"name": "Guthaben aufladen",
|
|
"description": "AI Service Guthaben (CHF)",
|
|
},
|
|
},
|
|
"quantity": 1,
|
|
}
|
|
],
|
|
success_url=success_url,
|
|
cancel_url=cancel_url,
|
|
metadata=metadata,
|
|
)
|
|
|
|
if not session or not session.url:
|
|
raise ValueError("Stripe Checkout Session creation failed")
|
|
|
|
logger.info(
|
|
f"Created Stripe Checkout Session {session.id} for mandate {mandate_id}, "
|
|
f"amount {amount_chf} CHF"
|
|
)
|
|
|
|
return session.url
|