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

Mi API BCV uses the Strategy pattern to handle different scraping approaches for each bank. This design pattern allows the system to be easily extended with new data sources without modifying existing code.

Why Strategy Pattern?

Each Venezuelan bank displays exchange rates differently:
  • BCV: Official rate in #dolar element
  • Banplus: News ticker with “tasa de cambio” text
  • BNC: ItemSpace divs with “USD $ Compra Bs:” label
  • BDV: JSON API endpoint (different approach entirely)
The Strategy pattern encapsulates these differences into separate, interchangeable classes.

Pattern Structure

┌─────────────────────┐
│   UrlStrategy       │ (Interface)
│   - getValue()      │
└──────────┬──────────┘

           │ implements

    ┌──────┴───────┬───────────┬──────────┐
    │              │           │          │
┌───▼────┐   ┌────▼───┐  ┌────▼───┐  ┌──▼─────┐
│UrlBcv  │   │UrlBanplus│ │UrlBnc │  │UrlBdv  │
└────────┘   └──────────┘  └────────┘  └────────┘

         ┌─────────────────────┐
         │  UrlProviderService │ (Context)
         │  - getStrategy()    │
         └─────────────────────┘

The Strategy Interface

Location: app/Interfaces/UrlStrategy.php
namespace App\Interfaces;

interface UrlStrategy
{
    public function getValue(): float;
}
Contract: Every strategy must implement getValue() and return a float representing the USD/VES exchange rate.

Concrete Strategies

UrlBcv Strategy

Location: app/Strategies/Url/UrlBcv.php
namespace App\Strategies\Url;

use App\Interfaces\UrlStrategy;

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;
    }
}
Responsibilities:
  1. Define the target URL (BCV website)
  2. Call ScraperService with ‘bcv’ identifier
  3. Return the extracted value

UrlBanplus Strategy

Location: app/Strategies/Url/UrlBanplus.php
namespace App\Strategies\Url;

use App\Interfaces\UrlStrategy;

class UrlBanplus implements UrlStrategy
{
    protected $scraper;

    public function __construct($scraper)
    {
        $this->scraper = $scraper;
    }

    public function getValue(): float
    {
        $url = 'https://www.banplus.com/';
        $value = $this->scraper->scrapeData($url, 'banplus');
        return $value;
    }
}
Similar structure, different URL and bank identifier.

UrlBnc Strategy

Location: app/Strategies/Url/UrlBnc.php
namespace App\Strategies\Url;

use App\Interfaces\UrlStrategy;

class UrlBnc implements UrlStrategy
{
    protected $scraper;

    public function __construct($scraper)
    {
        $this->scraper = $scraper;
    }

    public function getValue(): float
    {
        $url = 'https://www.bnc.com.ve/';
        $value = $this->scraper->scrapeData($url, 'bnc');
        return $value;
    }
}

UrlBdv Strategy

Location: app/Strategies/Url/UrlBdv.php This strategy is more complex because BDV uses a JSON API instead of HTML scraping:
namespace App\Strategies\Url;

use App\Interfaces\UrlStrategy;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Exception;

class UrlBdv implements UrlStrategy
{
    protected string $url = 'https://www.bancodevenezuela.com/files/tasas/tasas2.json';

    public function getValue(): float
    {
        try {
            $response = Http::timeout(10)
                ->withHeaders([
                    'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36',
                    'Accept'     => 'application/json',
                ])
                ->get($this->url);

            if ($response->failed()) {
                throw new Exception("Error de conexión con BDV: " . $response->status());
            }

            $data = $response->json();
            $rawRate = data_get($data, 'menudeo.compra.dolares');

            if (!$rawRate) {
                throw new Exception("Estructura de JSON no reconocida o tasa faltante.");
            }

            return $this->formatRate($rawRate);
        } catch (Exception $e) {
            Log::error("Fallo obteniendo tasa BDV: " . $e->getMessage());
            return "0.00";
        }
    }

    private function formatRate(string $rate): float
    {
        $cleanRate = str_replace(',', '.', $rate);
        return (float) preg_replace('/[^0-9.]/', '', $cleanRate);
    }
}
Key Differences:
  • BDV doesn’t receive ScraperService in constructor (doesn’t need it)
  • Makes HTTP request directly to BDV’s JSON API
  • Uses data_get($data, 'menudeo.compra.dolares') to extract the rate
  • Returns string “0.00” on error (note: this is a bug in the source - should return float)
This demonstrates the flexibility of the Strategy pattern—different strategies can use completely different approaches while maintaining the same interface.

The Context: UrlProviderService

Location: app/Services/UrlProviderService.php The UrlProviderService acts as the context in the Strategy pattern. It manages and provides access to concrete strategies.
namespace App\Services;

use App\Interfaces\UrlStrategy;
use App\Strategies\Url\UrlBanplus;
use App\Strategies\Url\UrlBcv;
use App\Strategies\Url\UrlBdv;
use App\Strategies\Url\UrlBnc;

class UrlProviderService
{
    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(),
        };
    }
}
Responsibilities:
  1. Instantiate strategies on-demand using a match expression
  2. Create a new ScraperService for each HTML-scraping strategy (banplus, bnc, bcv)
  3. BDV is instantiated without ScraperService since it uses JSON API directly
  4. Provide default fallback to BDV strategy

