NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Money for Laravel

Commands, events & scheduling

CommandPurpose
money:currencies {--iso} {--custom} {--code=*}List the registry: code, numeric code, exponent, symbol, name and origin.
money:rates:refresh {source=ecb} {--feed=daily} {--from=} {--to=} {--queue}Fetch rates into the table. --feed is daily, recent or history; --queue dispatches a unique job; supports --isolated.
money:rates:prune {--before=} {--keep-days=} {--include-manual} {--pretend}Delete old rates. One bound is required; manual rows stay unless --include-manual; --pretend only counts.
money:convert {amount} {from} {to} {--date=} {--driver=}Convert an amount and print the rate, source, date and result.
php artisan money:currencies --custom

php artisan money:rates:refresh                     # ECB daily feed into the rates table
php artisan money:rates:refresh ecb --feed=recent   # the last 90 days
php artisan money:rates:refresh ecb --feed=history --from=2020-01-01 --to=2020-12-31
php artisan money:rates:refresh --queue             # dispatch a unique queued job instead
php artisan money:rates:refresh --isolated          # keep overlapping runs out

php artisan money:rates:prune --keep-days=365 --pretend   # only count
php artisan money:rates:prune --before=2024-01-01 --include-manual

php artisan money:convert 19.99 EUR USD
php artisan money:convert 19.99 EUR USD --date=2026-09-01 --driver=database

Scheduled refresh

With MONEY_EXCHANGE_SCHEDULE=true the provider schedules money:rates:refresh on exchange.refresh.cron — 30 16 * * 1-5 in Europe/Berlin, just after the ECB publishes around 16:00 CET — with withoutOverlapping() and onOneServer(). Your app’s scheduler must be running. It’s off by default.

Queued refresh

use RoundlyConsulting\Money\Enums\EcbFeed;
use RoundlyConsulting\Money\Facades\Exchange;
use RoundlyConsulting\Money\Jobs\RefreshExchangeRates;

Exchange::rates()->refreshLater(feed: EcbFeed::Recent);   // queue the refresh

// the same job, dispatched yourself: source, feed, ?from, ?to (Y-m-d)
dispatch(new RefreshExchangeRates('ecb', 'recent'));

refreshLater() dispatches the RefreshExchangeRates job, which is unique per source and feed for ten minutes and holds scalars only.

Refreshing and pruning from code

use RoundlyConsulting\Money\Enums\EcbFeed;
use RoundlyConsulting\Money\Facades\Exchange;

$result = Exchange::rates()->refresh('ecb', EcbFeed::Recent);   // RefreshResult
$result->stored;                   // rows written
$result->skippedManual;            // manual rows left untouched
$result->skippedUnknownCurrency;   // feed currencies the registry does not know
$result->latestDate;               // ?CarbonImmutable

$deleted = Exchange::rates()->prune(now()->subYear());          // int

A refresh defaults to the last 7 days (daily feed), 90 days (recent) or everything since 1999 (history). A failed refresh never deletes rows. Prune returns the number of rows deleted — or counted, when pretending. The actions behind these calls are under DI and actions.

Events

use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Money\Events\ExchangeRatesRefreshed;
use RoundlyConsulting\Money\Events\ExchangeRatesRefreshFailed;

Event::listen(function (ExchangeRatesRefreshed $event) {
    logger()->info('Exchange rates refreshed', [
        'source' => $event->result->source,
        'stored' => $event->result->stored,
        'latest' => $event->result->latestDate?->toDateString(),
    ]);
});

Event::listen(function (ExchangeRatesRefreshFailed $event) {
    // alert your team: $event->source, $event->message — existing rows are untouched
});
  • ExchangeRatesRefreshed — RefreshResult $result: source, stored, skippedManual, skippedUnknownCurrency, latestDate.
  • ExchangeRatesRefreshFailed — string $source and string $message, fired before the exception is rethrown. Hook your alerting here.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.