package lkgateway import ( "context" "fmt" "strings" "sync" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore" ) // SeedStore — in-memory FansyStore с предзаполненными тестовыми данными // (5 клиентов с депо-счетами и портфелями), соответствующими // docs/fansy-contract/v1/examples/seed-data.sql. Используется в // dev-стенде без реальной БД. type SeedStore struct { mu sync.Mutex // защищает динамическую регистрацию клиентов ЛК clients map[string]*m2mcore.Client accounts map[string][]m2mcore.DepoAccount // by client_id } // NewSeedStore собирает SeedStore с фиксированными тестовыми клиентами. func NewSeedStore() *SeedStore { s := &SeedStore{ clients: make(map[string]*m2mcore.Client), accounts: make(map[string][]m2mcore.DepoAccount), } s.addClient("11111111-1111-1111-1111-111111111111", "Иванов", "Иван", "Иванович", m2m.DocCode21, "4512", "654321") s.addClient("22222222-2222-2222-2222-222222222222", "Петров", "Пётр", "Петрович", m2m.DocCode21, "4513", "654322") s.addClient("33333333-3333-3333-3333-333333333333", "Сидоров", "Сидор", "Сидорович", m2m.DocCode21, "4514", "654323") s.addClient("44444444-4444-4444-4444-444444444444", "Кузнецов", "Сергей", "Михайлович", m2m.DocCode03, "111", "222333") s.addClient("55555555-5555-5555-5555-555555555555", "Соколова", "Анна", "Викторовна", m2m.DocCode21, "4516", "654325") // Тест-клиенты плана M2M (Приложение 2): К1 Бубликов, К2 Баранкин — те же, что у ЗЕНИТа и // ГроттБьерн (16.09.2026 ГроттБьерн подтвердил своих клиентов; Мостовиков не используется). // ФИО и документ — как в тест-плане (m2mTestClients), чтобы EnrichRequest // собирал СОГЛАСОВАННУЮ личность (раньше подставлялся Петров из 2222). s.addClient(seedClientK1, "Бубликов", "Павел", "Иванович", m2m.DocCode21, "3453", "289090") s.addClient(seedClientK2, "Баранкин", "Сергей", "Сергеевич", m2m.DocCode21, "3763", "290089") s.addAccount("11111111-1111-1111-1111-111111111111", "DP789456", "31MC0021900000F01", "P001", "7702070139") s.addAccount("11111111-1111-1111-1111-111111111111", "AA789451", "33MC0021900000F02", "F002", "7802031669") // Тест-инвестор робота: место расчётов = наш реальный тестовый счёт в НРД // (депкод MC0413600000, счёт HL171004001C, раздел 36MC0413600000F00, // расчётный депозитарий НРД ИНН 7702165310). Иначе НРД отвечает M2M19 // «недопустимое место расчетов». s.addAccount("22222222-2222-2222-2222-222222222222", "MC0413600000", "HL171004001C", "36MC0413600000F00", "7702165310") s.addAccount("33333333-3333-3333-3333-333333333333", "DP300400", "31MC0030000000B01", "B001", "0702345678") s.addAccount("55555555-5555-5555-5555-555555555555", "DP500600", "31MC0050000000C01", "C001", "0710987654") // К1/К2 списывают с нашего реального тестового счёта в НРД (мы — Списывающий), // то же место расчётов, что у тест-инвестора робота (иначе НРД: M2M19). s.addAccount(seedClientK1, "MC0413600000", "HL171004001C", "36MC0413600000F00", "7702165310") s.addAccount(seedClientK2, "MC0413600000", "HL171004001C", "36MC0413600000F00", "7702165310") return s } // Идентификаторы сид-клиентов тест-плана M2M (реальный депозитарий, не робот). const ( seedClientK1 = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" // Бубликов Павел Иванович (К1) seedClientK2 = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" // Баранкин Сергей Сергеевич (К2) ) // Clients возвращает копию слайса клиентов (для UI выбора). func (s *SeedStore) Clients() []*m2mcore.Client { out := make([]*m2mcore.Client, 0, len(s.clients)) for _, c := range s.clients { out = append(out, c) } return out } func (s *SeedStore) addClient(id, last, first, middle string, doc m2m.IdentityDocumentCode, series, number string) { s.clients[id] = &m2mcore.Client{ ID: id, LastName: last, FirstName: first, MiddleName: middle, Document: m2mcore.ClientDocument{ DocumentType: doc, Series: series, Number: number, }, } } func (s *SeedStore) addAccount(clientID, dep, acc, sec, depINN string) { s.accounts[clientID] = append(s.accounts[clientID], m2mcore.DepoAccount{ ClientID: clientID, DeponentCode: dep, AccountID: m2m.AccountID(acc), SectionID: sec, DepositoryINN: m2m.OrganizationINN(depINN), }) } // EnsureLKClient регистрирует (или обновляет) клиента Личного кабинета в // справочнике: анкета берётся из учётной записи ЛК, депо-счета — из его // портфеля (bj_client_positions). Нужно, чтобы EnrichRequest мог собрать // M2MTransferRequest по заявке, поданной клиентом из кабинета. func (s *SeedStore) EnsureLKClient(u LKUser, positions []ClientPosition, settle m2mcore.DepoAccount) { s.mu.Lock() defer s.mu.Unlock() last, first, middle := u.LastName, u.FirstName, u.MiddleName if last == "" && u.FullName != "" { parts := strings.Fields(u.FullName) if len(parts) > 0 { last = parts[0] } if len(parts) > 1 { first = parts[1] } if len(parts) > 2 { middle = parts[2] } } docType := u.DocType if docType == "" { docType = "21" // паспорт РФ } s.clients[u.Contract] = &m2mcore.Client{ ID: u.Contract, LastName: last, FirstName: first, MiddleName: middle, Document: m2mcore.ClientDocument{ DocumentType: m2m.IdentityDocumentCode(docType), Series: u.DocSeries, Number: u.DocNumber, }, } // Депо-счета клиента — уникальные комбинации НРД-реквизитов места расчётов. // У позиций из таблицы обмена реквизиты свои; у депозитарных позиций их нет // (только внутренний счёт ВТНКЦ…) — тогда берём фиксированные реквизиты нашего // депозитария в НРД (settle: deponent/account/section/ИНН из настроек). seen := map[string]bool{} var accs []m2mcore.DepoAccount for _, p := range positions { a := m2mcore.DepoAccount{ ClientID: u.Contract, DeponentCode: p.DeponentCode, AccountID: m2m.AccountID(p.AccountID), SectionID: p.SectionID, DepositoryINN: m2m.OrganizationINN(p.SettlementIN), } if a.DeponentCode == "" { a.DeponentCode = settle.DeponentCode } if a.AccountID == "" { a.AccountID = settle.AccountID } if a.SectionID == "" { a.SectionID = settle.SectionID } if a.DepositoryINN == "" { a.DepositoryINN = settle.DepositoryINN } key := a.DeponentCode + "|" + string(a.AccountID) + "|" + a.SectionID if a.AccountID == "" || seen[key] { continue } seen[key] = true accs = append(accs, a) } if len(accs) == 0 && settle.AccountID != "" { // вообще нет позиций с реквизитами — один счёт по настройкам settle.ClientID = u.Contract accs = append(accs, settle) } if len(accs) > 0 { s.accounts[u.Contract] = accs } } // GetClientByID — реализация FansyStore. func (s *SeedStore) GetClientByID(_ context.Context, id string) (*m2mcore.Client, error) { s.mu.Lock() defer s.mu.Unlock() c, ok := s.clients[id] if !ok { return nil, fmt.Errorf("seedstore: клиент %s не найден", id) } return c, nil } // GetDepoAccounts — реализация FansyStore. func (s *SeedStore) GetDepoAccounts(_ context.Context, clientID string, _ m2m.OrganizationINN) ([]m2mcore.DepoAccount, error) { s.mu.Lock() defer s.mu.Unlock() accs := s.accounts[clientID] if len(accs) == 0 { return nil, fmt.Errorf("seedstore: нет счетов у клиента %s", clientID) } return accs, nil } // GetBalances — реализация FansyStore. На M2 возвращает пустой список, // потому что баланс проверяется при подаче заявки в самой UI (через демо-кнопку). func (s *SeedStore) GetBalances(_ context.Context, _ string, _ []m2m.SecurityCode) ([]m2mcore.SecurityBalance, error) { return nil, nil } // Verify тип SeedStore удовлетворяет m2mcore.FansyStore. var _ m2mcore.FansyStore = (*SeedStore)(nil)