feat: Query Runner — editor SQL con historial, exportar CSV/JSON, árbol de tablas
This commit is contained in:
@@ -0,0 +1,637 @@
|
||||
# Integración Bold — Guía completa
|
||||
|
||||
Guía de referencia para integrar Bold (pasarela de pagos colombiana) en cualquier sistema backend.
|
||||
Basada en experiencia real de producción con PHP; los conceptos aplican a cualquier lenguaje.
|
||||
|
||||
---
|
||||
|
||||
## Índice
|
||||
|
||||
1. [Credenciales y modos](#1-credenciales-y-modos)
|
||||
2. [Crear un Payment Link (API)](#2-crear-un-payment-link-api)
|
||||
3. [Consultar estado de un link](#3-consultar-estado-de-un-link)
|
||||
4. [Callback URL (retorno del usuario)](#4-callback-url-retorno-del-usuario)
|
||||
5. [Webhook — configuración](#5-webhook--configuración)
|
||||
6. [Webhook — verificación de firma](#6-webhook--verificación-de-firma)
|
||||
7. [Webhook — procesamiento del evento](#7-webhook--procesamiento-del-evento)
|
||||
8. [Estrategia de referencias múltiples](#8-estrategia-de-referencias-múltiples)
|
||||
9. [Idempotencia](#9-idempotencia)
|
||||
10. [Esquema de base de datos](#10-esquema-de-base-de-datos)
|
||||
11. [Flujo completo de extremo a extremo](#11-flujo-completo-de-extremo-a-extremo)
|
||||
12. [Errores frecuentes y soluciones](#12-errores-frecuentes-y-soluciones)
|
||||
|
||||
---
|
||||
|
||||
## 1. Credenciales y modos
|
||||
|
||||
Bold maneja dos entornos que se diferencian **únicamente por la API key**; el endpoint es el mismo.
|
||||
|
||||
| Parámetro | Descripción |
|
||||
|--------------------|-----------------------------------------------------------|
|
||||
| `bold_api_key` | Clave de la API (diferente para test y producción) |
|
||||
| `bold_secret_key` | Clave secreta para verificar la firma del webhook |
|
||||
| `bold_mode` | `"test"` / `"production"` (para lógica propia del sistema)|
|
||||
|
||||
> **Importante:** En modo **test** la clave secreta del webhook es una cadena vacía `""`.
|
||||
> En producción se usa la clave real del panel de Bold.
|
||||
|
||||
### Obtener las credenciales
|
||||
|
||||
1. Ingresa al panel de Bold → **Configuración → Integraciones → API**.
|
||||
2. Copia la **API Key** y la **Secret Key**.
|
||||
3. Para el webhook, registra tu URL en **Configuración → Webhooks**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Crear un Payment Link (API)
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
POST https://integrations.api.bold.co/online/link/v1
|
||||
```
|
||||
|
||||
### Headers
|
||||
|
||||
```
|
||||
Authorization: x-api-key {bold_api_key}
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
### Payload JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"amount_type": "CLOSE",
|
||||
"amount": {
|
||||
"currency": "COP",
|
||||
"total_amount": 25000
|
||||
},
|
||||
"description": "Descripción del producto o servicio",
|
||||
"callback_url": "https://tudominio.com/pago_exitoso.php",
|
||||
"payer_email": "cliente@ejemplo.com",
|
||||
"reference": "TU-REF-12345-1746123456"
|
||||
}
|
||||
```
|
||||
|
||||
> **Moneda COP:** Bold no usa centavos. `total_amount: 25000` equivale a $25.000 pesos.
|
||||
|
||||
#### Campos clave
|
||||
|
||||
| Campo | Tipo | Descripción |
|
||||
|----------------|---------|-----------------------------------------------------------------------------|
|
||||
| `amount_type` | string | `"CLOSE"` = monto fijo. `"OPEN"` = el usuario puede ingresar el monto. |
|
||||
| `total_amount` | int | Valor en pesos COP (sin centavos). |
|
||||
| `callback_url` | string | URL a la que Bold redirige al usuario tras el pago. |
|
||||
| `payer_email` | string | Pre-rellena el email en la pasarela. |
|
||||
| `reference` | string | **Tu referencia personalizada.** Llega al webhook. Max 60 chars, alfanumérico + guiones. |
|
||||
|
||||
### Respuesta exitosa (`HTTP 200`)
|
||||
|
||||
```json
|
||||
{
|
||||
"payload": {
|
||||
"payment_link": "LNK_abc123xyz",
|
||||
"url": "https://checkout.bold.co/payment/LNK_abc123xyz"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `payment_link` → ID interno de Bold (formato `LNK_xxx`). Guardarlo en tu base de datos.
|
||||
- `url` → URL a la que debes redirigir al usuario.
|
||||
|
||||
### Ejemplo en PHP
|
||||
|
||||
```php
|
||||
$payload = [
|
||||
'amount_type' => 'CLOSE',
|
||||
'amount' => ['currency' => 'COP', 'total_amount' => 25000],
|
||||
'description' => 'Inscripción al evento',
|
||||
'callback_url' => 'https://tudominio.com/pago_exitoso.php',
|
||||
'payer_email' => $email,
|
||||
'reference' => 'ERA-' . $usuarioId . '-' . time(),
|
||||
];
|
||||
|
||||
$ch = curl_init('https://integrations.api.bold.co/online/link/v1');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: x-api-key ' . $apiKey,
|
||||
'Content-Type: application/json',
|
||||
],
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
// Manejar error
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
$linkId = $data['payload']['payment_link'];
|
||||
$paymentUrl = $data['payload']['url'];
|
||||
|
||||
header('Location: ' . $paymentUrl);
|
||||
exit;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Consultar estado de un link
|
||||
|
||||
Útil si el webhook no llegó o como verificación extra en el callback.
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
GET https://integrations.api.bold.co/online/link/v1/{payment_link}
|
||||
```
|
||||
|
||||
### Headers
|
||||
|
||||
```
|
||||
Authorization: x-api-key {bold_api_key}
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
### Respuesta relevante
|
||||
|
||||
```json
|
||||
{
|
||||
"payload": {
|
||||
"payment_link": "LNK_abc123xyz",
|
||||
"status": "ACTIVE",
|
||||
"transactions": [
|
||||
{
|
||||
"status": "APPROVED",
|
||||
"payment_method": "CARD",
|
||||
"amount": 25000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Callback URL (retorno del usuario)
|
||||
|
||||
Cuando el usuario termina (o abandona) el pago, Bold lo redirige a tu `callback_url` con parámetros GET.
|
||||
|
||||
> **Advertencia:** Bold puede usar distintos nombres de parámetro según la versión del checkout.
|
||||
> **Nunca confíes en un único parámetro.** Revisa todos los posibles:
|
||||
|
||||
```
|
||||
bold-order-id, order_id, payment_link, payment_link_id, reference, id
|
||||
```
|
||||
|
||||
### Estrategia robusta en PHP
|
||||
|
||||
```php
|
||||
// Recolectar todos los candidatos posibles
|
||||
$candidateRefs = [];
|
||||
foreach (['bold-order-id', 'order_id', 'payment_link', 'reference', 'id'] as $key) {
|
||||
$val = trim((string)($_GET[$key] ?? ''));
|
||||
if ($val !== '') {
|
||||
$candidateRefs[] = $val;
|
||||
}
|
||||
}
|
||||
// Agregar lo guardado en sesión antes de la redirección
|
||||
if (!empty($_SESSION['bold_order_id'])) {
|
||||
$candidateRefs[] = $_SESSION['bold_order_id'];
|
||||
}
|
||||
$candidateRefs = array_values(array_unique(array_filter($candidateRefs)));
|
||||
|
||||
// Buscar usuario por cualquiera de las referencias
|
||||
$usuario = null;
|
||||
foreach ($candidateRefs as $ref) {
|
||||
$usuarioId = buscarUsuarioIdPorReferencia($pdo, $ref);
|
||||
if ($usuarioId) {
|
||||
// Cargar el usuario y salir del loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Respaldo: usar ID de sesión si no se encontró por referencia
|
||||
if (!$usuario && !empty($_SESSION['usuario_id'])) {
|
||||
// Cargar por $_SESSION['usuario_id']
|
||||
}
|
||||
```
|
||||
|
||||
> **Nota crítica:** La callback se dispara **antes** del webhook. No es seguro marcar un pago
|
||||
> como aprobado solo por el callback; el origen de verdad es el **webhook**.
|
||||
> En el callback solo confirma que el usuario regresó y muestra una pantalla de "procesando".
|
||||
|
||||
---
|
||||
|
||||
## 5. Webhook — configuración
|
||||
|
||||
### Registrar la URL en Bold
|
||||
|
||||
Panel de Bold → **Configuración → Webhooks** → agregar URL:
|
||||
|
||||
```
|
||||
https://tudominio.com/webhook.php
|
||||
```
|
||||
|
||||
### Eventos disponibles
|
||||
|
||||
| Evento | Descripción |
|
||||
|------------------|------------------------------------|
|
||||
| `SALE_APPROVED` | Pago aprobado exitosamente |
|
||||
| `SALE_REJECTED` | Pago rechazado por la entidad |
|
||||
| `SALE_REVERSED` | Reverso / devolución |
|
||||
| `CHARGEBACK` | Contracargo iniciado |
|
||||
|
||||
Para la mayoría de casos solo necesitas `SALE_APPROVED`.
|
||||
|
||||
### Requisitos del receptor
|
||||
|
||||
- Debe responder **HTTP 200** en **menos de 2 segundos**.
|
||||
- Si la respuesta demora más, Bold reintentará el envío.
|
||||
- La URL debe ser HTTPS en producción.
|
||||
|
||||
---
|
||||
|
||||
## 6. Webhook — verificación de firma
|
||||
|
||||
Bold firma cada notificación con HMAC-SHA256. El header es `x-bold-signature`.
|
||||
|
||||
### Algoritmo de verificación
|
||||
|
||||
```
|
||||
1. Leer el body RAW (antes de hacer cualquier json_decode)
|
||||
2. Codificar en Base64: encoded = base64(rawBody)
|
||||
3. Calcular HMAC: computed = HMAC-SHA256(encoded, secretKey) → hexadecimal
|
||||
4. Comparar con el header x-bold-signature (timing-safe)
|
||||
```
|
||||
|
||||
> **Modo test:** la `secretKey` es `""` (cadena vacía).
|
||||
> **Modo producción:** usa la clave del panel de Bold.
|
||||
|
||||
### Implementación PHP
|
||||
|
||||
```php
|
||||
$rawBody = (string) file_get_contents('php://input');
|
||||
$signatureHeader = $_SERVER['HTTP_X_BOLD_SIGNATURE'] ?? '';
|
||||
|
||||
// Responder 200 ANTES de todo procesamiento
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json');
|
||||
header('Connection: close');
|
||||
$body = '{"ok":true}';
|
||||
header('Content-Length: ' . strlen($body));
|
||||
echo $body;
|
||||
if (function_exists('fastcgi_finish_request')) {
|
||||
fastcgi_finish_request();
|
||||
} else {
|
||||
flush();
|
||||
}
|
||||
|
||||
// --- A partir de aquí el cliente ya recibió 200 ---
|
||||
|
||||
if ($signatureHeader !== '') {
|
||||
$keyForHmac = ($boldMode === 'production') ? $secretKey : '';
|
||||
$encoded = base64_encode($rawBody);
|
||||
$computed = hash_hmac('sha256', $encoded, $keyForHmac);
|
||||
|
||||
if (!hash_equals($computed, $signatureHeader)) {
|
||||
// Firma inválida: loguear y salir
|
||||
exit;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Webhook — procesamiento del evento
|
||||
|
||||
### Estructura del payload JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "notif_uuid_unico",
|
||||
"type": "SALE_APPROVED",
|
||||
"subject": "TXN_xyz789",
|
||||
"data": {
|
||||
"payment_id": "TXN_xyz789",
|
||||
"amount": {
|
||||
"total": 25000,
|
||||
"currency": "COP"
|
||||
},
|
||||
"payer_email": "cliente@ejemplo.com",
|
||||
"metadata": {
|
||||
"reference": "ERA-42-1746123456"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Campos a extraer
|
||||
|
||||
| Campo | Ruta en JSON | Descripción |
|
||||
|----------------------------|---------------------------------------------|------------------------------------------|
|
||||
| `notification_id` | `event.id` | ID único de la notificación (idempotencia) |
|
||||
| `tipo` | `event.type` | Tipo de evento |
|
||||
| `payment_id` | `event.data.payment_id` o `event.subject` | ID de la transacción Bold |
|
||||
| `referencia` | `event.data.metadata.reference` | Tu referencia personalizada |
|
||||
| `payer_email` | `event.data.payer_email` | Email del pagador |
|
||||
| `amount` | `event.data.amount.total` | Monto en pesos COP |
|
||||
|
||||
### Flujo de procesamiento
|
||||
|
||||
```php
|
||||
$event = json_decode($rawBody, true);
|
||||
$notificationId = $event['id'] ?? '';
|
||||
$tipo = $event['type'] ?? '';
|
||||
$data = $event['data'] ?? [];
|
||||
|
||||
$paymentId = $data['payment_id'] ?? ($event['subject'] ?? '');
|
||||
$referencia = $data['metadata']['reference'] ?? '';
|
||||
$email = $data['payer_email'] ?? '';
|
||||
|
||||
// Solo procesar ventas aprobadas
|
||||
if ($tipo !== 'SALE_APPROVED') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verificar idempotencia (ver sección 9)
|
||||
// Resolver usuario (ver sección 8)
|
||||
// Marcar pago en la base de datos
|
||||
// Enviar correo de confirmación
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Estrategia de referencias múltiples
|
||||
|
||||
**Problema real:** Bold puede enviar en el webhook un `payment_id` diferente al `LNK_xxx`
|
||||
original del payment link. Si solo guardas `bold_order_id`, la búsqueda falla.
|
||||
|
||||
**Solución:** tabla de historial de referencias. Cada vez que aparece una referencia nueva
|
||||
asociada a un usuario, se registra.
|
||||
|
||||
### Tabla `usuarios_referencias_pago`
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `usuarios_referencias_pago` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`usuario_id` INT NOT NULL,
|
||||
`referencia` VARCHAR(120) NOT NULL,
|
||||
`tipo` VARCHAR(40) NOT NULL DEFAULT 'desconocida',
|
||||
`origen` VARCHAR(40) NOT NULL DEFAULT 'sistema',
|
||||
`creado_en` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `uq_referencia` (`referencia`),
|
||||
INDEX `idx_usuario` (`usuario_id`),
|
||||
CONSTRAINT `fk_ref_usuario`
|
||||
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
### Cuándo registrar referencias
|
||||
|
||||
| Momento | Referencias a guardar | `tipo` |
|
||||
|----------------------|--------------------------------------------------------|------------------|
|
||||
| Al crear el link | `LNK_xxx` (payment_link) | `payment_link` |
|
||||
| Al crear el link | `ERA-{id}-{ts}` (tu referencia) | `referencia_era` |
|
||||
| En el callback URL | Todos los params GET de Bold | `callback` |
|
||||
| En el webhook | `payment_id` del evento | `webhook_payment_id` |
|
||||
| En el webhook | `metadata.reference` del evento | `webhook_referencia` |
|
||||
|
||||
### Función de búsqueda por cualquier referencia
|
||||
|
||||
```php
|
||||
function buscarUsuarioIdPorReferencia(PDO $pdo, string $referencia): ?int {
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT usuario_id
|
||||
FROM usuarios_referencias_pago
|
||||
WHERE referencia = ?
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([trim($referencia)]);
|
||||
$row = $stmt->fetchColumn();
|
||||
return $row ? (int)$row : null;
|
||||
}
|
||||
```
|
||||
|
||||
### Cascada de búsqueda en el webhook
|
||||
|
||||
```php
|
||||
$usuarioId = null;
|
||||
|
||||
// 1. Por referencia personalizada ERA-{id}-{ts}
|
||||
if ($referencia !== '') {
|
||||
$usuarioId = buscarUsuarioIdPorReferencia($pdo, $referencia);
|
||||
}
|
||||
|
||||
// 2. Por payment_id de la transacción
|
||||
if (!$usuarioId && $paymentId !== '') {
|
||||
$usuarioId = buscarUsuarioIdPorReferencia($pdo, $paymentId);
|
||||
}
|
||||
|
||||
// 3. Por email (último recurso, solo si hay un único pendiente)
|
||||
if (!$usuarioId && $payerEmail !== '') {
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT id FROM usuarios
|
||||
WHERE email = ? AND estado_pago = "pendiente"
|
||||
ORDER BY fecha_registro DESC LIMIT 1
|
||||
');
|
||||
$stmt->execute([$payerEmail]);
|
||||
$row = $stmt->fetchColumn();
|
||||
$usuarioId = $row ? (int)$row : null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Idempotencia
|
||||
|
||||
Bold puede reenviar la misma notificación varias veces (reintentos ante timeouts).
|
||||
Debes garantizar que procesar la misma notificación dos veces no cause efectos duplicados
|
||||
(doble correo, doble registro de pago, etc.).
|
||||
|
||||
### Tabla `webhook_log`
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS `webhook_log` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`notification_id` VARCHAR(64) NOT NULL,
|
||||
`tipo` VARCHAR(30) NOT NULL,
|
||||
`payment_id` VARCHAR(64) DEFAULT NULL,
|
||||
`referencia` VARCHAR(120) DEFAULT NULL,
|
||||
`usuario_id` INT DEFAULT NULL,
|
||||
`procesado` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`recibido_en` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `uq_notification` (`notification_id`),
|
||||
INDEX `idx_usuario` (`usuario_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
### Patrón de INSERT IGNORE
|
||||
|
||||
```php
|
||||
// Intentar insertar. Si ya existe (UNIQUE conflict) → rowCount() = 0 → duplicado
|
||||
$ins = $pdo->prepare('
|
||||
INSERT IGNORE INTO webhook_log (notification_id, tipo, payment_id, referencia)
|
||||
VALUES (?, ?, ?, ?)
|
||||
');
|
||||
$ins->execute([$notificationId, $tipo, $paymentId, $referencia]);
|
||||
|
||||
if ($ins->rowCount() === 0) {
|
||||
// Notificación ya procesada anteriormente
|
||||
exit;
|
||||
}
|
||||
|
||||
// Continúa procesamiento...
|
||||
|
||||
// Al finalizar, marcar como procesado
|
||||
$pdo->prepare('UPDATE webhook_log SET procesado=1, usuario_id=? WHERE notification_id=?')
|
||||
->execute([$usuarioId, $notificationId]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Esquema de base de datos
|
||||
|
||||
Tablas mínimas necesarias:
|
||||
|
||||
```sql
|
||||
-- Tabla principal de usuarios/registros
|
||||
CREATE TABLE `usuarios` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`email` VARCHAR(255) NOT NULL,
|
||||
`token` VARCHAR(64) NOT NULL UNIQUE,
|
||||
`bold_order_id` VARCHAR(80) DEFAULT NULL, -- LNK_xxx principal
|
||||
`estado_pago` ENUM('pendiente','pagado') NOT NULL DEFAULT 'pendiente',
|
||||
`fecha_registro` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Historial de todas las referencias de Bold asociadas al usuario
|
||||
CREATE TABLE `usuarios_referencias_pago` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`usuario_id` INT NOT NULL,
|
||||
`referencia` VARCHAR(120) NOT NULL,
|
||||
`tipo` VARCHAR(40) NOT NULL DEFAULT 'desconocida',
|
||||
`origen` VARCHAR(40) NOT NULL DEFAULT 'sistema',
|
||||
`creado_en` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `uq_referencia` (`referencia`),
|
||||
INDEX `idx_usuario` (`usuario_id`),
|
||||
CONSTRAINT `fk_ref_usuario`
|
||||
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Log de webhooks para idempotencia
|
||||
CREATE TABLE `webhook_log` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`notification_id` VARCHAR(64) NOT NULL,
|
||||
`tipo` VARCHAR(30) NOT NULL,
|
||||
`payment_id` VARCHAR(64) DEFAULT NULL,
|
||||
`referencia` VARCHAR(120) DEFAULT NULL,
|
||||
`usuario_id` INT DEFAULT NULL,
|
||||
`procesado` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`recibido_en` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `uq_notification` (`notification_id`),
|
||||
INDEX `idx_usuario` (`usuario_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Flujo completo de extremo a extremo
|
||||
|
||||
```
|
||||
Usuario llena formulario
|
||||
│
|
||||
▼
|
||||
[Tu backend] genera referencia personalizada:
|
||||
ERA-{usuarioId}-{timestamp}
|
||||
│
|
||||
▼
|
||||
POST → API Bold /online/link/v1
|
||||
│
|
||||
▼ respuesta LNK_xxx + URL
|
||||
[Guardar en DB]:
|
||||
- usuarios.bold_order_id = LNK_xxx
|
||||
- referencias_pago: LNK_xxx (tipo: payment_link)
|
||||
- referencias_pago: ERA-... (tipo: referencia_era)
|
||||
- sesión: bold_order_id, usuario_id
|
||||
│
|
||||
▼
|
||||
Redirigir usuario a URL Bold (checkout)
|
||||
│
|
||||
Usuario paga
|
||||
│
|
||||
┌────┴────────────────────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
callback_url (GET) webhook (POST) ← fuente de verdad
|
||||
Bold redirige al usuario Bold notifica el evento
|
||||
con params: bold-order-id, etc.
|
||||
│ │
|
||||
▼ ▼
|
||||
Mostrar pantalla "procesando" 1. Responder 200 INMEDIATAMENTE
|
||||
No marcar como pagado aún 2. Verificar firma HMAC
|
||||
3. Idempotencia (INSERT IGNORE)
|
||||
4. Resolver usuario por referencias
|
||||
5. UPDATE estado_pago = 'pagado'
|
||||
6. Enviar correo de confirmación
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Errores frecuentes y soluciones
|
||||
|
||||
### El webhook no llega
|
||||
|
||||
- Verificar que la URL esté registrada en el panel de Bold.
|
||||
- La URL debe ser **pública** (no localhost). Usar [ngrok](https://ngrok.com) para pruebas locales:
|
||||
```bash
|
||||
ngrok http 80
|
||||
# Registrar la URL https://xxxx.ngrok.io/webhook.php en Bold
|
||||
```
|
||||
- Confirmar que el servidor responde HTTP 200 en < 2 s.
|
||||
- Revisar el log de webhooks en el panel de Bold para ver reintentos.
|
||||
|
||||
### Firma inválida en modo test
|
||||
|
||||
El `secretKey` en modo test es `""` (cadena vacía). Asegúrate de usar eso, no la clave real.
|
||||
|
||||
```php
|
||||
$keyForHmac = ($boldMode === 'production') ? $secretKey : '';
|
||||
```
|
||||
|
||||
### No se encuentra el usuario en el webhook
|
||||
|
||||
El `payment_id` que llega en el webhook puede ser **diferente** al `LNK_xxx` creado con la API.
|
||||
Solución: usar la tabla de historial de referencias y la referencia personalizada `ERA-{id}-{ts}`.
|
||||
|
||||
### Bold reintenta y se procesan pagos dobles
|
||||
|
||||
Implementar idempotencia con `webhook_log` usando `INSERT IGNORE` sobre `notification_id`.
|
||||
|
||||
### HTTP 400 al crear el link
|
||||
|
||||
- Verificar que `total_amount` sea entero (no float).
|
||||
- `reference` no puede superar 60 caracteres ni contener caracteres especiales.
|
||||
- La `callback_url` debe ser una URL accesible públicamente.
|
||||
|
||||
### Error de timeout en curl
|
||||
|
||||
Bold tiene un timeout de respuesta. Si el servidor tarda más de 15 s, la llamada falla.
|
||||
Configurar `CURLOPT_TIMEOUT` en 15 y asegurar que la conexión a internet es estable.
|
||||
|
||||
---
|
||||
|
||||
## Referencias oficiales
|
||||
|
||||
- Documentación Bold: [https://developers.bold.co](https://developers.bold.co)
|
||||
- Panel Bold: [https://dashboard.bold.co](https://dashboard.bold.co)
|
||||
- API de links: `POST https://integrations.api.bold.co/online/link/v1`
|
||||
- Consultar link: `GET https://integrations.api.bold.co/online/link/v1/{id}`
|
||||
Reference in New Issue
Block a user