ЛК: итог «Исполнена» по MS010, GUID без учёта регистра, заявка при отказе МНП берётся один раз; тестовый контур не трогает боевой ЛК
Перевод 97EAD00C (заявка ЛК #71741, 23.10.2026) исполнен в QUIK и НРД (MS010), но в ЛК статус не изменился, а сделка осталась «Подтверждена»: - связки guid → заявка ЛК искались с учётом регистра: мы шлём GUID строчными, НРД отвечает заглавными — OnLKOutcome молча не находил заявку. Ключи — строчными (lkGUIDKey), старые записи приводятся при загрузке; итог модуля ЛК (lk.transfer_requests) — lower(guid); - после MS010 по всем строкам поручения 16 сделка → «Исполнена» (CompleteSUB16), итог — в ЛК (outOnExecuted, каждый проход order-watch, повтор безвреден); в бою согласие контрагента — ещё не «Исполнена»: статус ждёт MS010, как в тесте «боевой ЛК → TEST3»; имитатор НРД — итог сразу. Тестовый 28 с боевым ЛК взял ту же заявку и после отказа МНП («запрещен для операции шорт» — бумагу уже заблокировал 9) брал её каждые 10 секунд: 108 сделок с новыми GUID и столько же писем. - Сделка заведена, а отправка не прошла (МНП не заблокировал) — заявка помечается взятой и связывается с переводом: дальше — оператор в карточке. - Боевой ЛК при тестовом профиле ИШ (lkProdOnTestNSD): заявки клиентов не берутся (кроме теста «боевой ЛК → тестовый НРД»), портфели не заполняются, предупреждение в настройках ЛК. - Письма: в теме и первой строке — сервер bj и контур НРД («[bj ТЕСТ …]» у тестового); письма о входящих — метка только у тестового. - История FSM — по МСК с датой, «текущее состояние» вместо «сейчас». - Карточка теста «боевой ЛК → тестовый НРД» — только в режиме разработки (включённый тест виден всегда, чтобы его выключить). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
@@ -39,7 +39,7 @@ func lkClaimInvestorKey(cl map[string]any) string {
|
||||
|
||||
// lkClaimLinks — guid отправленного в НРД запроса → id заявки в ЛК, чтобы по
|
||||
// ответу НРД PATCH-нуть статус на их стороне. Переживает рестарт.
|
||||
const lkLinksFile = "/var/lib/bj/.bj/lk-claim-links.json"
|
||||
var lkLinksFile = "/var/lib/bj/.bj/lk-claim-links.json" // var — тесты подменяют путь
|
||||
|
||||
var (
|
||||
lkLinksMu sync.Mutex
|
||||
@@ -47,10 +47,24 @@ var (
|
||||
lkLoaded bool
|
||||
)
|
||||
|
||||
// lkGUIDKey — ключ связок по GUID без учёта регистра: мы шлём GUID строчными, НРД отвечает
|
||||
// заглавными (23.10.2026: по ответу 97EAD00C-… связка 97ead00c-… не нашлась — статус в ЛК не ушёл).
|
||||
func lkGUIDKey(guid string) string { return strings.ToLower(strings.TrimSpace(guid)) }
|
||||
|
||||
// lkNormKeys — ключи связок из файла к одному регистру (записи до 23.10.2026).
|
||||
func lkNormKeys(m map[string]int) {
|
||||
for k, v := range m {
|
||||
if n := lkGUIDKey(k); n != k {
|
||||
delete(m, k)
|
||||
m[n] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lkLinkGUID(guid string, _ LKEndpoint, claimID int) {
|
||||
lkLinksMu.Lock()
|
||||
lkEnsureLinksLoaded()
|
||||
lkLinks[guid] = claimID
|
||||
lkLinks[lkGUIDKey(guid)] = claimID
|
||||
if raw, err := json.Marshal(lkLinks); err == nil {
|
||||
_ = os.WriteFile(lkLinksFile, raw, 0o600)
|
||||
}
|
||||
@@ -67,11 +81,12 @@ func lkEnsureLinksLoaded() {
|
||||
if raw, err := os.ReadFile(lkLinksFile); err == nil {
|
||||
_ = json.Unmarshal(raw, &lkLinks)
|
||||
}
|
||||
lkNormKeys(lkLinks)
|
||||
}
|
||||
|
||||
// lkOrigin — guid → id заявки ЛК, ПОСТОЯННАЯ запись происхождения (для UI
|
||||
// «путь заявки»). lkLinks чистится после финального PATCH, а origin остаётся.
|
||||
const lkOriginFile = "/var/lib/bj/.bj/lk-origin.json"
|
||||
var lkOriginFile = "/var/lib/bj/.bj/lk-origin.json"
|
||||
|
||||
var (
|
||||
lkOriginMu sync.Mutex
|
||||
@@ -84,6 +99,7 @@ func lkEnsureOriginLoaded() {
|
||||
return
|
||||
}
|
||||
lkOriginLoaded = true
|
||||
defer lkNormKeys(lkOrigin)
|
||||
if raw, err := os.ReadFile(lkOriginFile); err == nil {
|
||||
_ = json.Unmarshal(raw, &lkOrigin)
|
||||
return
|
||||
@@ -98,7 +114,7 @@ func lkRecordOrigin(guid string, claimID int) {
|
||||
lkOriginMu.Lock()
|
||||
defer lkOriginMu.Unlock()
|
||||
lkEnsureOriginLoaded()
|
||||
lkOrigin[guid] = claimID
|
||||
lkOrigin[lkGUIDKey(guid)] = claimID
|
||||
if raw, err := json.Marshal(lkOrigin); err == nil {
|
||||
_ = os.WriteFile(lkOriginFile, raw, 0o600)
|
||||
}
|
||||
@@ -110,10 +126,19 @@ func LKClaimIDByGUID(guid string) (int, bool) {
|
||||
lkOriginMu.Lock()
|
||||
defer lkOriginMu.Unlock()
|
||||
lkEnsureOriginLoaded()
|
||||
id, ok := lkOrigin[guid]
|
||||
id, ok := lkOrigin[lkGUIDKey(guid)]
|
||||
return id, ok
|
||||
}
|
||||
|
||||
// lkLinked — заявка ЛК ещё ждёт итогового статуса (связка не закрыта OnLKOutcome).
|
||||
func lkLinked(guid string) bool {
|
||||
lkLinksMu.Lock()
|
||||
defer lkLinksMu.Unlock()
|
||||
lkEnsureLinksLoaded()
|
||||
_, ok := lkLinks[lkGUIDKey(guid)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// lkSent — id заявок ЛК, уже отправленных в НРД (at-most-once защита от дублей
|
||||
// переводов при повторных проходах поллера). Переживает рестарт.
|
||||
const lkSentFile = "/var/lib/bj/.bj/lk-sent.json"
|
||||
@@ -163,9 +188,9 @@ func lkMarkSent(id int) {
|
||||
func OnLKOutcome(ctx context.Context, rc *RuntimeConfig, guid, status, reason string) {
|
||||
lkLinksMu.Lock()
|
||||
lkEnsureLinksLoaded()
|
||||
id, ok := lkLinks[guid]
|
||||
id, ok := lkLinks[lkGUIDKey(guid)]
|
||||
if ok {
|
||||
delete(lkLinks, guid)
|
||||
delete(lkLinks, lkGUIDKey(guid))
|
||||
if raw, err := json.Marshal(lkLinks); err == nil {
|
||||
_ = os.WriteFile(lkLinksFile, raw, 0o600)
|
||||
}
|
||||
@@ -345,7 +370,7 @@ func PullLKClaims(ctx context.Context, rc *RuntimeConfig, svc *Service) (LKPullR
|
||||
continue // чужой вид/статус — не наша заявка
|
||||
}
|
||||
id := toInt(c["id"])
|
||||
if id == 0 || lkAlreadySent(id) {
|
||||
if id == 0 || lkAlreadySent(id) || lkTestSkipped(id) {
|
||||
continue
|
||||
}
|
||||
res.Fetched++
|
||||
@@ -354,6 +379,10 @@ func PullLKClaims(ctx context.Context, rc *RuntimeConfig, svc *Service) (LKPullR
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("#%d: %v", id, err))
|
||||
continue
|
||||
}
|
||||
if outcome == "skipped" {
|
||||
res.Fetched--
|
||||
continue
|
||||
}
|
||||
if outcome == "rejected" {
|
||||
res.Rejected++
|
||||
} else {
|
||||
@@ -385,6 +414,13 @@ func pullOneLKClaim(ctx context.Context, rc *RuntimeConfig, svc *Service, ep LKE
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if lkProdOnTestNSD(rc) && !realTestMatch(rc, claimReq.Investor.Document.Series, claimReq.Investor.Document.Number) {
|
||||
// Заявка клиента — для боевого сервера: здесь её не трогаем (ни QUIK, ни DEPO, ни статус в ЛК).
|
||||
lkTestSkip(id)
|
||||
log.Printf("lk-gateway: ЛК #%d: заявка клиента не взята — боевой ЛК на сервере с тестовым контуром НРД (%s); берутся только заявки теста «боевой ЛК → тестовый НРД»",
|
||||
id, rc.Snapshot().NSD.Profile)
|
||||
return "skipped", nil
|
||||
}
|
||||
// Регистрируем клиента в справочнике (как для ЛК-подачи).
|
||||
if ss, ok := svc.store.(*SeedStore); ok {
|
||||
nsd := rc.Snapshot().NSD
|
||||
@@ -427,8 +463,16 @@ func pullOneLKClaim(ctx context.Context, rc *RuntimeConfig, svc *Service, ep LKE
|
||||
return "", fmt.Errorf("заявка #%d уже отправлялась в НРД — повторная отправка подавлена", id)
|
||||
}
|
||||
if _, err := svc.SendBuiltRequest(ctx, req, claimReq.Investor.ID, nil); err != nil {
|
||||
// Отправка не удалась (напр. до реального Send в НРД) — НЕ помечаем
|
||||
// отправленной, чтобы можно было повторить после устранения причины.
|
||||
// Сделка уже заведена (напр. QUIK МНП не заблокировал бумаги) — заявку берём один раз:
|
||||
// дальше перевод повторяет или отклоняет оператор в карточке, итог уйдёт в ЛК по связке.
|
||||
// Иначе каждый проход заводил новую сделку с новым GUID и новым письмом (23.10.2026:
|
||||
// 108 повторов заявки #71741 за 20 минут).
|
||||
if d, gerr := svc.Repo().GetByGUID(ctx, req.Header.GUID); gerr == nil && d != nil {
|
||||
lkMarkSent(id)
|
||||
lkLinkGUID(string(req.Header.GUID), ep, id)
|
||||
return "", fmt.Errorf("отправка в НРД: %w — перевод %s ждёт оператора в карточке, повторно заявка не берётся", err, shortGUID(string(req.Header.GUID)))
|
||||
}
|
||||
// Сделки нет (сбой до её заведения) — НЕ помечаем, повторим после устранения причины.
|
||||
return "", fmt.Errorf("отправка в НРД: %w", err)
|
||||
}
|
||||
// Успешно ушло в НРД — фиксируем, чтобы повторный проход не создал дубль.
|
||||
@@ -443,6 +487,36 @@ func pullOneLKClaim(ctx context.Context, rc *RuntimeConfig, svc *Service, ep LKE
|
||||
return "sent", nil
|
||||
}
|
||||
|
||||
// lkProdOnTestNSD — боевой ЛК на сервере с тестовым контуром НРД (23.10.2026: тестовый 28 с боевым ЛК
|
||||
// брал заявки клиентов одновременно с боевым 9 и пытался заблокировать те же бумаги в МНП). Такой
|
||||
// сервер берёт только заявки теста «боевой ЛК → тестовый НРД» и не заполняет портфели в ЛК.
|
||||
func lkProdOnTestNSD(rc *RuntimeConfig) bool {
|
||||
if rc == nil {
|
||||
return false
|
||||
}
|
||||
s := rc.Snapshot()
|
||||
return s.LK.ActiveEnv == "prod" && !nsdProfileProd(s.NSD.Profile)
|
||||
}
|
||||
|
||||
// Заявки ЛК, не взятые сервером с тестовым контуром (lkProdOnTestNSD), — чтобы не запрашивать их
|
||||
// каждые 10 секунд. В памяти: после перезапуска (и смены настроек) проверяются заново.
|
||||
var lkTestSkips = struct {
|
||||
sync.Mutex
|
||||
m map[int]bool
|
||||
}{m: map[int]bool{}}
|
||||
|
||||
func lkTestSkip(id int) {
|
||||
lkTestSkips.Lock()
|
||||
lkTestSkips.m[id] = true
|
||||
lkTestSkips.Unlock()
|
||||
}
|
||||
|
||||
func lkTestSkipped(id int) bool {
|
||||
lkTestSkips.Lock()
|
||||
defer lkTestSkips.Unlock()
|
||||
return lkTestSkips.m[id]
|
||||
}
|
||||
|
||||
// lkClaimToRequest маппит заявку ЕСИА Финанс → наш CreateClaimRequest.
|
||||
func lkClaimToRequest(ctx context.Context, rc *RuntimeConfig, ep LKEndpoint, cl, data map[string]any) (CreateClaimRequest, error) {
|
||||
var req CreateClaimRequest
|
||||
|
||||
Reference in New Issue
Block a user