OMR money — bcmath scale 3, never floats
Omani Rial uses 3 decimal places (1 OMR = 1000 baisa). Float arithmetic produces 0.1 + 0.2 ≠ 0.3 — for money, that's catastrophic. AuditPro forbids floats; every monetary computation goes through bcmath at scale 3. Storage is DECIMAL(18,3). Rounding is banker's-rounding via explicit bcadd zero-string trick.
Why floats are forbidden
# Float arithmetic — DON'T do this for money:
>>> 0.1 + 0.2
0.30000000000000004 # NOT 0.3 — 53-bit float can't represent decimal exactly
# bcmath — DO this:
>>> bcadd('0.1', '0.2', 3)
'0.300' # Exact
An audit firm cannot tolerate this. A 0.001 OMR drift across 200 invoice lines = the books don't balance = audit qualification.
The 5 bcmath operators we use
| Operator | Use | Always pass scale |
|---|---|---|
bcadd($a, $b, 3) | Addition | 3 |
bcsub($a, $b, 3) | Subtraction | 3 |
bcmul($a, $b, 3) | Multiplication | 3 |
bcdiv($a, $b, 3) | Division | 3 |
bccomp($a, $b, 3) | Comparison (returns -1/0/1) | 3 |
Working pattern — invoice line total
// Inputs always strings or string-cast numbers from DB
$qty = '8.000';
$unitPrice = '125.000';
$discount = '10.0'; // percent
$vatRate = '5.0'; // percent
// Line subtotal = qty × price × (1 - discount/100)
$gross = bcmul($qty, $unitPrice, 3); // '1000.000'
$discFactor = bcsub('1', bcdiv($discount, '100', 4), 4); // '0.9000'
$subtotal = bcmul($gross, $discFactor, 3); // '900.000'
// VAT
$lineVat = bcmul($subtotal, bcdiv($vatRate, '100', 4), 3); // '45.000'
// Line total
$lineTotal = bcadd($subtotal, $lineVat, 3); // '945.000'
Storage
| Column type | Use |
|---|---|
| DECIMAL(18,3) | OMR amounts. 18 total digits, 3 after decimal. Max ≈ 999,999,999,999,999.999 (more than enough) |
| DECIMAL(5,2) | Percentages (discount, VAT rate) |
| DECIMAL(10,6) | FX rates (need more precision) |
Never use FLOAT, DOUBLE, or REAL for money. Migrations enforce this.
Rounding rules
bcmath truncates toward zero by default at the requested scale. For banker's rounding (half-to-even), AuditPro uses an explicit utility:
// Helper: round to 3 decimals, half-up
function moneyRound(string $value): string {
return bcadd($value, '0.0005', 4); // bias up by half-tick
// then truncate via bcadd($x, '0', 3) for final 3-decimal value
}
VAT computation rule
Per OTA: VAT computed line-by-line, NOT on the rolled-up subtotal. Worked example:
| Line | Subtotal | VAT 5% per-line | Total per-line |
|---|---|---|---|
| A | OMR 100.005 | 5.000 (truncated to 3 dp) | 105.005 |
| B | OMR 200.015 | 10.001 | 210.016 |
| Σ | 300.020 | 15.001 | 315.021 |
If VAT were rolled up (300.020 × 5% = 15.001), the result is the same here, but for floating-point boundary cases, line-by-line is safer + auditor-defensible.
Currency conversion
If invoice is in non-OMR (e.g. USD), the system stores both:
amount+currency_code— originalfx_rate— rate at invoice issuanceomr_equivalent— pre-computed for reporting consistency
FX gain/loss on settlement is currently out of scope (deferred until needed). For now, the invoice receives payment in the original currency + same fx_rate is used for the receipt.
Display formatting
The Helpers/Currency utility provides Currency::format($value, $currency = 'OMR'):
Currency::format('1234567.890', 'OMR')
// → 'OMR 1,234,567.890'
Currency::format('5000', 'OMR')
// → 'OMR 5,000.000'
Currency::format('-250.500', 'OMR')
// → '(OMR 250.500)' // accounting convention for negative
In any PHP file, run echo bcadd('0.1', '0.2', 3); — outputs 0.300 (exact). Now run echo 0.1 + 0.2; — outputs 0.30000000000000004. That tiny difference, multiplied across hundreds of invoice lines, is why we never use floats for money.
If you call any money method with a PHP float (e.g. 4.5 instead of '4.500'), bcmath will silently lose precision because PHP casts float → string with finite digits. Always pass strings. The DB returns strings already; if you cast to int/float for typing, you've broken the chain.
Don't use PHP == or === on bcmath strings: '5.000' == '5' returns false (string comparison). Use bccomp($a, $b, 3) === 0 for equality. The system encapsulates this in a Currency::eq($a, $b) helper for clarity.