The Amount & Currency System
Working with monetary values — Amount objects, cents-as-integers, Currency model, price adapters.
Last updated: April 6, 2026
The Amount & Currency System
Siren represents all monetary values as integers in the smallest currency unit (cents for USD, pence for GBP, etc.). This avoids the floating-point precision errors that plague financial software. The Amount model pairs an integer value with a Currency object, and the FloatToIntPriceAdapter handles conversions.
What is the Amount model?
The Amount model (Siren\Commerce\Models\Amount) is a value object that pairs an integer value with its currency.
class Amount
{
protected int $value;
protected Currency $currency;
public function __construct(int $value, Currency $currency)
{
$this->value = $value;
$this->currency = $currency;
}
public function getValue(): int // e.g., 1999 for $19.99
public function getCurrency(): Currency
}
Amount is a value object — it holds the numeric value and its associated currency, nothing more. It has no setters; create a new instance to represent a different amount.
Key Design Decision: Integers, Not Floats
All monetary values throughout Siren are integers representing the smallest currency unit. For USD, 1999 means $19.99. For JPY (which has no fractional unit), 1999 means 1999 yen.
Why integers?
// This is wrong — floating point:
0.1 + 0.2 === 0.3 // false in most languages
// This is correct — integer cents:
10 + 20 === 30 // always true
Floating-point arithmetic produces rounding errors that compound across thousands of transactions. When calculating commissions on affiliate sales, even a fraction-of-a-cent error per transaction becomes real money at scale.
How does Siren represent currencies?
The Currency model (Siren\Commerce\Models\Currency) stores a currency code, display symbol, and symbol position.
class Currency implements DataModel, HasSingleStringIdentity
{
use WithSingleStringIdentity;
protected string $symbol;
protected string $position;
public function __construct(
string $id, // ISO 4217 code: 'USD', 'EUR', 'GBP'
string $symbol, // Display symbol: '$', '€', '£'
string $position = 'before' // 'before' or 'after'
)
}
| Property | Type | Description |
|---|---|---|
$id | string | ISO 4217 currency code (e.g., 'USD') |
$symbol | string | Display symbol (e.g., '$', '€', 'R$') |
$position | string | Whether symbol appears 'before' or 'after' the number |
Available Currencies
Siren registers 23 currencies out of the box via RegisterCoreCurrencies:
| Code | Symbol | Code | Symbol | Code | Symbol |
|---|---|---|---|---|---|
| USD | $ | EUR | € | GBP | £ |
| JPY | ¥ | CNY | ¥ | AUD | $ |
| CAD | $ | INR | ₹ | BRL | R$ |
| ZAR | R | SGD | $ | MYR | RM |
| THB | ฿ | SEK | kr | CHF | CHF |
| NZD | $ | MXN | $ | HKD | $ |
| NOK | kr | KRW | ₩ | TRY | ₺ |
| RUB | ₽ | PLN | zł |
Additional currencies can be registered by listening to the CurrencyRegistryInitiated event:
use Siren\Configs\Core\Events\CurrencyRegistryInitiated;
use Siren\Commerce\Models\Currency;
// In your initializer's getListeners():
CurrencyRegistryInitiated::class => MyCustomCurrencyRegistrar::class,
// In the handler:
public function handle(Event $event): void
{
$event->addCurrency('NGN', fn() => new Currency('NGN', '₦'));
}
How do you convert between float prices and integer cents?
FloatToIntPriceAdapter converts between the float prices that e-commerce platforms use and the integer cents that Siren stores.
class FloatToIntPriceAdapter
{
public function toInt(float $amount): int // 19.99 → 1999
public function toFloat(int $amount): float // 1999 → 19.99
public function toString(Amount $amount, $decimalSeparator = '.', $thousandsSeparator = ','): string
}
toInt — Platform Price to Siren
$adapter = new FloatToIntPriceAdapter();
// WooCommerce returns floats:
$wcPrice = $order->get_total(); // 49.99
// Convert for Siren:
$cents = $adapter->toInt($wcPrice); // 4999
This is used extensively in order-to-transaction-details adapters. Every line item value, shipping total, tax total, and discount must go through toInt() before entering the transaction details array.
toFloat — Siren to Display
$displayPrice = $adapter->toFloat(4999); // 49.99
toString — Formatted Currency String
$amount = new Amount(4999, new Currency('USD', '$'));
$formatted = $adapter->toString($amount); // "$49.99"
$euroAmount = new Amount(4999, new Currency('EUR', '€', 'after'));
$formatted = $adapter->toString($euroAmount, ',', '.'); // "49,99€"
The toString method respects the currency’s position property — symbols marked 'after' are appended, otherwise prepended.
Using Amount in Transaction Details
When building the transaction details array for SaleTriggered, every value field must be in integer cents. Here is the pattern from the WooCommerce adapter:
// From OrderToTransactionDetailsAdapter::toArray()
$shippingTotal = $this->priceAdapter->toInt($order->get_shipping_total());
if ($shippingTotal > 0) {
$result[] = [
'name' => 'Shipping',
'description' => 'Shipping Fees',
'type' => 'shipping',
'value' => $shippingTotal, // Integer cents
'quantity' => 1,
'units' => $currency, // e.g., 'USD'
'externalId' => null
];
}
// For line items, value is per-unit:
$total = $this->priceAdapter->toInt($item->get_total());
$result[] = [
'value' => $total / $item->get_quantity(), // Per-unit price in cents
'quantity' => $item->get_quantity(),
// ...
];
The value field in transaction details is the per-unit price, not the line total. The system multiplies value * quantity internally.
Discounts are represented as negative values:
$discount = $this->priceAdapter->toInt($order->get_discount_total());
if ($discount > 0) {
$result[] = [
'type' => 'discount',
'value' => $discount * -1, // Negative value
// ...
];
}
Amount in Incentive Calculations
The incentive system uses Amount to represent commission pools — the total reward to distribute for a conversion.
How does Siren resolve currencies for programs?
This service resolves the correct currency for a program’s incentive calculations:
public function buildCurrency(int $programId): Currency
{
$program = $this->programs->find($programId);
$units = $this->currencyProviderService->getCurrencyFromCode($program->getUnits());
if (!isset($units)) {
$units = $this->currencyProviderService->getDefaultCurrency();
}
return $units;
}
Each program has a units field (e.g., 'USD'). The service resolves this to a Currency object, falling back to the system default if the program’s currency is not found.
Reward Pool Calculation
Pool calculators return Amount objects:
// StandardRewardPoolCalculationService
$pool = $poolCalculator($binding->getProgramId(), $conversion, $transaction);
return new Amount($pool, $this->currencyProvider->buildCurrency($binding->getProgramId()));
// LeadRewardPoolCalculationService (fixed amount per lead)
$pool = (int) $this->incentiveConfigDatastore->getConfig($programId, 'payoutPerLead', 0);
return new Amount($pool, $this->currencyProvider->buildCurrency($programId));
The $pool value is always an integer in cents. The Amount wrapping adds currency context so downstream formatters know how to display it.
How do you access currencies at runtime?
The CurrencyProviderService is the central service for currency operations. Inject it via the interface Siren\Commerce\Interfaces\CurrencyProviderService:
interface CurrencyProviderService
{
public function getCurrencyFromCode(string $currencyCode): ?Currency;
public function getDefaultCurrency(): ?Currency;
public function setCurrency(Currency $currency);
public function getCurrencyIdentifiers(): array; // ['USD', 'EUR', ...]
public function getCurrencies(): array; // [Currency, Currency, ...]
}
The default currency is stored in the config table under group 'commerce', key 'currencyCode', context 'default'. It defaults to 'USD' if not set.
Summary of Conventions
Always store monetary values as integers — multiply by 100 when ingesting from platforms, and always use FloatToIntPriceAdapter::toInt() rather than casting manually. The value field in transaction details is per-unit; the system handles the value * quantity multiplication. Discounts should be negative (multiply the amount by -1). Each program can operate in a different currency, so currency is resolved from the program configuration. And Amount is a value object — create new instances rather than mutating existing ones.