infrasynth-backend-kit/infrasynth/billing/gateways/mercadopago.py
2026-08-28 14:38:47 -05:00

134 lines
4.9 KiB
Python

import logging
from infrasynth.shared.enums import SubscriptionStatus
from .base import BasePaymentGateway, CheckoutSessionResult, WebhookResult
logger = logging.getLogger(__name__)
class MercadoPagoGateway(BasePaymentGateway):
"""MercadoPago payment gateway.
Configuration keys (read from ``PaymentGateway.config``):
- ``access_token`` (required for most operations)
"""
gateway_slug = "mercadopago"
STATUS_MAP = {
"authorized": SubscriptionStatus.ACTIVE,
"pending": SubscriptionStatus.PAST_DUE,
"paused": SubscriptionStatus.PAST_DUE,
"cancelled": SubscriptionStatus.CANCELLED,
}
def __init__(self, config: dict | None = None):
config = {str(key).lower(): value for key, value in (config or {}).items()}
self.access_token = config.get("access_token")
self._sdk = None
@property
def sdk(self):
if self._sdk is None:
import mercadopago
self._sdk = mercadopago.SDK(self.access_token)
return self._sdk
def create_checkout_session(self, plan, user, **kwargs) -> CheckoutSessionResult:
self._require_credentials()
preference = {
"items": [
{
"title": plan.name,
"quantity": 1,
"currency_id": plan.price_currency,
"unit_price": float(plan.price_amount),
}
],
"back_urls": {
"success": kwargs.get("success_url") or "https://example.com/success",
"failure": kwargs.get("cancel_url") or "https://example.com/cancel",
"pending": kwargs.get("cancel_url") or "https://example.com/cancel",
},
"auto_return": "approved",
"notification_url": kwargs.get("notification_url") or "",
"metadata": {"plan_slug": plan.slug, "user_id": str(getattr(user, "pk", ""))},
}
result = self.sdk.preference().create(preference)
if result.get("status") != 201:
raise ValueError(f"MercadoPago error: {result.get('response')}")
response = result["response"]
return CheckoutSessionResult(
session_id=response["id"],
checkout_url=response.get("init_point") or "",
client_secret="",
)
def handle_webhook(self, payload, headers) -> WebhookResult:
event_type = payload.get("type") or "payment"
data = payload.get("data") or payload
return WebhookResult(
event_type=event_type,
is_handled=True,
data=data,
)
def cancel_subscription(self, subscription) -> bool:
self._require_credentials()
if not subscription.external_id:
return False
result = self.sdk.preapproval().update(subscription.external_id, {"status": "cancelled"})
return result.get("status") in (200, 201)
def sync_subscription(self, subscription) -> dict:
self._require_credentials()
if not subscription.external_id:
return {}
result = self.sdk.preapproval().get(subscription.external_id)
if result.get("status") != 200:
return {}
data = result.get("response") or {}
raw_status: str | None = data.get("status") if isinstance(data, dict) else None
return {
"status": self.STATUS_MAP.get(raw_status or "", raw_status),
"current_period_start": self._parse_datetime(data.get("date_created")),
"current_period_end": self._parse_datetime(data.get("next_payment_date")),
"trial_end": self._parse_datetime(data.get("trial_end_date")),
"cancel_at_period_end": data.get("auto_recurring", {}).get("end_date") is None
and data.get("status") == "cancelled",
"metadata": data.get("metadata") or {},
}
def get_invoice(self, invoice) -> dict:
self._require_credentials()
if not invoice.external_id:
return {}
result = self.sdk.payment().get(invoice.external_id)
if result.get("status") != 200:
return {}
data = result.get("response") or {}
return {
"external_id": data.get("id"),
"status": data.get("status"),
"amount": data.get("transaction_amount"),
"currency": (data.get("currency_id") or "USD").upper(),
"paid_at": self._parse_datetime(data.get("date_approved")),
"payment_method": data.get("payment_method_id") or "",
}
def health_check(self) -> bool:
return bool(self.access_token)
def _require_credentials(self) -> None:
if not self.access_token:
raise ValueError("MercadoPago access token not configured")
@staticmethod
def _parse_datetime(value):
if not value:
return None
from django.utils.dateparse import parse_datetime
return parse_datetime(value)