Saltar al contenido principal

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/cotizaciones para 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

ColumnaTipoDescripción
idbigintPK
monedavarchar(3)ISO 4217 (USD, EUR, BRL, …)
fechadateFecha de la cotización
referencialdecimal(18,4) nullableGs/ME — valor oficial BCP, usar para SIFEN
compradecimal(18,4) nullableCompra minorista si BCP publica
ventadecimal(18,4) nullableVenta minorista si BCP publica
fuentevarchar(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:

  1. curl -L -A "Mozilla/5.0" https://www.bcp.gov.py/webapps/web/cotizacion/monedas > /tmp/bcp.html
  2. Inspeccionar nueva estructura.
  3. Ajustar BcpScraperService::parseReferencial() — XPath y offsets.
  4. Actualizar tests/Fixtures/bcp/cotizacion_monedas_sample.html.
  5. 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_cambio del 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