Multi-Currency & Exchange Rates

v0.1.0

ISO 4217 registry, base currency immutability, daily exchange rate feeds, integer minor unit conversions, and functional GL denomination.

Multi-Currency & Exchange Rates

International businesses must transact in foreign currencies while maintaining an unambiguous, compliant General Ledger denominated in their primary statutory currency.

Obelawium Accounting provides a built-in Multi-Currency Engine exposed through ium()->accounting()->currencies().


1. Architectural Principles

  1. Integer Minor Units: All monetary calculations and database columns are stored as 64-bit integers in minor currency units (e.g., $100.50 USD is stored as 10050 cents; 100.500 KWD is stored as 100500 fils).
  2. Base Functional Currency: One currency serves as the organization’s legal ledger denominator. Its exchange rate is locked to 1.000000 and cannot be altered.
  3. Immutable Daily FX Snapshots: Exchange rates are recorded by date and provider source. Historical rates are permanently preserved.
  4. Dual Denomination on Postings: Vouchers capture transactions in the original transaction currency, while General Ledger postings persist both transaction units and converted base functional units (amount_functional).

2. Managing Currencies

Registering an ISO 4217 Currency

use Obelaw\Ium\Accounting\Data\CreateCurrencyDto;

$eur = ium()->accounting()->currencies()->create(
    new CreateCurrencyDto(
        code: 'EUR',
        name: 'Euro',
        symbol: '€',
        subUnit: 'Cent',
        subUnitFactor: 100, // 2 decimals: 1 EUR = 100 Cents
        isBase: false
    )
);

Seeding Standard Currencies

To seed standard currencies (USD, EUR, GBP, JPY, SAR, AED, CAD, CHF, AUD):

$currencies = ium()->accounting()->currencies()->seedDefaultCurrencies();

3. Recording Exchange Rates

Foreign currencies quote their rate relative to the base functional currency:

ium()->accounting()->currencies()->setRate(
    currencyCode: 'EUR',
    rateToBase: 1.085000, // 1 EUR = 1.085 USD
    effectiveDate: '2026-03-31',
    source: 'ecb_daily_feed'
);

Base Rate Invariant: Calling setRate() on the base currency throws BaseCurrencyRateImmutableException.


4. Currency Conversion Utilities

Minor Unit Conversion (convertMinor)

Performs high-precision conversion between two currencies, taking differing sub-unit factors into account:

// Convert 10,000 EUR cents (100.00 EUR) into USD cents on 2026-03-31
$usdCents = ium()->accounting()->currencies()->convertMinor(
    amountMinor: 10000,
    fromCode: 'EUR',
    toCode: 'USD',
    date: '2026-03-31'
);

echo $usdCents; // 10850 ($108.50)

Conversion with Detailed Audit Breakdown

$result = ium()->accounting()->currencies()->convertDetailed(
    amount: 1500.00,
    fromCode: 'GBP',
    toCode: 'USD',
    date: '2026-03-31'
);

echo "From: {$result->fromCode} {$result->fromAmountMajor}\n";
echo "Rate Applied: {$result->rate}\n";
echo "To: {$result->toCode} {$result->toAmountMajor}\n";
echo "Minor Result: {$result->toAmountMinor}\n";

5. Sub-Unit Formatting & Helpers

Format minor units for presentation layers:

// Format minor units: "$ 108.50"
echo ium()->accounting()->currencies()->formatMinor(10850, 'USD');

// Format sub-units only: "50 Cents"
echo ium()->accounting()->currencies()->formatSubUnit(10850, 'USD');

// Split into major and minor components:
$split = ium()->accounting()->currencies()->splitUnits(108.50, 'USD');
// ['major' => 108, 'minor' => 50, 'formatted' => '$ 108.50']