""" Paddle payment provider — checkout, webhook verification, subscription management. Exports the 5 functions that billing/routes.py dispatches to: - build_checkout_payload() - build_multi_item_checkout_payload() - cancel_subscription() - get_management_url() - handle_webhook() """ import json from paddle_billing import Client as PaddleClient from paddle_billing import Environment, Options from paddle_billing.Notifications import Secret, Verifier from ..core import config def _paddle_client() -> PaddleClient: """Create a Paddle SDK client.""" env = Environment.SANDBOX if config.PADDLE_ENVIRONMENT == "sandbox" else Environment.PRODUCTION return PaddleClient(config.PADDLE_API_KEY, options=Options(env)) class _WebhookRequest: """Minimal wrapper satisfying paddle_billing's Request Protocol.""" def __init__(self, body: bytes, headers): self.body = body self.headers = headers _verifier = Verifier(maximum_variance=300) def build_checkout_payload( price_id: str, custom_data: dict, success_url: str, ) -> dict: """Build JSON payload for a single-item Paddle.js overlay checkout.""" return { "items": [{"priceId": price_id, "quantity": 1}], "customData": custom_data, "settings": {"successUrl": success_url}, } def build_multi_item_checkout_payload( items: list[dict], custom_data: dict, success_url: str, ) -> dict: """Build JSON payload for a multi-item Paddle.js overlay checkout.""" return { "items": items, "customData": custom_data, "settings": {"successUrl": success_url}, } def cancel_subscription(provider_subscription_id: str) -> None: """Cancel a Paddle subscription at end of current billing period.""" from paddle_billing.Resources.Subscriptions.Operations import CancelSubscription paddle = _paddle_client() paddle.subscriptions.cancel( provider_subscription_id, CancelSubscription(effective_from="next_billing_period"), ) def get_management_url(provider_subscription_id: str) -> str: """Get the Paddle customer portal URL for updating payment method.""" paddle = _paddle_client() paddle_sub = paddle.subscriptions.get(provider_subscription_id) return paddle_sub.management_urls.update_payment_method def verify_webhook(payload: bytes, headers) -> bool: """Verify Paddle webhook signature. Returns True if valid or no secret configured.""" if not config.PADDLE_WEBHOOK_SECRET: return True try: return _verifier.verify( _WebhookRequest(payload, headers), Secret(config.PADDLE_WEBHOOK_SECRET), ) except (ConnectionRefusedError, ValueError): return False def parse_webhook(payload: bytes) -> dict: """Parse a Paddle webhook payload into a normalized event dict. Returns dict with keys: event_type, subscription_id, customer_id, user_id, supplier_id, plan, status, current_period_end, data, items. """ event = json.loads(payload) event_type = event.get("event_type", "") data = event.get("data") or {} custom_data = data.get("custom_data") or {} billing_period = data.get("current_billing_period") or {} return { "event_type": event_type, "subscription_id": data.get("id", ""), "customer_id": str(data.get("customer_id", "")), "user_id": custom_data.get("user_id"), "supplier_id": custom_data.get("supplier_id"), "plan": custom_data.get("plan", ""), "status": data.get("status", ""), "current_period_end": billing_period.get("ends_at"), "data": data, "items": data.get("items", []), "custom_data": custom_data, }