import hashlib import hmac import json import logging import requests from infrasynth.shared.enums import SubscriptionStatus from .base import BasePaymentGateway, CheckoutSessionResult, WebhookResult logger = logging.getLogger(__name__) class WompiGateway(BasePaymentGateway): """Wompi (Colombia) payment gateway. Configuration keys (read from ``PaymentGateway.config``): - ``public_key`` (required for checkout sessions) - ``secret_key`` (required for voiding transactions) - ``webhook_secret`` (used to verify inbound webhook signatures) - ``environment`` ("sandbox" | "production", default "sandbox") - ``base_url`` (optional override) """ gateway_slug = "wompi" BASE_URLS = { "production": "https://production.wompi.co/v1", "sandbox": "https://sandbox.wompi.co/v1", } STATUS_MAP = { "APPROVED": SubscriptionStatus.ACTIVE, "PENDING": SubscriptionStatus.PAST_DUE, "VOIDED": SubscriptionStatus.CANCELLED, "DECLINED": SubscriptionStatus.PAST_DUE, "ERROR": SubscriptionStatus.PAST_DUE, } def __init__(self, config: dict | None = None): config = {str(key).lower(): value for key, value in (config or {}).items()} self.public_key = config.get("public_key") self.secret_key = config.get("secret_key") self.webhook_secret = config.get("webhook_secret") environment = config.get("environment", "sandbox") self.base_url = config.get("base_url") or self.BASE_URLS.get(environment, self.BASE_URLS["sandbox"]) self.timeout = config.get("timeout") or 30 def create_checkout_session(self, plan, user=None, *, tenant=None, **kwargs) -> CheckoutSessionResult: if not self.public_key: raise ValueError("Wompi public key not configured") payload = { "name": plan.name, "amount_in_cents": int(plan.price_amount), "currency": plan.price_currency.lower(), "single_use": True, "redirect_url": kwargs.get("success_url") or "https://example.com/success", "customer_email": getattr(user, "email", None) or None, } try: response = requests.post( f"{self.base_url}/payment_links", headers={"Authorization": f"Bearer {self.public_key}"}, json=payload, timeout=self.timeout, ) except requests.RequestException as exc: raise ValueError(f"Wompi request failed: {exc}") from exc if response.status_code >= 400: raise ValueError(f"Wompi error {response.status_code}: {response.text[:500]}") data = response.json().get("data") or {} return CheckoutSessionResult( session_id=data.get("id", ""), checkout_url=data.get("url", ""), client_secret="", ) def handle_webhook(self, payload, headers) -> WebhookResult: event_type = payload.get("event") or "transaction.updated" data = payload.get("data") or payload is_handled = True if self.webhook_secret: signature = headers.get("x-signature") or headers.get("X-Signature") or "" raw_body = json.dumps(payload, separators=(",", ":")) expected = hmac.new(self.webhook_secret.encode(), raw_body.encode(), hashlib.sha256).hexdigest() is_handled = hmac.compare_digest(signature, expected) return WebhookResult(event_type=event_type, is_handled=is_handled, data=data) def cancel_subscription(self, subscription) -> bool: if not self.secret_key or not subscription.external_id: return False try: response = requests.post( f"{self.base_url}/transactions/{subscription.external_id}/void", headers={"Authorization": f"Bearer {self.secret_key}"}, timeout=self.timeout, ) except requests.RequestException: logger.exception("Wompi void request failed for %s", subscription.external_id) return False return response.status_code == 200 def sync_subscription(self, subscription) -> dict: if not self.public_key or not subscription.external_id: return {} try: response = requests.get( f"{self.base_url}/transactions/{subscription.external_id}", headers={"Authorization": f"Bearer {self.public_key}"}, timeout=self.timeout, ) except requests.RequestException: logger.exception("Wompi transaction request failed for %s", subscription.external_id) return {} if response.status_code != 200: return {} data: dict = response.json().get("data") or {} status: str | None = data.get("status") return { "status": self.STATUS_MAP.get(status or "", status), "metadata": data.get("metadata") or {}, } def get_invoice(self, invoice) -> dict: if not self.public_key or not invoice.external_id: return {} try: response = requests.get( f"{self.base_url}/transactions/{invoice.external_id}", headers={"Authorization": f"Bearer {self.public_key}"}, timeout=self.timeout, ) except requests.RequestException: logger.exception("Wompi transaction request failed for %s", invoice.external_id) return {} if response.status_code != 200: return {} data = response.json().get("data") or {} return { "external_id": data.get("id"), "status": data.get("status"), "amount": (data.get("amount_in_cents") or 0) / 100, "currency": (data.get("currency") or "cop").upper(), "payment_method": data.get("payment_method", {}).get("type", ""), } def health_check(self) -> bool: return bool(self.public_key)