feat(m2m): сквозной поток с веб-интерфейсами — lk-gateway BFF + admin UI + lk-emulator + mock NSD
Реализован M2-шаг-1: первый рабочий сквозной поток M2M-заявки от ЛК
через нашу систему и обратно, с двумя видимыми веб-интерфейсами.
internal/nsdadapter/mock/:
- mock NSDSender с реалистичным синтетическим Response и асинхронной
эмиссией Decision через настраиваемую задержку (Confirm/Reject/Timeout)
- использует собственный жизненный цикл, чтобы HTTP-контексты вызывающего
не прерывали эмиссию Decision до истечения DecisionDelay
internal/lkgateway/:
- REST по контракту ESIA Finance V1 (POST/GET/PATCH/list claims)
- admin web UI (/admin/, /admin/claims, /admin/claims/{id}, /admin/status):
- дашборд со статусом подсистем (postgres, crypto-service UDS,
nsd-adapter, lk-emulator callback) и счётчиками сделок
- журнал и карточка заявки с историей FSM, ответом НРД, решением
принимающей стороны и последним callback'ом
- in-memory SeedStore с 5 тестовыми клиентами и счетами депо
- фоновый consumeDecisions: подписан на mock.Sender.Decisions(),
применяет ApplyDecision и отправляет PATCH callback в ЛК
internal/lkemulator/:
- имитация ЛК клиента (порт 8083)
- веб-формы: журнал, форма «новая заявка», карточка заявки
- HTTP-клиент к lk-gateway (создание заявки + регистрация callback URL)
- приёмник PATCH callback'ов, локальное хранилище заявок,
автообновление страницы каждые 3 сек
cmd/lk-gateway/main.go и cmd/lk-emulator/main.go — заглушки заменены
на полные сервисы с graceful shutdown.
Сквозной поток проверен smoke-test'ом: подача заявки через форму
эмулятора → создание сделки в lk-gateway → Send в mock NSD →
эмиссия Decision через 3 сек → ApplyDecision → PATCH callback в ЛК →
эмулятор показывает confirmed. Дашборд lk-gateway: Total=1, Подтверждено=1.
make ci зелёный.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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 {
|
||||
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")
|
||||
|
||||
s.addAccount("11111111-1111-1111-1111-111111111111",
|
||||
"DP789456", "31MC0021900000F01", "P001", "7702070139")
|
||||
s.addAccount("11111111-1111-1111-1111-111111111111",
|
||||
"AA789451", "33MC0021900000F02", "F002", "7802031669")
|
||||
s.addAccount("22222222-2222-2222-2222-222222222222",
|
||||
"DP100200", "31MC0010000000A01", "A001", "7702070139")
|
||||
s.addAccount("33333333-3333-3333-3333-333333333333",
|
||||
"DP300400", "31MC0030000000B01", "B001", "0702345678")
|
||||
s.addAccount("55555555-5555-5555-5555-555555555555",
|
||||
"DP500600", "31MC0050000000C01", "C001", "0710987654")
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// 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),
|
||||
})
|
||||
}
|
||||
|
||||
// GetClientByID — реализация FansyStore.
|
||||
func (s *SeedStore) GetClientByID(_ context.Context, id string) (*m2mcore.Client, error) {
|
||||
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) {
|
||||
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)
|
||||
Reference in New Issue
Block a user