User guide
Multi-Currency
Looking for what it does rather than how to use it? Read the Multi-Currency overview .
How Solidlio decides what currency a number is in, when it converts one, and how it is displayed.
What it is
Solidlio prices, invoices and settles in three currencies: CAD, USD and EUR. Every money record carries the currency it was written in, conversion happens at exactly four defined points, and everywhere else the platform refuses to mix currencies rather than guessing a rate.
Concepts
| Concept | What it means | Where it lives |
|---|---|---|
| Account currency | The default currency an account bills and is billed in. Set from its region at creation: CA→CAD, US→USD, EU→EUR. | Account.defaultCurrency |
| Org sell currency | An optional per-organization override. Lets an MSP sell one client in USD while its account default is CAD. | Organization.sellCurrency (nullable) |
| Sell currency | The currency a given buyer is quoted in. Org override → buyer account default → seller account default. | resolveSellCurrency, resolveOrgCurrency |
| Buy currency | The currency a distributor invoices Solidlio in. Every product cost derived from that distributor is in it. | Distributor.costCurrency, CatalogProduct.costCurrency |
| Payout currency | The currency an MSP is paid out in — its region currency, fixed when its reserve is first created. Not the sell currency. | MspReserveAccount.currency, MspPayout.currency |
| Point-in-time currency | A document’s currency is stamped when it is created and never recalculated. An invoice raised in USD stays USD. | Invoice.currency, Quote.currency, MspStoreOrder.currency |
| Exchange rate | One directed pair (USD → CAD) with a rate, a provider quote date, and the instant it was last confirmed. | FxRate |
Sell currency and payout currency are different things. A US MSP selling to a Canadian client quotes that client in CAD and is paid out in USD. The conversion between them happens once, at payout.
The four places conversion happens
Everywhere else, Solidlio refuses rather than converts.
| # | Where | Direction | On a missing or stale rate |
|---|---|---|---|
| 1 | Store pricing | product buy currency → buyer sell currency | The product is returned with priceable: false and zero prices |
| 2 | Store checkout | seller account currency → buyer sell currency | The line is unavailable and the order is refused with 400 |
| 3 | MSP payout | each sale’s sell currency → MSP region currency | The whole account is skipped; its sales roll into the next run |
| 4 | AP→A/R rebill | vendor invoice currency → client sell currency | The rebill is refused with 400 naming both currencies |
A chargeback is converted too — from the dispute’s currency into the reserve’s — but it is never refused, because a dispute is a debt that has already happened. If no rate exists it is booked 1:1 and logged at error, with Stripe’s original amount and currency preserved on the row for correction.
Roles and permissions
Exchange rates are platform reference data, not tenant data. There is deliberately no per-account rate: a tenant that could write its own rate could rewrite what it owes.
Walkthroughs
Bill one client in a different currency from the rest
- Go to Settings → Organizations → [the client].
- Set Sell currency to USD (or EUR). “Account default” leaves it inherited.
- Save.
From that point, new quotes, invoices, store carts, store orders, contracts, credit limits and project invoices for that organization are created in USD. Documents already issued keep the currency they were issued in — an invoice is a statement of what was owed at the time, and re-denominating it after the fact would make it disagree with what the client already received.
Store prices for that client are converted from each product’s buy currency into USD before any markup is applied, so the markup percentage means the same thing whatever the product cost.
Price a plan tier in USD or EUR
The tier’s own price fields are its CAD price. Other currencies are separate rows.
- Go to Platform → Service Tiers, open a tier, scroll to Regional prices.
- Enter the USD and/or EUR monthly, yearly and per-seat prices.
- Save.
Until a tier has a price row for an account’s currency, platform billing holds that account’s invoice as DRAFT rather than charging the CAD number labelled as USD. A held draft is never auto-charged and never dunned. Set its
Clearing a regional price returns that currency to the DRAFT hold.
Check and correct an exchange rate
- Go to Platform → Exchange Rates.
- The table shows all six directed pairs among CAD, USD and EUR — the rate, where it came from, when it was last confirmed, and whether it is stale.
- To pin a rate, press Override, enter it, add a reason, and save.
- To go back to the live feed, press Use feed.
An override wins over the provider feed for that pair and is exempt from the freshness bound until you remove it. Both actions are written to the platform audit log, because an override moves real money.
Rebill a foreign vendor invoice to a client
- Open the vendor invoice under Purchasing → Vendor Invoices.
- Generate the customer invoice as usual.
If the vendor billed in USD and the client is billed in CAD, the cost is converted at the current rate, the markup is applied to the converted figure, and the customer invoice is issued in CAD. The link row records the vendor currency, the client currency and the rate used, so the rebill can be reconstructed. If no rate is available the request is refused rather than issuing a receivable at a guessed rate.
Configuration
| Setting | What it does | If unset |
|---|---|---|
Account.defaultCurrency | The account’s billing currency. | Derived from region at creation; CAD for anything else |
Organization.sellCurrency | Per-org override of the sell currency. | Inherits the account default |
Distributor.costCurrency | The currency a distributor invoices in. | CAD |
CatalogProduct.costCurrency | The currency a product’s cost is recorded in. | Stamped from the distributor during sync; CAD otherwise |
MspPricingProfile.currencyBuffer | A hedge added to a converted cost on cross-currency sales only. 0.03 = 3%. Range 0–0.5. | No buffer |
ServiceTierPrice | A tier’s fixed price in USD or EUR. | Platform billing holds that account’s invoice as DRAFT |
Anything outside CAD/USD/EUR that reaches a currency field is clamped to CAD rather than persisted, so a stray code cannot become a currency the platform cannot price.
How rates are kept current
- Provider: European Central Bank reference rates, via frankfurter. No key required.
- Schedule: daily at 05:30 UTC (
fx-rate-sync). ECB publishes around 16:00 CET, so the run picks up the previous publication. - Coverage: all six directed pairs among CAD, USD and EUR. Rates are stored at
Decimal(18,8). - On provider failure: the run fails soft per base currency — yesterday’s rows stay in place and the other bases still refresh. A partial failure raises a platform alert.
- Freshness bound: 48 hours, enforced on store pricing, checkout and payouts.
- Manual overrides never expire. That is the point of them.
Troubleshooting
“No exchange rate to price <SKU> in <CUR>.” Checkout could not convert this product’s cost currency into the buyer’s currency. Either the pair has no rate at all, or the rate is older than 48 hours. Check Platform → Exchange Rates; a stale pair is flagged there, and the page warns when any pair has expired.
A store product shows no price and cannot be added to the cart. Same cause. The list and detail routes return priceable: false for a product they cannot convert, rather than showing $0.00 and failing at checkout.
“No exchange rate is available to rebill a <CUR> vendor invoice to a <CUR> client.” The AP→A/R markup path needs a rate between the vendor’s currency and the client’s. Set one under Platform → Exchange Rates, or bill that client in the vendor’s currency.
“This invoice is issued in <CUR>; a payment recorded in <CUR> cannot be applied to it.” A manual receipt has to be in the invoice’s currency. Solidlio does not convert here on purpose: a foreign cheque settles at the bank’s rate on the day it clears, which the platform does not know. Record the settled amount in the invoice’s currency, or issue a credit note.
A credit note will not apply — CURRENCY_MISMATCH. The credit note and the invoice are in different currencies. Credits are applied at face value, never converted.
A platform invoice is stuck in DRAFT and was never charged. The account’s currency has no ServiceTierPrice row for its tier. This is deliberate — the alternative is charging the CAD number labelled as USD. Set the regional price under Platform → Service Tiers and the next run will bill it.
An MSP payout did not run. The payout sweep is all-or-nothing per account: if any settled sale cannot be converted into the MSP’s region currency with a fresh rate, the whole account is skipped and its sales roll into the next run. Check the pair under Platform → Exchange Rates.
“Send costCurrency alongside costPrice.” The catalog is shared across every tenant, so changing a product’s cost without saying what currency it is in would silently re-price it in every store that sells it.
Limits and known behaviour
- Three currencies: CAD, USD, EUR. Anything else is clamped to CAD. Adding a fourth means adding it to
SUPPORTED_CURRENCIESand pricing every tier in it. - All three are two-decimal currencies. Every money path assumes 100 minor units. A zero-decimal currency such as JPY would need that assumption revisited before it could be added.
- Documents are never re-denominated. Changing an account or organization currency affects new records only.
- Solidlio does not convert at settlement. A payment must be recorded in the invoice’s currency; a credit note must match the invoice it is applied to.
- Reporting totals are single-currency. Dashboard figures report one currency and say which. Money in other currencies is listed separately, never added in.
- Cost comparison across distributors is normalised, not tolerant. A distributor whose currency cannot be converted into a product’s cost currency is excluded from that product’s lowest-cost and average-cost figures.
- The currency buffer applies only to cross-currency sales, and only to the already-converted cost, before any markup.