Appearance
Adapter: MercadoPago
Estado: Planificado
Resumen
Adapter de MercadoPago Checkout Pro que implementa la interfaz estandar PaymentGatewayAdapter. Utiliza el SDK oficial mercadopago/dx-php para interactuar con la API de MercadoPago.
Cuando el webhook confirma estado approved, el sistema crea el recibo en CtaCte automaticamente sin intervencion manual.
Configuracion
1. Obtener Credenciales
- Registrarse en MercadoPago
- Ir a Tus integraciones -> Credentials
- Obtener:
- Access Token (produccion)
- Access Token de prueba (sandbox)
- Webhook Secret (para validar webhooks)
2. Instalar SDK
bash
composer require mercadopago/dx-php3. Variables de Entorno
env
MERCADOPAGO_ACCESS_TOKEN=APP_USR-xxx
MERCADOPAGO_WEBHOOK_SECRET=secret4. Configurar Tenant
En ini.sistemas:
json
{
"payment_gateway": "mercadopago",
"payment_gateway_config": {
"access_token": "APP_USR-xxx",
"webhook_secret": "secret",
"environment": "production"
}
}Mapeo a la Interfaz Estandar
Campos
| Campo Estandar | MercadoPago | Notas |
|---|---|---|
checkoutUrl | init_point | URL del Checkout Pro |
gatewayPaymentId | preference.id | ID de la preferencia |
externalId | external_reference | Nuestra referencia (portal_payment.id) |
items | items[] | Array de items de la preferencia |
items[].amount | items[].unit_price | Precio unitario |
items[].description | items[].title | Titulo del item |
notificationUrl | notification_url | URL del webhook |
returnUrl | back_urls.success | URL post-pago exitoso |
backUrl | back_urls.failure | URL post-pago fallido |
payer.name | payer.name | Nombre del pagador |
payer.email | payer.email | Email del pagador |
payer.dniCuit | payer.identification.number | Documento del pagador |
currency | currency_id | "ARS" |
metadata | metadata | JSON libre |
Estados
| Estado Estandar | MercadoPago | Accion |
|---|---|---|
PENDING | pending | Esperar siguiente webhook |
ISSUED | N/A | MercadoPago no usa este estado |
APPROVED | approved | Crear recibo automatico en CtaCte |
REJECTED | rejected | Marcar como rechazado |
REFUNDED | refunded | Revertir acreditacion |
CANCELLED | cancelled | Marcar como cancelado |
Implementacion del Adapter
createPayment()
Mapeo: PaymentRequest -> MercadoPago Preference
php
public function createPayment(PaymentRequest $request): PaymentResponse
{
// 1. Crear Preference del SDK
$preference = new Preference();
$preference->items = [
[
'title' => $request->items[0]['description'],
'quantity' => 1,
'unit_price' => $request->amount,
'currency_id' => $request->currency,
]
];
// 2. URLs de retorno (relativas a la instancia Docker del tenant)
$preference->back_urls = [
'success' => $request->returnUrl,
'failure' => $request->backUrl,
'pending' => str_replace('/exito', '/pendiente', $request->returnUrl),
];
// 3. Referencia y notificacion
$preference->external_reference = $request->externalId;
$preference->notification_url = $request->notificationUrl;
// 4. Datos del pagador
$preference->payer = [
'name' => $request->payer->name,
'email' => $request->payer->email,
'identification' => [
'type' => 'DNI',
'number' => $request->payer->dniCuit,
],
];
// 5. Guardar preferencia
$result = $preference->save();
// 6. Mapear a PaymentResponse estandar
return new PaymentResponse(
gatewayPaymentId: $result->id,
checkoutUrl: $result->init_point, // <-- init_point -> checkoutUrl
status: 'pending',
finalAmount: $request->amount,
);
}processWebhook()
Mapeo: Webhook payload -> WebhookResult
MercadoPago envia un payload minimo con type y data.id. El adapter debe consultar la API para obtener el detalle completo.
php
public function processWebhook(array $headers, array $body): WebhookResult
{
// 1. Verificar tipo de notificacion
if ($body['type'] !== 'payment') {
throw new \InvalidArgumentException('Tipo de webhook no soportado');
}
// 2. Obtener detalle del pago desde la API
$paymentId = $body['data']['id'];
$payment = MercadoPago\Payment::find_by_id($paymentId);
// 3. Mapear a WebhookResult estandar
return new WebhookResult(
gatewayPaymentId: (string) $payment->id,
externalId: $payment->external_reference, // <-- nuestra referencia
status: $this->mapStatus($payment->status),
amount: (float) $payment->transaction_amount,
paymentDate: $payment->date_approved,
rawResponse: (array) $payment,
);
}validateWebhook()
MercadoPago usa firma HMAC-SHA256 con un formato de manifest especifico.
php
public function validateWebhook(array $headers, array $body): bool
{
// 1. Extraer headers
$signature = $headers['x-signature'] ?? '';
$requestId = $headers['x-request-id'] ?? '';
// 2. Parsear signature (formato: "ts=123456,v1=hash...")
$parts = [];
foreach (explode(',', $signature) as $part) {
[$key, $value] = explode('=', $part, 2);
$parts[$key] = $value;
}
// 3. Construir manifest
$dataId = $body['data']['id'] ?? '';
$manifest = "id:{$dataId};request-id:{$requestId};ts:{$parts['ts']};";
// 4. Calcular HMAC-SHA256
$computed = hash_hmac('sha256', $manifest, $this->webhookSecret);
// 5. Comparar
return hash_equals($computed, $parts['v1']);
}Formato del header x-signature:
ts=1234567890,v1=abcdef1234567890...Manifest:
id:{data.id};request-id:{x-request-id};ts:{timestamp};getPaymentStatus()
php
public function getPaymentStatus(string $externalId): PaymentStatusResult
{
$payment = MercadoPago\Payment::find_by_id($externalId);
return new PaymentStatusResult(
status: $this->mapStatus($payment->status),
amount: (float) $payment->transaction_amount,
paymentDate: $payment->date_approved,
rawResponse: (array) $payment,
);
}cancelPayment()
MercadoPago no soporta cancelacion directa de preferencias. La preferencia simplemente expira.
php
public function cancelPayment(string $externalId, string $reason): CancelResult
{
// MercadoPago: las preferencias expiran automaticamente.
// Se puede actualizar la preferencia para deshabilitarla.
return new CancelResult(
success: true,
status: 'cancelled',
);
}refundPayment()
php
public function refundPayment(string $externalId, RefundRequest $request): RefundResult
{
$refund = new MercadoPago\Refund();
$refund->payment_id = $externalId;
$refund->save();
return new RefundResult(
refundId: (string) $refund->id,
status: $refund->status, // "approved" o "rejected"
amount: (float) $refund->amount,
feeDetails: null,
);
}Flujo Completo
mermaid
sequenceDiagram
participant C as Cliente
participant FE as Frontend (Docker tenant)
participant BE as Backend (compartido)
participant MP as MercadoPago API
participant CC as CtaCte
C->>FE: Selecciona facturas, click "Pagar"
FE->>BE: POST /portal/pagos/iniciar
BE->>BE: PaymentGatewayFactory -> MercadoPagoAdapter
BE->>MP: Crear Preference (SDK)
MP-->>BE: preference_id + init_point
BE->>BE: Guardar portal_payments (status=PENDING)
BE-->>FE: {payment_id, redirect_url: init_point}
FE->>C: Redirige a MercadoPago Checkout Pro
C->>MP: Completa pago
MP-->>C: Redirige a back_urls.success
Note over MP,BE: Asincrono - webhook
MP->>BE: POST /portal/pagos/webhook
BE->>BE: validateWebhook() - HMAC-SHA256
BE->>MP: GET /v1/payments/{id}
MP-->>BE: status, amount, date, external_reference
BE->>BE: processWebhook() -> WebhookResult
alt status == approved
BE->>BE: Verificar idempotencia (external_id)
BE->>CC: Crear recibo automatico
BE->>BE: Actualizar portal_payments (status=APPROVED, recibo_id)
else status == rejected
BE->>BE: Actualizar portal_payments (status=REJECTED)
endConfigurar Webhook en MercadoPago
- Panel MercadoPago -> Tus integraciones -> Webhooks
- URL:
https://api.bautista.com/portal/pagos/webhook - Eventos a escuchar:
payment.createdpayment.updated
La URL del webhook apunta al backend compartido, no a la instancia Docker del tenant. Un solo webhook endpoint procesa pagos de todos los tenants.
Resolucion de Tenant
El webhook no tiene JWT ni contexto de tenant. La resolucion se hace via portal_payments:
- MercadoPago envia
external_reference(nuestropayment_id) - Se busca en
portal_paymentsporexternal_id - La fila contiene
tenant_idysucursal_id - Con esos datos se resuelve la DB y el schema del tenant
Ver ADR-011 en la arquitectura general.
URLs de Retorno
Las URLs se construyen usando la URL base de la instancia Docker del tenant:
success: {portal_url}/pagar/exito?payment_id=xxx
failure: {portal_url}/pagar/error?payment_id=xxx
pending: {portal_url}/pagar/pendiente?payment_id=xxxMercadoPago usa back_urls con tres destinos separados (success, failure, pending), a diferencia de otros gateways que pueden usar una unica return_url.
Particularidades
Timeout de Webhook
MercadoPago espera respuesta en menos de 5 segundos. Si el procesamiento es lento:
- Responder 200 OK inmediatamente
- Procesar webhook (creacion de recibo) en background
SDK vs API directa
Este adapter usa el SDK oficial mercadopago/dx-php en lugar de llamadas HTTP directas. El SDK maneja autenticacion, serialization y manejo de errores.
Preferencias vs Pagos
MercadoPago distingue entre:
- Preference (preferencia): la intencion de pago, creada por el adapter
- Payment (pago): el pago real, creado por MercadoPago cuando el cliente paga
El init_point pertenece a la preferencia. Los webhooks reportan sobre pagos.
Testing
Ambiente Sandbox
Usar Access Token de prueba en lugar del de produccion.
Tarjetas de prueba:
| Tarjeta | Numero | Resultado |
|---|---|---|
| Visa | 4509 9535 6623 3704 | Aprobado |
| Mastercard | 5031 7557 3453 0604 | Rechazado |
CVV: 123 | Vencimiento: 11/25
Simular Webhook
bash
curl -X POST https://api.bautista.com/portal/pagos/webhook \
-H "Content-Type: application/json" \
-H "x-signature: ts=123456,v1=hash_simulado" \
-H "x-request-id: test-id" \
-d '{"type": "payment", "data": {"id": "123456"}}'Verificar Idempotencia
- Enviar webhook con status
approved - Verificar creacion de recibo en CtaCte
- Enviar mismo webhook nuevamente
- Verificar que NO se crea recibo duplicado
Monitoreo
Logs importantes:
- Inicio de pago con
preference_idy monto - Recepcion de webhook con
payment_idy estado - Creacion automatica de recibo con
recibo_id - Validacion fallida de webhook (posible ataque)
- Errores de comunicacion con API de MercadoPago
- Errores de idempotencia (intento de doble acreditacion)
Recursos Oficiales
Ver tambien
- Arquitectura de Medios de Pago — Interfaz estandar y patron Adapter
- Pago TIC (PayPerTIC) — Adapter alternativo