Счёт = инструмент = валюта + способ. EUR на Revolut и EUR наличными — это разные инструменты, разные счета, разные балансы.
EUR-REVOLUT ≠ EUR-CASH. USDT-TRC20 ≠ USDT-ERC20. Один инструмент — один счёт у контрагента. Это снимает проблему «EUR на Revolut и EUR кэшем — это разные деньги». Да, разные — разные инструменты, разные счета.
AccountEntity: partner_id → PartnerEntity location_id → LocationEntity ← привязка к офису currency_code → "EUR" ← не отличает способ balance → 5000.00 Проблема: EUR cash и EUR Revolut = один и тот же счёт (оба "EUR")
AccountEntity: partner_id → PartnerEntity instrument_id → InstrumentEntity ← содержит всё balance → 5000.00 hold → 0.00 Решено: EUR-CASH и EUR-REVOLUT = два разных счёта
// Примеры инструментов EUR-CASH → Евро, наличные (без привязки к локации) EUR-REVOLUT → Евро на Revolut USDT-TRC20 → USDT в сети Tron USDT-ERC20 → USDT в сети Ethereum (другой инструмент!) USDT-BINANCE → USDT на бирже Binance (отдельно от on-chain!) RUB-CASH → Рубли, наличные RUB-SBER → Рубли, Сбербанк BTC-ONCHAIN → Биткоин в блокчейне // Пример счетов клиента Васи у Альфы: Вася + EUR-REVOLUT : EUR 5,000 Вася + EUR-CASH : EUR 3,000 ← отдельно! Вася + USDT-TRC20 : 10,000 Вася + USDT-BINANCE : 8,000 ← отдельно от on-chain!
Раньше: LocationEntity = офис + валюты + балансы. Теперь: InstrumentEntity содержит всё что нужно. Если брокеру нужен «офис в Барселоне» — это тег #barcelona на инструментах. Фильтруешь по тегу — видишь «кассу Барселоны».
// РАНЬШЕ: LocationEntity = офис + валюты + балансы + rate modifiers AccountEntity = partner × location × currency // ТЕПЕРЬ: InstrumentEntity = валюта + способ (всё что нужно) AccountEntity = partner × instrument LocationEntity → УДАЛЕНА из финансовой модели // Если нужно понятие «офис» — это тег: EUR-CASH + tag #barcelona → касса Барселоны EUR-CASH + tag #vilnius → касса Вильнюса // "Касса Барселоны" = фильтр по тегу, а не сущность
Альфа создал «EUR-REVOLUT» с тегами [#ewallet, #europe]. Бета нашла в каталоге, подключила. Оба говорят на одном языке. Тег «ewallet» или «безнал» — неважно для операции. Важно что код совпадает: оба понимают ЧТО это.
// Пример жизненного цикла: 1. Брокер Альфа создаёт EUR-CASH-BCN (private, usage_count=1) 2. Бета находит в каталоге, подключает (usage_count=2) 3. Ещё 3 брокера подключают (usage_count=5) 4. Автоматически: scope = 'community' (виден всем в каталоге) 5. Платформа стандартизирует: scope = 'system' (предустановлен для новых)
Ключ меняется с (partner_id, location_id, currency_code) на (partner_id, instrument_id). Стратегия: nullable instrument_id + dual-write. Текущий бизнес не должен сломаться.
instrument_id (nullable FK). Старые поля (location_id, currency_code) остаются. Dual-write: при любой записи заполнять оба.// Dual-write период (безопасность) function createAccount($partnerId, $instrumentId) { $instrument = InstrumentEntity::find($instrumentId); $account = new AccountEntity(); $account->partner_id = $partnerId; $account->instrument_id = $instrumentId; // ← новый путь $account->currency_code = $instrument->currency_code; // ← legacy (dual-write) $account->location_id = null; // ← deprecated $account->balance = 0; $account->hold = 0; return $account->save(); } // available = balance - hold (вычисляемое) function getAvailable(): Decimal { return bcsub($this->balance, $this->hold); }