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 Laravel’s task scheduler to automatically fetch exchange rates daily. The rates:update command runs on a schedule and updates the database with the latest rates from all configured banks.

The FetchExchangeRates Command

Location: app/Console/Commands/FetchExchangeRates.php

Command Signature

protected $signature = 'rates:update';
protected $description = 'Consulta el BCV y otros bancos para actualizar tasas';
Run manually:
php artisan rates:update

Command Structure

namespace App\Console\Commands;

use App\Models\ReferenceRecord;
use App\Services\UrlProviderService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;

class FetchExchangeRates extends Command
{
    protected $signature = 'rates:update';
    protected $description = 'Consulta el BCV y otros bancos para actualizar tasas';
    protected $urlProvider;
    protected array $banco = [];

    public function __construct(UrlProviderService $urlProvider)
    {
        parent::__construct();
        $this->urlProvider = $urlProvider;
        $this->banco = ['bdv', 'banplus', 'bnc', 'bcv'];
    }

    public function handle()
    {
        $this->info('Iniciando actualización de tasas...');
        $this->logApi();

        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.');
    }

    protected function store(string $banco)
    {
        $today = now()->toDateString();

        // Check if record 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;
        }

        // Fetch value via strategy
        $value = $this->urlProvider->getStrategy($banco)->getValue();

        if ($value) {
            $record = new ReferenceRecord();
            $record->source = $banco;
            $record->value = $value;
            $record->date = $today;
            $record->save();

            $this->info("Guardado exitoso: {$banco} -> {$value}");
        } else {
            $this->warn("No se obtuvo valor para {$banco}");
        }
    }

    protected function logApi()
    {
        Log::info("SE EJECUTO EL CRON", [
            'time' => now()->toDateTimeString()
        ]);
    }
}

How It Works

Execution Flow

1

Command starts

Laravel invokes the handle() method
2

Log execution

Records the cron run in the log file
3

Iterate through banks

Loops through ['bdv', 'banplus', 'bnc', 'bcv']
4

Check for existing record

Queries database to see if today’s rate already exists
ReferenceRecord::where('source', $banco)
    ->where('date', today())
    ->exists();
5

Fetch rate if needed

If no record exists:
  1. Get appropriate strategy from UrlProviderService
  2. Call getValue() to scrape/fetch the rate
  3. Save to database
6

Handle errors gracefully

If one bank fails, log the error and continue with the next bank
7

Complete

Output success message

Error Isolation

Each bank is processed independently:
foreach ($this->banco as $banco) {
    try {
        $this->store($banco);
    } catch (\Exception $e) {
        // Log error but continue
        $this->error("Error en {$banco}: " . $e->getMessage());
        Log::error("Fallo en cron para {$banco}: " . $e->getMessage());
    }
}
Result: If BCV fails, Banplus, BNC, and BDV still update.

Duplicate Prevention

The command checks if today’s rate already exists:
$exists = ReferenceRecord::where('source', $banco)
    ->where('date', $today)
    ->exists();

if ($exists) {
    $this->info("El registro para {$banco} ya existe hoy. Saltando...");
    return;
}
Benefit: Running the command multiple times per day won’t create duplicate records.

Setting Up Laravel Scheduler

Step 1: Configure Schedule

Edit app/Console/Kernel.php (or routes/console.php in Laravel 11+):
namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    protected function schedule(Schedule $schedule)
    {
        // Run rates:update every day at 9:00 AM
        $schedule->command('rates:update')
                 ->dailyAt('09:00')
                 ->timezone('America/Caracas');
    }
}
In Laravel 11+, scheduled tasks may be defined in routes/console.php instead of Kernel.php.

Scheduling Options

Daily at Specific Time

$schedule->command('rates:update')->dailyAt('09:00');

Every Weekday

$schedule->command('rates:update')
         ->weekdays()
         ->at('09:00');

Multiple Times Per Day

$schedule->command('rates:update')->twiceDaily(9, 15);
Runs at 9:00 AM and 3:00 PM.

Custom Times

$schedule->command('rates:update')->dailyAt('09:00');
$schedule->command('rates:update')->dailyAt('15:00');
$schedule->command('rates:update')->hourly();
Exchange rates don’t change every hour. Daily updates are sufficient and reduce server load and scraping requests.

Advanced Scheduling

Prevent Overlaps

Ensure command doesn’t run if previous execution is still running:
$schedule->command('rates:update')
         ->dailyAt('09:00')
         ->withoutOverlapping();

Run in Background

$schedule->command('rates:update')
         ->dailyAt('09:00')
         ->runInBackground();

Send Output to Log

$schedule->command('rates:update')
         ->dailyAt('09:00')
         ->appendOutputTo(storage_path('logs/scheduler.log'));

Email on Failure

$schedule->command('rates:update')
         ->dailyAt('09:00')
         ->emailOutputOnFailure('admin@example.com');
Requires mail configuration in .env.

Ping URL on Completion

For monitoring services like Laravel Envoyer or Oh Dear:
$schedule->command('rates:update')
         ->dailyAt('09:00')
         ->pingBefore('https://monitor.example.com/start/12345')
         ->thenPing('https://monitor.example.com/end/12345');

Setting Up System Cron

Laravel’s scheduler requires a single cron entry that runs every minute:

Step 2: Add Cron Entry

Edit the crontab:
crontab -e
Add this line:
* * * * * cd /path/to/miapibcv && php artisan schedule:run >> /dev/null 2>&1
Replace /path/to/miapibcv with your actual project path. Example (production):
* * * * * cd /var/www/miapibcv && php artisan schedule:run >> /dev/null 2>&1

