BCP — Cotizaciones diarias
Guía técnica del módulo App\Domains\Bcp que sincroniza las cotizaciones oficiales del Banco Central del Paraguay y las expone por API REST.
Estado: funcional — 26 monedas se cargan diariamente.
Contexto
Los DTEs multi-moneda requieren tipo_cambio expresado como Gs por unidad de moneda extranjera (Manual SIFEN v150 campo D018 dTiCam). El BCP publica esos valores diariamente pero no tiene API JSON pública — solo tablas HTML.
Este módulo:
- Scrapea el HTML oficial del BCP una vez por día (scheduler 08:15 America/Asuncion).
- Persiste las cotizaciones en la tabla
bcp_cotizaciones. - Expone los datos por
GET /api/bcp/cotizacionespara que cualquier cliente los consuma.
:::info Desacoplado de la emisión
No toca el flujo de emisión de DTEs. El cliente API sigue enviando tipo_cambio explícito. Este módulo solo sirve valores actualizados.
:::
Arquitectura
app/Domains/Bcp/
├── Models/BcpCotizacion.php # Eloquent
├── DTOs/CotizacionData.php # Value object
└── Services/
├── BcpScraperService.php # HTTP + parser HTML
└── BcpCotizacionService.php # Fachada del dominio
app/Console/Commands/BcpSyncRatesCommand.php # artisan bcp:sync-rates
app/Http/Controllers/Api/BcpController.php # GET /api/bcp/cotizaciones
database/migrations/2026_04_21_000000_create_bcp_cotizaciones_table.php
routes/console.php # Schedule::command('bcp:sync-rates')->dailyAt('08:15')
routes/api.php # Rutas /api/bcp/*
Tabla bcp_cotizaciones
| Columna | Tipo | Descripción |
|---|---|---|
id | bigint | PK |
moneda | varchar(3) | ISO 4217 (USD, EUR, BRL, …) |
fecha | date | Fecha de la cotización |
referencial | decimal(18,4) nullable | Gs/ME — valor oficial BCP, usar para SIFEN |
compra | decimal(18,4) nullable | Compra minorista si BCP publica |
venta | decimal(18,4) nullable | Venta minorista si BCP publica |
fuente | varchar(40) | bcp o bcp_minorista (extensible) |
Unique: (moneda, fecha, fuente) — permite updateOrCreate idempotente.
Fuentes scrapeadas
Referencial — todas las monedas (Gs/ME)
URL: https://www.bcp.gov.py/webapps/web/cotizacion/monedas
Parser busca la tabla #cotizacion-interbancaria con XPath. Monedas publicadas al 2026-04 (26 en total): USD, EUR, BRL, ARS, UYU, BOB, CLP, COP, PEN, JPY, GBP, CNY, CAD, AUD, CHF, MXN, SEK, NOK, DKK, SGD, HKD, TWD, NZD, AED, ZAR, XAU, XDR.
Fluctuante USD — compra/venta intraday
URL: https://www.bcp.gov.py/webapps/web/cotizacion/referencial-fluctuante
Solo USD. Se merge con el registro referencial (misma fecha + moneda).
:::info Política de compra/venta Todas las monedas tienen los 3 valores poblados:
- Referencial: valor oficial BCP — usar para SIFEN
tipo_cambio. - USD: compra/venta reales del mercado fluctuante (hay spread).
- Resto (25 monedas):
compra = venta = referencial(el BCP no publica compra/venta multi-moneda por HTTP).
Es la práctica estándar en Paraguay — el referencial BCP es la cotización oficial de referencia. Si necesitás spread real para otras monedas, se puede integrar una fuente secundaria.
La URL /cotizacion-minorista-del-tipo-de-cambio-nominal NO sirve: Cloudflare Challenge + solo informes mensuales en Excel con 1 mes de retraso.
:::
Uso
Sync manual
# Hoy
php artisan bcp:sync-rates
# Fecha específica
php artisan bcp:sync-rates --fecha=2026-04-20
Scheduler automático
// routes/console.php
Schedule::command('bcp:sync-rates')
->dailyAt('08:15')
->timezone('America/Asuncion')
->withoutOverlapping()
->runInBackground();
API REST
Listar última cotización de cada moneda:
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/api/bcp/cotizaciones
{
"data": [
{
"moneda": "USD",
"descripcion": "US Dollar",
"fecha": "2026-04-20",
"referencial": 6379.25,
"compra": null,
"venta": null,
"fuente": "bcp"
}
],
"meta": { "total": 26, "fuente": "bcp" }
}
Una moneda específica:
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/api/bcp/cotizaciones/USD
curl -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8000/api/bcp/cotizaciones/USD?fecha=2026-04-20"
curl -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:8000/api/bcp/cotizaciones/USD?fuente=bcp_minorista"
Desde código PHP
use App\Domains\Bcp\Services\BcpCotizacionService;
$service = app(BcpCotizacionService::class);
$usd = $service->ultima('USD');
$tipoCambio = $usd->referencial; // 6379.25
$todas = $service->listarUltimas();
$enFecha = $service->en('EUR', \Carbon\Carbon::parse('2026-04-15'));
Cache
Tag bcp.cotizaciones con TTL 1 hora sobre ultima y listarUltimas. Se invalida automáticamente en cada sincronizar().
Permisos
Permiso Spatie nuevo: bcp:read.
admin— total.operador— para consultar al emitir.lector— solo lectura.
Errores comunes
Tabla #cotizacion-interbancaria no encontrada
BCP cambió el HTML. Recuperación:
curl -L -A "Mozilla/5.0" https://www.bcp.gov.py/webapps/web/cotizacion/monedas > /tmp/bcp.html- Inspeccionar nueva estructura.
- Ajustar
BcpScraperService::parseReferencial()— XPath y offsets. - Actualizar
tests/Fixtures/bcp/cotizacion_monedas_sample.html. php artisan bcp:sync-rates.
La emisión de DTEs NO se ve afectada — módulo aislado.
Sin cotizaciones fines de semana / feriados
BCP no publica esos días. listarUltimas() devuelve la fecha más reciente disponible.
Fuera de scope
- No se pobla
tipo_cambiodel DTE automáticamente — sigue siendo responsabilidad del cliente. - No se scrapea histórico anual (solo cotización del día).
- No hay UI/frontend — se consume solo vía API.
- No se integran otras fuentes (BNF, Itacambios, XE).
Referencias
- Multi-moneda SIFEN — campo
tipo_cambioen DTEs. - Manual SIFEN v150 sección D1 — campos D015–D018.
docs/xsd/Monedas_v150.xsd— catálogo ISO 4217.- Sitio BCP: cotizaciones oficiales.