6fc0ff5d3d
Квитанции ЭДО НРД (пакеты C, «Уведомление о получении Пакетов…») разбираются: для нашей анкеты (#M2MPF…) — отметка «доставлена в НРД» с временем, ошибка первичной обработки — отказ с текстом квитанции. Ответ сервиса: INFO — «принята в обработку», OK — принята, ERROR — отказ с кодом; документ-ответ M2MTransferParticipantForm — регистрация подтверждена. Всё видно в строке «Исходящих», на странице анкеты и в плашке раздела НРД. После запуска bj перечитывает квитанции за день отправки, если отметки ещё нет (22.10 на 9 квитанция пришла раньше). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
188 lines
8.0 KiB
Go
188 lines
8.0 KiB
Go
package lkgateway
|
||
|
||
// participant_reg.go — учёт регистрации участника M2M: что и когда отправили
|
||
// анкетой (#M2MPF) и какой ответ дал НРД (сервис МОСТ, M2MTransferResponse с
|
||
// нулевым GUID — он не относится к переводу). Показывается на странице анкеты.
|
||
|
||
import (
|
||
"encoding/json"
|
||
"html/template"
|
||
"os"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
|
||
)
|
||
|
||
const participantRegFile = "/var/lib/bj/.bj/participant-reg.json"
|
||
|
||
// participantReg — последняя отправка анкеты и ответ НРД на неё.
|
||
type participantReg struct {
|
||
SentAt time.Time `json:"sent_at,omitempty"`
|
||
PkgID string `json:"pkg_id,omitempty"`
|
||
Name string `json:"name,omitempty"` // отправленное полное наименование
|
||
Channel string `json:"channel,omitempty"`
|
||
RespAt time.Time `json:"resp_at,omitempty"`
|
||
Status string `json:"status,omitempty"` // OK | ERROR
|
||
Codes []string `json:"codes,omitempty"`
|
||
RespText string `json:"resp_text,omitempty"`
|
||
RawFile string `json:"raw_file,omitempty"`
|
||
// Квитанция ЭДО НРД о получении пакета (пакет C): НРД получил анкету, первичная обработка прошла.
|
||
DeliveredAt time.Time `json:"delivered_at,omitempty"`
|
||
Package string `json:"package,omitempty"` // имя пакета из квитанции (#M2MPF….ZIP)
|
||
}
|
||
|
||
var participantRegMu sync.Mutex
|
||
|
||
// participantRegsFile — все отправки анкеты (строки в «Исходящих» и «Журнале»); participantRegFile —
|
||
// прежний файл с одной последней отправкой, из него история начинается при первом чтении.
|
||
const participantRegsFile = "/var/lib/bj/.bj/participant-regs.json"
|
||
|
||
func readParticipantRegs() []participantReg {
|
||
var list []participantReg
|
||
if b, err := os.ReadFile(participantRegsFile); err == nil {
|
||
_ = json.Unmarshal(b, &list)
|
||
return list
|
||
}
|
||
var r participantReg
|
||
if b, err := os.ReadFile(participantRegFile); err == nil && json.Unmarshal(b, &r) == nil && !r.SentAt.IsZero() {
|
||
list = append(list, r)
|
||
}
|
||
return list
|
||
}
|
||
|
||
func writeParticipantRegs(list []participantReg) {
|
||
b, err := json.MarshalIndent(list, "", " ")
|
||
if err != nil {
|
||
return
|
||
}
|
||
tmp := participantRegsFile + ".tmp"
|
||
if os.WriteFile(tmp, b, 0o640) == nil {
|
||
_ = os.Rename(tmp, participantRegsFile)
|
||
}
|
||
}
|
||
|
||
// loadParticipantRegs — все отправки анкеты, старые первыми.
|
||
func loadParticipantRegs() []participantReg {
|
||
participantRegMu.Lock()
|
||
defer participantRegMu.Unlock()
|
||
return readParticipantRegs()
|
||
}
|
||
|
||
// loadParticipantReg — последняя отправка анкеты.
|
||
func loadParticipantReg() participantReg {
|
||
list := loadParticipantRegs()
|
||
if len(list) == 0 {
|
||
return participantReg{}
|
||
}
|
||
return list[len(list)-1]
|
||
}
|
||
|
||
// addParticipantReg — новая отправка анкеты.
|
||
func addParticipantReg(r participantReg) {
|
||
participantRegMu.Lock()
|
||
defer participantRegMu.Unlock()
|
||
writeParticipantRegs(append(readParticipantRegs(), r))
|
||
}
|
||
|
||
// saveParticipantReg — изменить последнюю отправку (ответ НРД приходит на неё).
|
||
func saveParticipantReg(mut func(*participantReg)) {
|
||
participantRegMu.Lock()
|
||
defer participantRegMu.Unlock()
|
||
list := readParticipantRegs()
|
||
if len(list) == 0 {
|
||
list = append(list, participantReg{})
|
||
}
|
||
mut(&list[len(list)-1])
|
||
writeParticipantRegs(list)
|
||
}
|
||
|
||
// StatusClass, StatusText — статус отправки анкеты для строк списков (класс бейджа и текст).
|
||
func (r participantReg) StatusClass() string {
|
||
switch {
|
||
case r.RespAt.IsZero(), strings.EqualFold(r.Status, "INFO"):
|
||
return "awaiting_decision"
|
||
case strings.EqualFold(r.Status, "ERROR"):
|
||
return "rejected"
|
||
}
|
||
return "confirmed"
|
||
}
|
||
|
||
func (r participantReg) StatusText() string {
|
||
switch {
|
||
case r.RespAt.IsZero() && !r.DeliveredAt.IsZero():
|
||
return "Доставлена в НРД · ждём ответ"
|
||
case r.RespAt.IsZero():
|
||
return "Отправлена · ждём квитанцию НРД"
|
||
case strings.EqualFold(r.Status, "INFO"):
|
||
return "Принята НРД в обработку"
|
||
case strings.EqualFold(r.Status, "ERROR"):
|
||
if len(r.Codes) > 0 {
|
||
return "Отклонена НРД · " + strings.Join(r.Codes, ", ")
|
||
}
|
||
return "Отклонена НРД"
|
||
}
|
||
return "Принята НРД"
|
||
}
|
||
|
||
// recordParticipantReply сохраняет ответ НРД на анкету (статус, коды, тексты, файл XML).
|
||
func recordParticipantReply(status string, resp *m2m.M2MTransferResponse, rawFile string) {
|
||
var codes []string
|
||
seen := map[string]bool{}
|
||
for _, rr := range resp.Responses {
|
||
c := strings.TrimSpace(rr.Code)
|
||
if c != "" && !seen[c] {
|
||
seen[c] = true
|
||
codes = append(codes, c)
|
||
}
|
||
}
|
||
text := strings.Join(serviceErrorTexts(resp), "; ")
|
||
saveParticipantReg(func(r *participantReg) {
|
||
r.RespAt = time.Now()
|
||
r.Status = status
|
||
r.Codes = codes
|
||
r.RespText = text
|
||
r.RawFile = rawFile
|
||
})
|
||
}
|
||
|
||
// participantAccepted — последняя отправленная анкета принята НРД.
|
||
func participantAccepted() bool {
|
||
r := loadParticipantReg()
|
||
return !r.RespAt.IsZero() && !strings.EqualFold(r.Status, "ERROR") && !strings.EqualFold(r.Status, "INFO")
|
||
}
|
||
|
||
// recordParticipantFormReply — НРД прислал документ-ответ на анкету (M2MTransferParticipantForm):
|
||
// регистрация подтверждена.
|
||
func recordParticipantFormReply(rawFile string) {
|
||
saveParticipantReg(func(r *participantReg) {
|
||
r.RespAt, r.Status, r.Codes = time.Now(), "OK", nil
|
||
r.RespText, r.RawFile = "НРД подтвердил регистрацию участника", rawFile
|
||
})
|
||
}
|
||
|
||
// participantBanner — плашка наверху раздела НРД: анкета не отправлена, ждёт ответа или отклонена.
|
||
// Пропадает, когда НРД принял анкету. Анкету шлём только в боевом контуре — в тестовом плашки нет.
|
||
func participantBanner() template.HTML {
|
||
if participantAccepted() || contourOf(currentNSDProfile()) != "prod" {
|
||
return ""
|
||
}
|
||
esc := template.HTMLEscapeString
|
||
r := loadParticipantReg()
|
||
cls, head, body := "stc-err", "Анкета участника M2M не отправлена",
|
||
"Без регистрации участника НРД не принимает запросы на переводы. Проверьте анкету и отправьте её."
|
||
switch {
|
||
case r.SentAt.IsZero():
|
||
case r.RespAt.IsZero() && !r.DeliveredAt.IsZero():
|
||
cls, head, body = "stc-run", "Анкета участника M2M доставлена в НРД — ждём ответ", "Квитанция НРД "+r.DeliveredAt.In(listMSK).Format("02.01.2006 15:04:05")+": пакет получен, ошибок нет. Плашка пропадёт, когда НРД примет анкету."
|
||
case r.RespAt.IsZero(), strings.EqualFold(r.Status, "INFO"):
|
||
cls, head, body = "stc-run", "Анкета участника M2M ждёт ответа НРД", "Отправлена "+r.SentAt.In(listMSK).Format("02.01.2006 15:04")+". Плашка пропадёт, когда НРД примет анкету."
|
||
case strings.EqualFold(r.Status, "ERROR"):
|
||
cls, head = "stc-err", "НРД отклонил анкету участника M2M"
|
||
body = r.RespAt.In(listMSK).Format("02.01.2006 15:04") + ": " + r.RespText + ". Исправьте и отправьте снова."
|
||
}
|
||
return template.HTML(`<div class="stc ` + cls + `" style="margin:0 0 14px"><span class="stc-ic"></span><div class="stc-b"><b>` + esc(head) +
|
||
`</b><small>` + esc(body) + ` <a href="#nsd-participant" onclick="var d=document.getElementById('pf-box');if(d)d.open=true">Открыть анкету →</a></small></div></div>`)
|
||
}
|