# Webhook de notificación de pagos — Implementación Laravel ## Contexto El sistema **soft_usite** (Go/Fiber) envía una notificación HTTP `POST` a una URL configurable cada vez que se confirma un pago mediante **Bold** o **dLocal**. El proveedor SaaS debe exponer un endpoint en Laravel para recibirla. --- ## 1. Ruta ``` POST /api/pagos/notificacion ``` - Registrar en `routes/api.php`. - Excluir del middleware CSRF (`VerifyCsrfToken` no aplica en rutas `api`, pero verificar que no haya middleware adicional que lo requiera). --- ## 2. Autenticación El sistema envía un header de API key configurable (nombre y valor definidos en `saas_api_configs`). Por defecto el header es `X-API-Key`. | Variable `.env` | Descripción | |---|---| | `WEBHOOK_SECRET` | Valor secreto que debe coincidir con el configurado en soft_usite | | `WEBHOOK_HEADER` | Nombre del header (default: `X-API-Key`) | Si el header no coincide → responder `401 Unauthorized`. --- ## 3. Body JSON recibido ```json { "contrato_id": 42, "referencia": "contrato-42", "email": "cliente@ejemplo.com", "monto": 150000.00, "moneda": "COP", "fuente": "bold" } ``` | Campo | Tipo | Descripción | |---|---|---| | `contrato_id` | `integer` | ID del contrato en soft_usite | | `referencia` | `string` | Formato `"contrato-{id}"` | | `email` | `string` | Email del pagador | | `monto` | `float` | Monto pagado | | `moneda` | `string` | ISO 4217: `COP`, `USD`, etc. | | `fuente` | `string` | `"bold"` o `"dlocal"` | --- ## 4. Lógica a implementar 1. **Validar** el body con las reglas mínimas (`contrato_id` required integer, `referencia` required string, etc.). 2. **Idempotencia:** buscar en `webhook_logs` si ya existe un registro con la misma `referencia` + `fuente` procesado. Si existe → responder `200 {"ok": true}` sin reprocesar. 3. **Guardar log** en tabla `webhook_logs` con todos los campos más `ip` y `raw_body`. 4. **Buscar el contrato** local (o el recurso equivalente) por `contrato_id`. 5. **Marcar como pagado:** `pago_confirmado = true`, `fecha_pago = now()`. 6. **Disparar evento/job** extensible: `PagoConfirmadoEvent` o `ProcesarPagoJob` — envío de correo, activación de cuenta, etc. --- ## 5. Respuesta Siempre responder `HTTP 200` con `{"ok": true}`, incluso ante errores internos. Los errores deben loguearse con `Log::error()` sin exponer detalles en la respuesta, para evitar reintentos innecesarios desde el sistema externo. --- ## 6. Tabla `webhook_logs` ```php Schema::create('webhook_logs', function (Blueprint $table) { $table->id(); $table->unsignedInteger('contrato_id'); $table->string('referencia')->index(); $table->string('email')->nullable(); $table->decimal('monto', 12, 2)->nullable(); $table->string('moneda', 10)->nullable(); $table->string('fuente', 20)->nullable(); // bold | dlocal $table->string('ip', 45)->nullable(); $table->text('raw_body')->nullable(); $table->boolean('procesado')->default(false); $table->timestamps(); $table->unique(['referencia', 'fuente']); // idempotencia }); ``` --- ## 7. Archivos a generar | Archivo | Descripción | |---|---| | `app/Http/Controllers/Api/WebhookPagoController.php` | Controlador principal | | `app/Models/WebhookLog.php` | Modelo Eloquent | | `database/migrations/xxxx_create_webhook_logs_table.php` | Migración | | `app/Events/PagoConfirmado.php` | Evento (opcional pero recomendado) | | `app/Listeners/ProcesarPagoConfirmado.php` | Listener del evento | | `routes/api.php` | Registro de la ruta | | `.env.example` | Variables `WEBHOOK_SECRET` y `WEBHOOK_HEADER` | --- ## 8. Variables `.env.example` ```env WEBHOOK_SECRET=tu_clave_secreta_aqui WEBHOOK_HEADER=X-API-Key ``` --- ## 9. Notas de seguridad - **No** exponer detalles de error en la respuesta JSON. - El valor de `WEBHOOK_SECRET` debe tener al menos 32 caracteres aleatorios. - Registrar siempre la IP de origen (`$request->ip()`) en el log. - No procesar el pago si el log ya existe (idempotencia por `referencia + fuente`). - Comparar el API key con `hash_equals()` para evitar timing attacks.