Usage in FetchExchangeRates Command

Location: app/Console/Commands/FetchExchangeRates.php
protected array $banco = ['bdv', 'banplus', 'bnc', 'bcv'];

public function handle()
{
    foreach ($this->banco as $banco) {
        try {
            $this->store($banco);
        } catch (\Exception $e) {
            $this->error("Error en {$banco}: " . $e->getMessage());
        }
    }
}

protected function store(string $banco)
{
    // Check if record exists for today
    $exists = ReferenceRecord::where('source', $banco)
        ->where('date', $today)
        ->exists();

    if ($exists) {
        return;
    }

    // Get strategy and fetch value
    $value = $this->urlProvider->getStrategy($banco)->getValue();

    if ($value) {
        $record = new ReferenceRecord();
        $record->source = $banco;
        $record->value = $value;
        $record->date = $today;
        $record->save();
    }
}
Flow:
  1. Iterate through bank identifiers
  2. Get appropriate strategy from UrlProviderService
  3. Call getValue() on strategy
  4. Save result to database

Benefits of This Design

1. Open/Closed Principle

Open for extension: Add new banks by creating new strategy classes Closed for modification: Existing code doesn’t need to change

2. Single Responsibility

Each strategy class has one job: fetch the rate for its specific bank.

3. Easy Testing

You can test each strategy independently:
use Tests\TestCase;
use App\Services\ScraperService;
use App\Strategies\Url\UrlBcv;

class UrlBcvTest extends TestCase
{
    public function test_fetches_bcv_rate()
    {
        $scraper = new ScraperService();
        $strategy = new UrlBcv($scraper);
        
        $value = $strategy->getValue();
        
        $this->assertIsFloat($value);
        $this->assertGreaterThan(0, $value);
    }
}

4. Flexibility

Different strategies can use different approaches:
  • HTML scraping (BCV, Banplus, BNC)
  • JSON API (BDV)
  • FTP, database, or any other source

5. Maintainability

When a bank changes their website:
  1. Only one strategy class needs updating
  2. Other banks continue working
  3. Changes are isolated and safe

Adding a New Strategy

See the complete guide: Adding Banks Quick Steps:
1

Create strategy class

// app/Strategies/Url/UrlNuevoBanco.php
namespace App\Strategies\Url;

use App\Interfaces\UrlStrategy;

class UrlNuevoBanco implements UrlStrategy
{
    protected $scraper;
    
    public function __construct($scraper)
    {
        $this->scraper = $scraper;
    }
    
    public function getValue(): float
    {
        $url = 'https://www.nuevobanco.com.ve/';
        return $this->scraper->scrapeData($url, 'nuevobanco');
    }
}
2

Register in UrlProviderService

Update the match expression in getStrategy() method:
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(),
        'nuevobanco' => new UrlNuevoBanco(new ScraperService()), // Add this
        default  => new UrlBdv(),
    };
}
3

Add parser to ScraperService

return match ($banco) {
    'banplus' => $this->parseBanplusData($crawler),
    'bnc' => $this->parseBNCData($crawler),
    'bcv' => $this->parseBCVData($crawler),
    'nuevobanco' => $this->parseNuevoBancoData($crawler), // Add this
    default  => 0.00,
};
4

Update command

protected array $banco = ['bdv', 'banplus', 'bnc', 'bcv', 'nuevobanco'];

Alternative Patterns Considered

Factory Pattern

Could create strategies on-demand instead of registering upfront. Pros: More memory efficient Cons: More complex, strategies are lightweight anyway

Command Pattern

Each bank could be a command object. Pros: Built-in undo/retry logic Cons: Overkill for simple rate fetching

Simple If/Else

if ($banco === 'bcv') {
    // scrape BCV
} elseif ($banco === 'banplus') {
    // scrape Banplus
}
Pros: Simple, straightforward Cons:
  • Violates Open/Closed Principle
  • Hard to test
  • Gets messy with many banks
  • High coupling
The Strategy pattern is the right choice here because bank-specific logic is complex enough to warrant separate classes, but not so complex as to require heavier patterns.

Testing Strategies

Create a test command to verify strategies work:
// app/Console/Commands/TestStrategy.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\UrlProviderService;

class TestStrategy extends Command
{
    protected $signature = 'rates:test {bank}';
    protected $description = 'Test a specific bank strategy';

    public function handle(UrlProviderService $urlProvider)
    {
        $bank = $this->argument('bank');
        
        $strategy = $urlProvider->getStrategy($bank);
        
        if (!$strategy) {
            $this->error("Strategy not found for: {$bank}");
            return 1;
        }
        
        $this->info("Testing {$bank} strategy...");
        
        try {
            $value = $strategy->getValue();
            $this->info("Success! Rate: {$value}");
        } catch (\Exception $e) {
            $this->error("Failed: {$e->getMessage()}");
            return 1;
        }
        
        return 0;
    }
}
Usage:
php artisan rates:test bcv
php artisan rates:test banplus

Next Steps

ScraperService

Deep dive into the scraping implementation

Adding Banks

Step-by-step guide to adding new data sources