Documentation Index Fetch the complete documentation index at: https://mintlify.com/MiguelNavas19/miapibcv/llms.txt
Use this file to discover all available pages before exploring further.
Architecture Overview
Mi API BCV uses a combination of web scraping and API integration to collect exchange rates from multiple Venezuelan banks. The system is built on Laravel 12 and follows clean architecture principles using the Strategy pattern.
Core Components
1. ScraperService
The ScraperService is the heart of the scraping system. It handles HTTP requests and HTML parsing.
app/Services/ScraperService.php
public function scrapeData ( string $url , string $banco )
{
$response = Http :: withoutVerifying () -> withHeaders ([
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' ,
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9' ,
'Accept-Language' => 'es-ES,es;q=0.9,en;q=0.8' ,
'Referer' => $url ,
]) -> get ( $url );
if ( $response -> status () !== 200 ) {
return null ;
}
$crawler = new Crawler ( $response -> body ());
return match ( $banco ) {
'banplus' => $this -> parseBanplusData ( $crawler ),
'bnc' => $this -> parseBNCData ( $crawler ),
'bcv' => $this -> parseBCVData ( $crawler ),
default => 0.00 ,
};
}
Key features :
Uses realistic browser headers to avoid blocking
Leverages Symfony DomCrawler for HTML parsing
Bank-specific parsing methods for different website structures
Returns null on failure, allowing the system to continue
2. Strategy Pattern
Each bank has its own strategy class implementing the UrlStrategy interface:
app/Interfaces/UrlStrategy.php
interface UrlStrategy
{
public function getValue () : float ;
}
This allows easy addition of new banks without modifying existing code.
Strategy Examples
BCV Strategy
BDV Strategy (API)
class UrlBcv implements UrlStrategy
{
protected $scraper ;
public function __construct ( $scraper )
{
$this -> scraper = $scraper ;
}
public function getValue () : float
{
$url = 'https://www.bcv.org.ve/' ;
$value = $this -> scraper -> scrapeData ( $url , 'bcv' );
return $value ;
}
}
BDV uses a JSON API endpoint instead of web scraping, demonstrating the flexibility of the strategy pattern.
3. UrlProviderService
This service acts as a factory for strategy instances:
app/Services/UrlProviderService.php
public function getStrategy ( string $identifier ) : UrlStrategy
{
return match ( $identifier ) {
'banplus' => new UrlBanplus ( new ScraperService ()),
'bnc' => new UrlBnc ( new ScraperService ()),
'bcv' => new UrlBcv ( new ScraperService ()),
'bdv' => new UrlBdv (),
default => new UrlBdv (),
};
}
4. FetchExchangeRates Command
The artisan command orchestrates the entire scraping process:
app/Console/Commands/FetchExchangeRates.php
protected $signature = 'rates:update' ;
protected $description = 'Consulta el BCV y otros bancos para actualizar tasas' ;
public function handle ()
{
$this -> info ( 'Iniciando actualización de tasas...' );
foreach ( $this -> banco as $banco ) {
try {
$this -> store ( $banco );
$this -> info ( "Banco { $banco } procesado." );
} catch ( \ Exception $e ) {
$this -> error ( "Error en { $banco }: " . $e -> getMessage ());
Log :: error ( "Fallo en cron para { $banco }: " . $e -> getMessage ());
}
}
$this -> info ( 'Tasas actualizadas exitosamente.' );
}
Error handling : If one bank fails, the command continues processing others.
Data Flow
Scheduled Trigger
Laravel scheduler triggers rates:update command at configured times (11 times daily). $schedules = [ '00:00' , '00:30' , '02:00' , '03:00' , '03:30' , '04:00' ,
'05:00' , '05:30' , '06:30' , '07:00' , '07:30' ];
foreach ( $schedules as $time ) {
Schedule :: command ( 'rates:update' ) -> dailyAt ( $time );
}
Strategy Selection
For each bank, the command gets the appropriate strategy from UrlProviderService.
Data Retrieval
The strategy calls getValue() which either:
Scrapes HTML using ScraperService (BCV, Banplus, BNC)
Fetches JSON from API (BDV)
HTML Parsing
For scraped sources, ScraperService uses CSS selectors to extract the rate: // BCV example
private function parseBCVData ( $crawler )
{
$element = $crawler -> filter ( '#dolar' );
$text = $element -> text ();
if ( preg_match ( '/USD \s + (. * )/' , $text , $matches )) {
if ( preg_match_all ( '/[0-9] + ,[0-9] + /' , $matches [ 1 ], $coincidencias )) {
$valor = trim ( $coincidencias [ 0 ][ 0 ]);
}
}
return $this -> cleanValue ( $valor );
}
Data Normalization
Raw values are cleaned and converted to float: private function cleanValue ( $value )
{
return ( float ) preg_replace ( '/[^0-9.]/' , '' , str_replace ( ',' , '.' , $value ));
}
Database Storage
Rates are stored in the reference_records table: $record = new ReferenceRecord ();
$record -> source = $banco ; // 'bcv', 'banplus', etc.
$record -> value = $value ; // 45.25
$record -> date = $today ; // '2026-03-04'
$record -> save ();
Cache Invalidation
A model observer automatically invalidates the cache when new records are saved: app/Observers/ReferenceObserver.php
public function saved ( ReferenceRecord $referenceRecord ) : void
{
Cache :: forget ( 'tasas_bancos_' . $referenceRecord -> date );
}
API Request Flow
When a client makes an API request:
Route Matching
Route :: get ( '/' , [ ScraperController :: class , 'show' ]);
Route :: get ( '/info/{date}/{source?}' , [ ScraperController :: class , 'getInfo' ]);
Cache Check
Controller checks if cached data exists: app/Http/Controllers/Api/ScraperController.php
public function show ( Request $request )
{
$cacheKey = 'tasas_bancos_' . now () -> toDateString ();
$records = Cache :: remember ( $cacheKey , 3600 , function () {
return ReferenceRecord :: where ( 'date' , now () -> toDateString ())
-> get ()
-> keyBy ( 'source' );
});
if ( $records -> isEmpty ()) {
return response () -> json ([ 'message' => 'Tasas no disponibles aún' ], 404 );
}
return response () -> json ([
'message' => 'Consulta exitosa' ,
... $records -> map ( fn ( $item ) => [
'value' => $item -> value ,
'date' => $item -> date
]) -> all ()
]);
}
Database Query (if cache miss)
If cache doesn’t exist, query the database and cache the result for 1 hour (3600 seconds).
Response Formatting
Data is transformed to a clean JSON structure with bank codes as keys.
Scraping Techniques
CSS Selector Targeting
Each bank uses specific selectors:
BCV : #dolar - ID selector for the dollar rate element
Banplus : .awb-news-ticker-link - Class selector for news ticker
BNC : .ItemSpace - Class selector, then text filtering
Regular Expression Parsing
After selecting elements, regex extracts numeric values:
// Matches patterns like "45,25" or "45,30"
preg_match_all ( '/[0-9] + ,[0-9] + /' , $text , $matches )
Mimics a real browser to avoid being blocked:
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' ,
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9' ,
'Accept-Language' => 'es-ES,es;q=0.9,en;q=0.8' ,
Error Handling
HTTP errors return null, allowing the system to continue with other banks.
If a selector doesn’t match, an exception is thrown and logged, but the command continues.
Before scraping, the system checks if data already exists for today: $exists = ReferenceRecord :: where ( 'source' , $banco )
-> where ( 'date' , $today )
-> exists ();
if ( $exists ) {
$this -> info ( "El registro para { $banco } ya existe hoy. Saltando..." );
return ;
}
Caching : 1-hour cache reduces database queries
Indexed queries : Date and source fields should be indexed
Lazy loading : Only scrapes when scheduler runs
Fail-fast : Invalid responses return early
Observer pattern : Automatic cache invalidation on data changes