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,211 @@
|
||||
// Package lkgateway реализует REST API контракта ESIA Finance V1
|
||||
// (docs/lk-contract/v1/openapi.yaml) и admin web-интерфейс.
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
|
||||
)
|
||||
|
||||
// CreateClaimRequest — DTO входа POST /api/v1/back_office/claims/.
|
||||
type CreateClaimRequest struct {
|
||||
Investor Investor `json:"investor"`
|
||||
TransferringDepositoryINN string `json:"transferring_depository_inn"`
|
||||
ReceivingDepositoryINN string `json:"receiving_depository_inn"`
|
||||
CostInfo CostInfo `json:"cost_info"`
|
||||
IIAAgreement *IIAAgreement `json:"iia_agreement,omitempty"`
|
||||
Securities []ClaimSec `json:"securities"`
|
||||
SignedDocument string `json:"signed_document"`
|
||||
SignatureFormat string `json:"signature_format"`
|
||||
}
|
||||
|
||||
// Investor — анкета.
|
||||
type Investor struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
LastName string `json:"last_name"`
|
||||
FirstName string `json:"first_name"`
|
||||
MiddleName string `json:"middle_name,omitempty"`
|
||||
Document Document `json:"document"`
|
||||
}
|
||||
|
||||
// Document — удостоверение личности.
|
||||
type Document struct {
|
||||
DocumentType string `json:"document_type"`
|
||||
Series string `json:"series,omitempty"`
|
||||
Number string `json:"number"`
|
||||
}
|
||||
|
||||
// CostInfo — choice yes|no.
|
||||
type CostInfo struct {
|
||||
Yes *CostInfoYes `json:"yes,omitempty"`
|
||||
No *struct{} `json:"no,omitempty"`
|
||||
}
|
||||
|
||||
// CostInfoYes — учёт ведётся, с кодом депонента-источника.
|
||||
type CostInfoYes struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
// IIAAgreement — реквизиты договора ИИС.
|
||||
type IIAAgreement struct {
|
||||
AgreementType string `json:"agreement_type"`
|
||||
AgreementNumber string `json:"agreement_number"`
|
||||
AgreementDate string `json:"agreement_date"`
|
||||
BrokerINN string `json:"broker_inn"`
|
||||
}
|
||||
|
||||
// ClaimSec — одна ЦБ в заявке.
|
||||
type ClaimSec struct {
|
||||
SecurityCode string `json:"security_code"`
|
||||
SecurityDetails SecurityDetails `json:"security_details"`
|
||||
Quantity Quantity `json:"quantity"`
|
||||
SettlementAccounts []SettlementAccount `json:"settlement_accounts"`
|
||||
}
|
||||
|
||||
// SecurityDetails — choice isin|security_info.
|
||||
type SecurityDetails struct {
|
||||
ISIN string `json:"isin,omitempty"`
|
||||
SecurityInfo *SecurityInfo `json:"security_info,omitempty"`
|
||||
}
|
||||
|
||||
// SecurityInfo — описание ЦБ без ISIN.
|
||||
type SecurityInfo struct {
|
||||
Classification string `json:"classification"`
|
||||
Category string `json:"category"`
|
||||
SecurityType string `json:"security_type,omitempty"`
|
||||
SecuritySeries string `json:"security_series,omitempty"`
|
||||
IdentificationDetails IdentificationDetails `json:"identification_details"`
|
||||
}
|
||||
|
||||
// IdentificationDetails — choice reg_number|fund_shares.
|
||||
type IdentificationDetails struct {
|
||||
RegNumber string `json:"reg_number,omitempty"`
|
||||
FundShares *FundShares `json:"fund_shares,omitempty"`
|
||||
}
|
||||
|
||||
// FundShares — ПИФ.
|
||||
type FundShares struct {
|
||||
RegNumber string `json:"reg_number"`
|
||||
Class string `json:"class,omitempty"`
|
||||
}
|
||||
|
||||
// Quantity — choice whole|fractional.
|
||||
type Quantity struct {
|
||||
Whole uint64 `json:"whole,omitempty"`
|
||||
Fractional string `json:"fractional,omitempty"`
|
||||
}
|
||||
|
||||
// SettlementAccount — реквизиты счёта.
|
||||
type SettlementAccount struct {
|
||||
SettlementRequisitesINN string `json:"settlement_requisites_inn"`
|
||||
SettlementLocation SettlementLocation `json:"settlement_location"`
|
||||
}
|
||||
|
||||
// SettlementLocation — место хранения.
|
||||
type SettlementLocation struct {
|
||||
DeponentCode string `json:"deponent_code"`
|
||||
AccountID string `json:"account_id"`
|
||||
SectionID string `json:"section_id"`
|
||||
}
|
||||
|
||||
// CreateClaimResponse — DTO ответа POST.
|
||||
type CreateClaimResponse struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// ClaimView — полная заявка с историей (GET и admin).
|
||||
type ClaimView struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Investor Investor `json:"investor"`
|
||||
TransferringDepositoryINN string `json:"transferring_depository_inn"`
|
||||
ReceivingDepositoryINN string `json:"receiving_depository_inn"`
|
||||
CostInfo CostInfo `json:"cost_info"`
|
||||
IIAAgreement *IIAAgreement `json:"iia_agreement,omitempty"`
|
||||
Securities []ClaimSec `json:"securities"`
|
||||
LastCallback *StatusCallback `json:"last_callback,omitempty"`
|
||||
Stages []StageView `json:"stages,omitempty"`
|
||||
M2MGUID m2m.UUID `json:"m2m_guid,omitempty"`
|
||||
M2MResponse *NSDResponseView `json:"m2m_response,omitempty"`
|
||||
M2MDecision *NSDDecisionView `json:"m2m_decision,omitempty"`
|
||||
}
|
||||
|
||||
// StageView — точка истории FSM для UI.
|
||||
type StageView struct {
|
||||
State string `json:"state"`
|
||||
EnteredAt time.Time `json:"entered_at"`
|
||||
LeftAt *time.Time `json:"left_at,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// StatusCallback — callback статуса от lk-gateway к ЛК.
|
||||
type StatusCallback struct {
|
||||
ClaimID string `json:"claim_id"`
|
||||
NewStatus string `json:"new_status"`
|
||||
ReasonCode string `json:"reason_code,omitempty"`
|
||||
ReasonText string `json:"reason_text,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
NSDResponse *NSDResponseView `json:"nsd_response,omitempty"`
|
||||
}
|
||||
|
||||
// NSDResponseView — сжатое представление M2MTransferResponse для UI/callback.
|
||||
type NSDResponseView struct {
|
||||
GUID string `json:"guid"`
|
||||
StatusCode string `json:"status_code"`
|
||||
Responses []NSDResponseEntry `json:"responses"`
|
||||
}
|
||||
|
||||
// NSDResponseEntry — одна запись Response.
|
||||
type NSDResponseEntry struct {
|
||||
ReferenceID string `json:"reference_id,omitempty"`
|
||||
Code string `json:"code"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
// NSDDecisionView — сжатое представление M2MTransferDecision для UI.
|
||||
type NSDDecisionView struct {
|
||||
GUID string `json:"guid"`
|
||||
Securities []NSDDecisionSecurity `json:"securities"`
|
||||
}
|
||||
|
||||
// NSDDecisionSecurity — решение по одной ЦБ.
|
||||
type NSDDecisionSecurity struct {
|
||||
ReferenceID string `json:"reference_id"`
|
||||
Outcome string `json:"outcome"` // "confirmed" | "rejected"
|
||||
RejectCodes []string `json:"reject_codes,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorResponse — формат ошибки, идентичный ESIA Finance.
|
||||
type ErrorResponse struct {
|
||||
Error bool `json:"error"`
|
||||
Status int `json:"status"`
|
||||
Code string `json:"code"`
|
||||
Title string `json:"title"`
|
||||
Meta *ErrorMeta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorMeta — детали ошибки.
|
||||
type ErrorMeta struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
Errors []FieldErrorDetail `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// FieldErrorDetail — ошибка по конкретному полю.
|
||||
type FieldErrorDetail struct {
|
||||
Field string `json:"field"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ClaimsPage — постраничная выдача.
|
||||
type ClaimsPage struct {
|
||||
Items []ClaimView `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
}
|
||||
Reference in New Issue
Block a user