Building a SaaS product in the MENA region means dealing with a mix of currencies that behave differently from anything you encounter in single-currency Western markets. The Lebanese pound in particular forces you to rethink assumptions about how monetary amounts are stored and displayed.
Building a SaaS product in the MENA region means dealing with a mix of currencies that behave differently from anything you encounter in single-currency Western markets. The Lebanese pound forces you to rethink assumptions about how monetary amounts are stored and displayed. The AED and SAR are pegged to the dollar but require separate invoice lines for legal and accounting reasons. Some clients pay in USD regardless of where they are based.
Multi-currency billing is not a display formatting problem. It is a data modeling problem, a business logic problem, and an accounting problem simultaneously.
The fundamental data modeling mistake
Most teams approach multi-currency billing by storing amounts as floating-point numbers with a currency code attached. This causes problems that compound over time.
Floating-point arithmetic is not suitable for monetary calculations. Floating-point arithmetic on the number 0.1 does not produce 0.1 in binary. Multiply 0.1 by three in most programming languages and the result is 0.30000000000000004. This is academically interesting and commercially catastrophic when it shows up in an invoice total.
The correct model: store monetary amounts as integers in the smallest unit of the currency. For USD, store cents. For AED, store fils. For SAR, store halalas.
type Money struct {
Amount int64
Currency string // ISO 4217: "USD", "AED", "SAR", "LBP"
}
The Lebanese pound complicates this immediately. The smallest unit of the LBP is the piastre, but piastres have not been practically meaningful for decades. You will encounter amounts like 7,500,000 LBP for a monthly software subscription. Storing this as 750,000,000 piastres (in the smallest unit) introduces no mathematical problems but requires displaying it correctly to users who expect to see "LBP 7,500,000" not "LBP 750,000,000".
The solution: maintain a currency registry that maps ISO codes to their decimal places for display purposes:
var currencyDecimals = map[string]int{
"USD": 2, // $1.00 = 100 cents
"AED": 2, // 1 AED = 100 fils
"SAR": 2, // 1 SAR = 100 halalas
"LBP": 0, // LBP has no meaningful subunit in practice
"EGP": 2, // 1 EGP = 100 piastres
"KWD": 3, // 1 KWD = 1000 fils
}
func FormatAmount(m Money) string {
decimals := currencyDecimals[m.Currency]
if decimals == 0 {
return fmt.Sprintf("%s %s", m.Currency, formatInteger(m.Amount))
}
divisor := math.Pow10(decimals)
whole := m.Amount / int64(divisor)
frac := m.Amount % int64(divisor)
return fmt.Sprintf("%s %d.%0*d", m.Currency, whole, decimals, frac)
}
Pricing: tenant currency vs billing currency vs reporting currency
In a MENA SaaS with multi-currency billing, you typically have three distinct concepts that should not be conflated:
The tenant's chosen display currency is what they see in the UI. A Lebanese business might want to see everything in USD even though they pay from a Lebanese bank account.
The billing currency is the currency of the invoice and the actual payment. This affects which payment method is available (LBP transfers have different mechanics than USD wire transfers) and the legal denomination of the contractual obligation.
The reporting currency is what you use internally for consolidated revenue and MRR calculations. Most MENA SaaS companies report internally in USD regardless of what currencies they bill in.
Keep these three currencies distinct in your data model:
CREATE TABLE subscriptions (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
plan_id UUID NOT NULL,
display_currency TEXT NOT NULL DEFAULT 'USD',
billing_currency TEXT NOT NULL,
billing_amount BIGINT NOT NULL, -- in smallest unit of billing_currency
reporting_amount BIGINT NOT NULL, -- in USD cents, at time of subscription creation
exchange_rate NUMERIC(18,8), -- rate used for reporting conversion
rate_locked_at TIMESTAMPTZ
);
The reporting_amount should be calculated and locked when the subscription is created, not recalculated on every report. This ensures your historical MRR figures are stable even as exchange rates move.
When to lock exchange rates
A common mistake: calculating exchange rates at invoice generation time. If you generate invoices monthly and LBP has moved against USD since the subscription was created, your reported revenue changes retroactively. This makes financial reporting unreliable.
For subscriptions, lock the exchange rate at creation and do not change it unless the customer explicitly changes their plan or currency. The rate used for the reporting_amount represents the revenue you recognized at the time of commitment, not what the market is doing today.
For one-time charges and usage-based billing, lock the exchange rate at the time the charge is generated, not when the invoice is sent or paid.
LBP deserves special mention. The Lebanese pound has experienced significant volatility over the past several years. If you allow billing in LBP, you need a clear policy for how rates are updated and communicated to tenants. Many SaaS companies operating in Lebanon choose to bill in USD and accept LBP payments at a market rate at payment time, treating the exchange as the tenant's responsibility rather than the platform's. This simplifies your accounting considerably.
Invoice generation across currencies
Each invoice should be denominated in a single currency: the billing currency for that subscription. Do not generate invoices that mix currencies.
For tenants with subscriptions in multiple currencies (rare but possible), generate a separate invoice per currency. Trying to collapse a USD subscription and an AED subscription into a single invoice creates accounting complexity that your clients' finance teams will not appreciate.
Invoice line items must show the amount in the billing currency. If you have internal costs or fees denominated in a different currency, convert them before showing them on the invoice and disclose the conversion rate used.
type InvoiceLineItem struct {
Description string
Quantity int
UnitAmount Money
TotalAmount Money
TaxAmount Money
ExchangeRate *decimal.Decimal // nil if no conversion needed
OriginalAmount *Money // nil if no conversion needed
}
Payment processing in MENA
Stripe supports most MENA currencies, but with limitations. AED and SAR are well-supported. LBP is not supported by most international payment processors, which means LBP billing typically flows through bank transfer outside of your payment processor, with manual reconciliation.
This is the reality of operating in Lebanon: some payments cannot be automated through standard payment infrastructure. Build your system to handle a mix of automated card payments, automated bank transfers, and manual transfer confirmations without requiring special-casing in your core billing logic.
The cleanest model: treat every payment as pending until confirmed, regardless of whether confirmation comes from a Stripe webhook or a manual entry by your operations team. The code path for marking a payment as confirmed should not know or care how the confirmation was generated.
Reporting and reconciliation
MRR calculations in a multi-currency system require picking a reporting currency and applying exchange rates consistently. Use the locked rate stored with each subscription rather than a live rate. This produces stable MRR figures that do not fluctuate with the currency market.
For reconciliation, store every amount twice: in the transaction currency and in the reporting currency, with the exchange rate used for the conversion. This lets you reconstruct how a reporting figure was calculated even years later.
Key lessons from production
Never use floating-point for monetary amounts. Use integer arithmetic in the smallest currency unit. This eliminates an entire class of rounding bugs.
Lock exchange rates at transaction time. Never recalculate historical figures with current rates.
Keep display currency, billing currency, and reporting currency as separate concepts. They look the same at the start and diverge as the product grows.
Build manual payment confirmation into the core billing flow from the beginning. In Lebanon and parts of the broader MENA region, some payment flows cannot be fully automated, and retrofitting manual payment support into an automated-only system is more painful than including it from the start.
Enjoying this article?
Enter your email and get a clean, formatted PDF of this article - free, no spam.
Not sure where to start?
Voxire builds SaaS billing systems for products serving Lebanon and the broader MENA region, including multi-currency support, payment reconciliation workflows, and integration with regional payment infrastructure.
https://voxire.com/get-a-quote/



