Skip to main content

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

1

Open the bank's website

Navigate to the page where exchange rates are displayed.
2

Open Developer Tools

Press F12 or right-click → Inspect Element
3

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”)
4

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:
php artisan rates:update
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:
php artisan tinker
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:
  1. Find the API endpoint the JavaScript calls
  2. Use a headless browser (see “Advanced: JavaScript Sites” below)
  3. Contact the bank for an official API

Common Issues

Selector Not Found

Problem: Exception: No se encontró el elemento esperado en Mercantil Solutions:
  1. Verify the selector is correct
  2. Check if the page structure changed
  3. Ensure the page loaded fully (check $response->status())
  4. Try a more specific or less specific selector

Rate Extraction Fails

Problem: Rate is 0.00 or NaN Solutions:
  1. Print the raw text: dd($text) to see what was extracted
  2. Check the regex pattern
  3. Verify comma/period handling
  4. 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:
  1. Use realistic User-Agent (already done)
  2. Add delays: sleep(2) between requests
  3. Rotate User-Agents
  4. Use proxy servers
  5. 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:
  1. Reload the page
  2. Look for XHR/Fetch requests
  3. Find the JSON endpoint
  4. 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:
  • Strategy class created and implements UrlStrategy
  • Parser method added to ScraperService
  • Strategy registered in UrlProviderService
  • Bank added to FetchExchangeRates command array
  • Manual rates:update runs without errors
  • Database contains new bank’s record
  • API returns new bank in response
  • Historical endpoint works: /info/YYYY-MM-DD/mercantil
  • Edge cases handled (rate unavailable, weekends)
  • Error messages are descriptive
  • Code follows existing conventions

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