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 | Actualizar portal_payments; recibo por reconciliacion manual |
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 /backend/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->>BE: Actualizar portal_payments (status=APPROVED)
Note over BE,CC: Recibo queda para reconciliacion manual
else status == rejected
BE->>BE: Actualizar portal_payments (status=REJECTED)
endConfigurar Webhook en MercadoPago
- Panel MercadoPago -> Tus integraciones -> Webhooks
- URL:
https://api.bautista.com/backend/portal/pagos/webhook?tenant_id=1&sucursal_id=1&token=webhook-token - Eventos a escuchar:
payment.createdpayment.updated
La URL del webhook apunta al backend compartido, no a la instancia Docker del tenant. Incluye tenant_id, sucursal_id y token como query params para resolver y validar el contexto.
Resolucion de Tenant
El webhook no tiene JWT. La resolucion usa los query params de la URL registrada y luego portal_payments:
- MercadoPago llama la URL con
tenant_id,sucursal_idytoken - Backend valida el token configurado
- MercadoPago envia
external_reference(nuestropayment_id) - Se busca en
portal_paymentsporexternal_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 y actualizar
portal_paymentsen 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/backend/portal/pagos/webhook?tenant_id=1&sucursal_id=1&token=webhook-token' \
-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 que
portal_paymentsse actualiza una sola vez - 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 - Actualizacion de
portal_payments.status; conciliacion posterior registrarecibo_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