What This Does

  • * * * * * - Run every minute
  • cd /path/to/miapibcv - Navigate to project directory
  • php artisan schedule:run - Laravel checks if any scheduled tasks should run
  • >> /dev/null 2>&1 - Suppress output
The cron runs every minute, but schedule:run only executes scheduled tasks when their time arrives.

Verify Cron is Active

List current cron jobs:
crontab -l
Should output:
* * * * * cd /var/www/miapibcv && php artisan schedule:run >> /dev/null 2>&1

Check Cron Logs

View system cron logs:
grep CRON /var/log/syslog
For Laravel’s output, check:
tail -f storage/logs/laravel.log

Production Configuration

Timezone Configuration

Ensure correct timezone in .env:
APP_TIMEZONE=America/Caracas
Or in config/app.php:
'timezone' => 'America/Caracas',

Scheduler in Schedule Definition

Explicitly set timezone:
$schedule->command('rates:update')
         ->dailyAt('09:00')
         ->timezone('America/Caracas');

User Permissions

Ensure cron user has permissions:
sudo chown -R www-data:www-data /var/www/miapibcv
sudo chmod -R 755 /var/www/miapibcv/storage
sudo chmod -R 755 /var/www/miapibcv/bootstrap/cache

PHP Path

If php isn’t in PATH, use absolute path:
* * * * * cd /var/www/miapibcv && /usr/bin/php artisan schedule:run >> /dev/null 2>&1
Find PHP path:
which php

Monitoring Scheduled Tasks

Log Output

Modify cron to log output:
* * * * * cd /var/www/miapibcv && php artisan schedule:run >> /var/www/miapibcv/storage/logs/cron.log 2>&1
View logs:
tail -f storage/logs/cron.log

Laravel Logs

Check application logs:
tail -f storage/logs/laravel.log
Look for:
[2026-03-04 09:00:00] local.INFO: SE EJECUTO EL CRON {"time":"2026-03-04 09:00:00"}

Database Verification

Check if rates are being updated:
php artisan tinker
use App\Models\ReferenceRecord;

// Get today's records
ReferenceRecord::whereDate('date', today())->get();

// Count records per day
ReferenceRecord::selectRaw('date, count(*) as count')
    ->groupBy('date')
    ->orderBy('date', 'desc')
    ->limit(7)
    ->get();

Health Check Endpoint

Create a health check endpoint to monitor updates:
// routes/api.php
Route::get('/health', function () {
    $today = now()->toDateString();
    $count = ReferenceRecord::where('date', $today)->count();
    
    return response()->json([
        'status' => $count > 0 ? 'healthy' : 'unhealthy',
        'date' => $today,
        'banks_updated' => $count,
        'expected' => 4, // bcv, banplus, bnc, bdv
    ]);
});
Test:
curl http://localhost:8000/api/health

Troubleshooting

Cron Not Running

Check cron service:
sudo systemctl status cron
Restart if needed:
sudo systemctl restart cron
Verify crontab:
crontab -l

Command Not Executing

Test manually:
cd /var/www/miapibcv
php artisan schedule:run
Check output for errors. Test the rates:update command directly:
php artisan rates:update

Permission Denied

Check file permissions:
ls -la storage/logs
Fix:
sudo chown -R www-data:www-data storage
sudo chmod -R 775 storage

Database Connection Issues

Check .env configuration:
DB_CONNECTION=sqlite
DB_DATABASE=/var/www/miapibcv/database/database.sqlite
Verify database file exists and is writable:
ls -la database/database.sqlite

Scraping Failures

If all banks fail:
  1. Check network connectivity
  2. Verify DNS resolution
  3. Test bank websites manually
  4. Check server firewall rules
For individual bank failures, see ScraperService troubleshooting.

Alternative: Supervisor Queue

For more robust scheduling, use Laravel queues with Supervisor:

1. Configure Queue

In .env:
QUEUE_CONNECTION=database

2. Create Migration

php artisan queue:table
php artisan migrate

3. Dispatch Job

Create a job:
php artisan make:job FetchExchangeRatesJob
Implement:
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Services\UrlProviderService;
use App\Models\ReferenceRecord;

class FetchExchangeRatesJob implements ShouldQueue
{
    use Queueable;

    public function handle(UrlProviderService $urlProvider)
    {
        $banks = ['bdv', 'banplus', 'bnc', 'bcv'];
        
        foreach ($banks as $bank) {
            // ... fetch logic
        }
    }
}
Schedule:
$schedule->job(new FetchExchangeRatesJob)->dailyAt('09:00');

4. Run Queue Worker

php artisan queue:work
Use Supervisor to keep it running (see Deployment Guide).

Best Practices

Scheduling Time

Choose when banks typically update their rates:
  • 9:00 AM: After banks open
  • 3:00 PM: Midday update
  • Weekdays only: Banks don’t update rates on weekends

Error Notifications

Set up alerts for failures:
$schedule->command('rates:update')
         ->dailyAt('09:00')
         ->onFailure(function () {
             // Send email, Slack notification, etc.
             \Log::critical('Rates update failed!');
         });

Retries

Add retry logic for transient failures:
use Illuminate\Support\Facades\Http;

$value = retry(3, function () use ($url, $banco) {
    return $this->scraper->scrapeData($url, $banco);
}, 100); // 100ms delay between retries

Monitoring

Use services like:

Next Steps

Deployment Guide

Deploy to production with full scheduler setup

Architecture

Understand how the scheduler fits into the system