Multi-currency model
casinocrm.io ukládá každou peněžní částku DUAL: originál (měna peněženky hráče) + base-converted (reporting měna tenanta), s FX kurzem zafixovaným v čase události. Reporty se vždy renderují v base tenanta; profil hráče zobrazí native peněženku.
Proč DUAL ukládání
Naivní single-currency CRMs konvertují at query time pomocí current FX. To způsobí drift lifetime metrik — "lifetime deposit" hráče skáče, jak se mění FX. DUAL pattern v casinocrm.io ukládá converted částku v čase události, takže historické metriky zůstávají stabilní.
Konvence ukládání
- Subunit BIGINT — každá částka uložená jako integer v nejmenší jednotce. EUR €123.45 =
12345; BTC 0.00012345 =12345. - Currency code — spárováno s každou částkou.
"EUR","USD","BTC","USDT","ETH". - Scale — desetinná místa pro měnu. EUR/USD = 2, BTC = 8, USDT = 6, JPY = 0.
- Crypto cap — max 8 desetinných míst (industry standard). ETH wei (18 desetinných) přetéká BIGINT a je pro CRM display nepraktický.
Currency catalogue
Každý tenant má currencies katalog. Migrace seedne EUR + USD pro každého tenanta; operátoři přidávají krypto podle potřeby.
SELECT code, kind, scale, symbol, display_name, is_base
FROM currencies WHERE tenant_id = $1;
code | kind | scale | symbol | display_name | is_base
------+--------+-------+--------+--------------+---------
EUR | fiat | 2 | € | Euro | t
USD | fiat | 2 | $ | US Dollar | f
BTC | crypto | 8 | ₿ | Bitcoin | f
USDT | crypto | 6 | USDT | Tether | f
ETH | crypto | 8 | Ξ | Ethereum | f
Přesně jedna měna per tenant má is_base = true; to je reporting měna.
FX kurzy
fx_rates je per-tenant timeline. FX fetcher (pnpm --filter @ucrm/api fx:fetch) běží hodinově a:
- Stahuje FIAT páry z Frankfurter (ECB-backed, bez auth)
- Stahuje krypto páry z CoinGecko (no-auth tier)
- Upsertuje řádky s
(tenant_id, base, quote, ts)PK +sourcefield
Lookup je WHERE tenant_id = ? AND base = ? AND quote = ? AND ts <= ? ORDER BY ts DESC LIMIT 1 — nejnovější kurz at-or-před event time.
DUAL sloupce na transakčních tabulkách
bonus_grants (vystaveno v event time):
bonus_amount BIGINT NOT NULL, -- originál (např. 1000 USD cents)
bonus_currency TEXT NOT NULL, -- "USD"
bonus_amount_base BIGINT NOT NULL, -- např. 910 EUR cents (zafixováno)
base_currency_code TEXT NOT NULL, -- "EUR"
fx_rate_locked NUMERIC(28, 12) NOT NULL,
fx_locked_at TIMESTAMPTZ NOT NULL
Stejný DUAL pattern na wagering_events. ClickHouse raw_events properties se obohatí o amount_base + base_currency + fx_rate_locked v insert time, takže dashboard queries SUM(amount_base) fungují koherentně napříč heterogenními wallet měnami.
Per-player wallet měna
players.default_currency_code je primární wallet měna hráče, captured na prvním deposit_confirmed. Do té doby NULL → admin UI zobrazuje tenant base. Po nastavení profil hráče renderuje částky nativně (₿0.0023 BTC) plus base ekvivalent v tooltipu.
Querying
- Reporty + segmenty — query přes
amount_base_subunitsloupce neboJSONExtractInt(properties, 'amount_base')v CH. Vždy koherentní. - Player profile — zobrazte native + base (už poskytuje schéma).
- Bonus issuance — předejte
currency_override, pokud má být bonus denominován v něčem jiném než wallet měna hráče; engine zafixuje FX v issue time.