131 lines
4.0 KiB
Markdown
131 lines
4.0 KiB
Markdown
# 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.
|