149 lines
5.9 KiB
Python
149 lines
5.9 KiB
Python
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, **kwargs) -> CheckoutSessionResult:
|
|
if not self.public_key:
|
|
raise ValueError("Wompi public key not configured")
|
|
payload = {
|
|
"name": plan.name,
|
|
"amount_in_cents": int(round(float(plan.price_amount) * 100)),
|
|
"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)
|