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.
Overview
This guide walks you through adding a new Venezuelan bank as a data source for exchange rates. Thanks to the Strategy pattern, this process is straightforward and doesn’t require modifying existing code.
Prerequisites
Before starting, ensure you have:
Access to the bank’s website displaying USD/VES rates
Basic understanding of HTML/CSS selectors
Browser developer tools for inspecting page structure
PHP 8.2+ development environment
Step 1: Analyze the Target Website
Inspect the HTML Structure
Open the bank's website
Navigate to the page where exchange rates are displayed.
Open Developer Tools
Press F12 or right-click → Inspect Element
Locate the exchange rate element
Find the HTML element containing the USD/VES rate. Look for:
ID attributes (e.g., #exchange-rate)
Class names (e.g., .rate-display)
Unique text patterns (e.g., “USD $ Compra”)
Note the rate format
Document how the rate is displayed:
"USD 69,50" (comma as decimal separator)
"69.50 Bs" (period as decimal separator)
"Tasa: 69,50" (with label)
Example: Analyzing Mercantil Bank
Let’s say we want to add Banco Mercantil:
< div class = "exchange-rates" >
< div class = "rate-item" >
< span class = "currency" > USD </ span >
< span class = "value" id = "usd-rate" > 69,50 </ span >
< span class = "unit" > Bs </ span >
</ div >
</ div >
Key findings :
Selector: #usd-rate or .value
Format: “69,50” (comma decimal)
Clean element with just the number
Step 2: Create the Strategy Class
Create a new file: app/Strategies/Url/UrlMercantil.php
<? php
namespace App\Strategies\Url ;
use App\Interfaces\ UrlStrategy ;
class UrlMercantil implements UrlStrategy
{
protected $scraper ;
public function __construct ( $scraper )
{
$this -> scraper = $scraper ;
}
public function getValue () : float
{
$url = 'https://www.mercantilbanco.com/' ;
$value = $this -> scraper -> scrapeData ( $url , 'mercantil' );
return $value ;
}
}
Replace https://www.mercantilbanco.com/ with the actual URL.
The strategy class is minimal—it just defines the URL and delegates scraping to ScraperService.
Step 3: Add Parser to ScraperService
Edit app/Services/ScraperService.php:
Add to match expression
return match ( $banco ) {
'banplus' => $this -> parseBanplusData ( $crawler ),
'bnc' => $this -> parseBNCData ( $crawler ),
'bcv' => $this -> parseBCVData ( $crawler ),
'mercantil' => $this -> parseMercantilData ( $crawler ), // Add this line
default => 0.00 ,
};
Create the parser method
Add this method to ScraperService:
private function parseMercantilData ( $crawler )
{
// Find the element by ID
$element = $crawler -> filter ( '#usd-rate' );
// Verify element exists
if ( $element -> count () === 0 ) {
throw new \Exception ( 'No se encontró el elemento esperado en Mercantil' );
}
// Extract text content
$text = $element -> text ();
// Clean and return value
return $this -> cleanValue ( $text );
}
Alternative: Complex Parsing
If the rate isn’t in a clean element:
private function parseMercantilData ( $crawler )
{
// Find all rate items
$items = $crawler -> filter ( '.rate-item' ) -> each ( function ( Crawler $node ) {
$currency = $node -> filter ( '.currency' ) -> text ();
$value = $node -> filter ( '.value' ) -> text ();
// Return USD rate only
return $currency === 'USD' ? $value : null ;
});
// Filter out nulls
$filteredItems = array_values ( array_filter ( $items ));
if ( empty ( $filteredItems )) {
throw new \Exception ( 'No se encontró la tasa USD en Mercantil' );
}
return $this -> cleanValue ( $filteredItems [ 0 ]);
}
Using Regex
For rates embedded in text:
private function parseMercantilData ( $crawler )
{
$element = $crawler -> filter ( '.exchange-info' );
if ( $element -> count () === 0 ) {
throw new \Exception ( 'No se encontró el elemento esperado en Mercantil' );
}
$text = $element -> text ();
// Text might be: "Tasa de cambio USD: 69,50 Bs"
// Extract number with regex
if ( preg_match ( '/USD[:\s] + ([0-9] + ,[0-9] + )/' , $text , $matches )) {
return $this -> cleanValue ( $matches [ 1 ]);
}
throw new \Exception ( 'No se pudo extraer el valor numérico de Mercantil' );
}
Use cleanValue() for all extracted values—it handles comma/period conversion and removes non-numeric characters.
Step 4: Register Strategy in UrlProviderService
Edit app/Services/UrlProviderService.php:
Import the new strategy
use App\Strategies\Url\ UrlBcv ;
use App\Strategies\Url\ UrlBanplus ;
use App\Strategies\Url\ UrlBnc ;
use App\Strategies\Url\ UrlBdv ;
use App\Strategies\Url\ UrlMercantil ; // Add this
Register in the strategies array
protected function registerStrategies ()
{
$this -> strategies = [
'bcv' => new UrlBcv ( $this -> scraper ),
'banplus' => new UrlBanplus ( $this -> scraper ),
'bnc' => new UrlBnc ( $this -> scraper ),
'bdv' => new UrlBdv ( $this -> scraper ),
'mercantil' => new UrlMercantil ( $this -> scraper ), // Add this
];
}
Step 5: Update FetchExchangeRates Command
Edit app/Console/Commands/FetchExchangeRates.php:
Add the new bank identifier to the array:
protected array $banco = [
'bdv' ,
'banplus' ,
'bnc' ,
'bcv' ,
'mercantil' // Add this
];
The command will now automatically fetch rates from Mercantil during scheduled updates.
Step 6: Test the Implementation
Manual Testing
Run the update command manually:
Watch for output:
Iniciando actualización de tasas...
Banco bdv procesado.
Banco banplus procesado.
Banco bnc procesado.
Banco bcv procesado.
Banco mercantil procesado.
Tasas actualizadas exitosamente.
Check the Database
Verify the rate was saved:
use App\Models\ ReferenceRecord ;
ReferenceRecord :: where ( 'source' , 'mercantil' )
-> where ( 'date' , now () -> toDateString ())
-> first ();
Should return:
=> App \ Models \ ReferenceRecord { #xxxx
id : 123 ,
source : "mercantil" ,
value : "69.5000" ,
date : "2026-03-04" ,
created_at : "2026-03-04 10:30:00" ,
updated_at : "2026-03-04 10:30:00" ,
}
Test the API
Query the API to see the new bank’s data:
curl http://localhost:8000/api/
Response should include:
{
"message" : "Consulta exitosa" ,
"bcv" : {
"value" : 69.50 ,
"date" : "2026-03-04"
},
"banplus" : {
"value" : 69.48 ,
"date" : "2026-03-04"
},
"bnc" : {
"value" : 69.45 ,
"date" : "2026-03-04"
},
"bdv" : {
"value" : 69.47 ,
"date" : "2026-03-04"
},
"mercantil" : {
"value" : 69.50 ,
"date" : "2026-03-04"
}
}
Create a Test Command (Optional)
For easier testing during development:
// app/Console/Commands/TestBankStrategy.php
namespace App\Console\Commands ;
use Illuminate\Console\ Command ;
use App\Services\ UrlProviderService ;
class TestBankStrategy extends Command
{
protected $signature = 'rates:test {bank}' ;
protected $description = 'Test a specific bank scraping strategy' ;
public function handle ( UrlProviderService $urlProvider )
{
$bank = $this -> argument ( 'bank' );
$this -> info ( "Testing { $bank } strategy..." );
try {
$strategy = $urlProvider -> getStrategy ( $bank );
if ( ! $strategy ) {
$this -> error ( "Strategy not found: { $bank }" );
return 1 ;
}
$value = $strategy -> getValue ();
$this -> info ( "✓ Success!" );
$this -> info ( "Bank: { $bank }" );
$this -> info ( "Rate: { $value }" );
return 0 ;
} catch ( \ Exception $e ) {
$this -> error ( "✗ Failed: { $e -> getMessage ()}" );
return 1 ;
}
}
}
Usage :
php artisan rates:test mercantil
Step 7: Handle Edge Cases
Rate Not Available
Some banks might not display rates on weekends or holidays:
private function parseMercantilData ( $crawler )
{
$element = $crawler -> filter ( '#usd-rate' );
if ( $element -> count () === 0 ) {
// Rate might not be available
$this -> warn ( 'Tasa no disponible en Mercantil' );
return 0.00 ; // or throw exception
}
$text = $element -> text ();
// Check for "N/A" or similar
if ( str_contains ( strtolower ( $text ), 'no disponible' )) {
return 0.00 ;
}
return $this -> cleanValue ( $text );
}
Multiple Rates
If a bank displays buy/sell rates, choose one consistently:
private function parseMercantilData ( $crawler )
{
// Get buy rate (compra)
$buyRate = $crawler -> filter ( '.rate-buy' ) -> text ();
// Get sell rate (venta)
$sellRate = $crawler -> filter ( '.rate-sell' ) -> text ();
// Use buy rate or average
return $this -> cleanValue ( $buyRate );
// OR use average:
// $buy = $this->cleanValue($buyRate);
// $sell = $this->cleanValue($sellRate);
// return ($buy + $sell) / 2;
}
Dynamic Content (JavaScript)
If the rate is loaded via JavaScript:
The current ScraperService doesn’t execute JavaScript. You’ll need a headless browser solution like Laravel Dusk or Puppeteer.
Alternative approaches:
Find the API endpoint the JavaScript calls
Use a headless browser (see “Advanced: JavaScript Sites” below)
Contact the bank for an official API
Common Issues
Selector Not Found
Problem : Exception: No se encontró el elemento esperado en Mercantil
Solutions :
Verify the selector is correct
Check if the page structure changed
Ensure the page loaded fully (check $response->status())
Try a more specific or less specific selector
Problem : Rate is 0.00 or NaN
Solutions :
Print the raw text: dd($text) to see what was extracted
Check the regex pattern
Verify comma/period handling
Look for hidden characters or HTML entities
SSL Certificate Errors
Problem : cURL error 60: SSL certificate problem
Already handled by withoutVerifying() in ScraperService.
Timeout
Problem : Bank’s website is slow
Solution : Increase timeout in ScraperService:
$response = Http :: timeout ( 30 ) // 30 seconds
-> withoutVerifying ()
-> withHeaders ([ ... ])
-> get ( $url );
Bot Detection
Problem : Bank blocks automated requests
Solutions :
Use realistic User-Agent (already done)
Add delays: sleep(2) between requests
Rotate User-Agents
Use proxy servers
Contact bank for official API access
Advanced: JavaScript-Rendered Sites
If the bank uses a JavaScript framework (React, Vue, Angular):
Option 1: Find the API
Inspect Network tab in browser DevTools:
Reload the page
Look for XHR/Fetch requests
Find the JSON endpoint
Make a direct HTTP request to that endpoint
public function getValue () : float
{
$response = Http :: get ( 'https://api.mercantilbanco.com/v1/rates' );
$data = $response -> json ();
return ( float ) $data [ 'usd_rate' ];
}
Option 2: Use Laravel Dusk
Install Laravel Dusk for headless browser automation:
composer require --dev laravel/dusk
php artisan dusk:install
Create a custom scraper:
use Laravel\Dusk\ Browser ;
public function getValue () : float
{
return $this -> browse ( function ( Browser $browser ) {
$browser -> visit ( 'https://www.mercantilbanco.com/' )
-> waitFor ( '#usd-rate' , 10 );
$text = $browser -> text ( '#usd-rate' );
return $this -> cleanValue ( $text );
});
}
Headless browser solutions are slower and more resource-intensive. Only use when necessary.
Testing Checklist
Before considering the implementation complete:
Complete Example: Adding Banco Provincial
Here’s a complete example from start to finish:
1. Strategy Class
// app/Strategies/Url/UrlProvincial.php
<? php
namespace App\Strategies\Url ;
use App\Interfaces\ UrlStrategy ;
class UrlProvincial implements UrlStrategy
{
protected $scraper ;
public function __construct ( $scraper )
{
$this -> scraper = $scraper ;
}
public function getValue () : float
{
$url = 'https://www.provincial.com/' ;
$value = $this -> scraper -> scrapeData ( $url , 'provincial' );
return $value ;
}
}
2. Parser Method
// In app/Services/ScraperService.php
private function parseProvincialData ( $crawler )
{
$element = $crawler -> filter ( '.exchange-rate-value' );
if ( $element -> count () === 0 ) {
throw new \Exception ( 'No se encontró el elemento esperado en Provincial' );
}
$text = $element -> text ();
// Text format: "USD 69,50"
if ( preg_match ( '/USD \s + ([0-9] + ,[0-9] + )/' , $text , $matches )) {
return $this -> cleanValue ( $matches [ 1 ]);
}
// Fallback: try to clean entire text
return $this -> cleanValue ( $text );
}
// Add to match:
return match ( $banco ) {
'banplus' => $this -> parseBanplusData ( $crawler ),
'bnc' => $this -> parseBNCData ( $crawler ),
'bcv' => $this -> parseBCVData ( $crawler ),
'provincial' => $this -> parseProvincialData ( $crawler ),
default => 0.00 ,
};
3. Registration
// In app/Services/UrlProviderService.php
use App\Strategies\Url\ UrlProvincial ;
protected function registerStrategies ()
{
$this -> strategies = [
'bcv' => new UrlBcv ( $this -> scraper ),
'banplus' => new UrlBanplus ( $this -> scraper ),
'bnc' => new UrlBnc ( $this -> scraper ),
'bdv' => new UrlBdv ( $this -> scraper ),
'provincial' => new UrlProvincial ( $this -> scraper ),
];
}
4. Command Update
// In app/Console/Commands/FetchExchangeRates.php
protected array $banco = [
'bdv' ,
'banplus' ,
'bnc' ,
'bcv' ,
'provincial'
];
5. Test
php artisan rates:update
php artisan tinker
ReferenceRecord :: where ( 'source' , 'provincial' ) -> latest () -> first ();
Done!
Next Steps
Scheduler Setup Configure automatic daily updates
Architecture Understand the overall system design