Multi-currency модель
Translator note: machine-translated baseline. Needs human review by a native UK speaker. EN source is canonical.
casinocrm.io зберігає кожну грошову суму DUAL: оригінал (валюта гаманця гравця) + base-converted (reporting валюта tenant), з FX курсом зафіксованим у час події. Звіти завжди рендеряться в base tenant; profile гравця показує native гаманець.
Чому DUAL зберігання
Наївні single-currency CRMs конвертують at query time, використовуючи поточні FX курси. Це робить lifetime metrics drifting — "lifetime deposit" гравця стрибає, коли FX курси змінюються. casinocrm.io's DUAL pattern зберігає converted суму в event time, тож історичні метрики залишаються стабільними.
Конвенція зберігання
- Subunit BIGINT — кожна сума зберігається як integer у найменшій одиниці. EUR €123.45 =
12345; BTC 0.00012345 =12345. - Currency code — спарований з кожною сумою.
"EUR","USD","BTC","USDT","ETH". - Scale — десяткові місця для валюти. EUR/USD = 2, BTC = 8, USDT = 6, JPY = 0.
- Crypto cap — 8 десяткових max (industry standard). ETH wei (18 десяткових) переповнює BIGINT і непрактичний для CRM display.
Currency catalogue
Кожен tenant має currencies каталог. Migration seedить EUR + USD для кожного tenant; оператори додають крипту за потреби.
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
Рівно одна валюта per tenant має is_base = true; це reporting валюта.
FX курси
fx_rates — це per-tenant timeline. FX fetcher (pnpm --filter @ucrm/api fx:fetch) запускається щогодини і:
- Тягне FIAT пари з Frankfurter (ECB-backed, без auth)
- Тягне crypto пари з CoinGecko (no-auth tier)
- Upsert-ить rows з
(tenant_id, base, quote, ts)PK +sourcefield
Lookup це WHERE tenant_id = ? AND base = ? AND quote = ? AND ts <= ? ORDER BY ts DESC LIMIT 1 — найновіший курс at-or-before event time.
DUAL columns на transactional таблицях
bonus_grants (видається в event time):
bonus_amount BIGINT NOT NULL, -- оригінал (наприклад, 1000 USD cents)
bonus_currency TEXT NOT NULL, -- "USD"
bonus_amount_base BIGINT NOT NULL, -- наприклад, 910 EUR cents (locked)
base_currency_code TEXT NOT NULL, -- "EUR"
fx_rate_locked NUMERIC(28, 12) NOT NULL,
fx_locked_at TIMESTAMPTZ NOT NULL
Той самий DUAL pattern на wagering_events. ClickHouse raw_events properties збагачуються amount_base + base_currency + fx_rate_locked в insert time, тож dashboard queries SUM(amount_base) працюють когерентно через гетерогенні wallet валюти.
Per-player wallet валюта
players.default_currency_code — primary wallet валюта гравця, captured на першому deposit_confirmed. До того NULL → admin UI показує tenant base. Після встановлення player profile рендерить суми нативно (₿0.0023 BTC) плюс base equivalent у tooltip.
Querying
- Reports + segments — query
amount_base_subunitcolumns абоJSONExtractInt(properties, 'amount_base')у CH. Завжди когерентно. - Player profile — показує native + base (вже надано schema).
- Bonus issuance — передайте
currency_override, якщо bonus має бути denominated у чомусь, ніж wallet валюта гравця; engine locks FX в issue time.