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 { if in, _, _, _ := nsdHandbookHasUs(globalRC); in { return true // уже в справочнике участников НРД — зарегистрированы (в т.ч. без анкеты из bj) } 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(`
` + esc(head) + `` + esc(body) + ` Открыть анкету →
`) } // nsdHandbookHasUs — наша организация в справочнике участников M2M, который этот bj получил от НРД // (handbookFile, текущий контур). known=false — справочника от НРД ещё нет. Общий справочник с // сервера лицензий здесь не годится: он собран с других установок и контуров. func nsdHandbookHasUs(rc *RuntimeConfig) (in, known bool, at time.Time, n int) { b, err := os.ReadFile(handbookFile) if err != nil || rc == nil { return false, false, time.Time{}, 0 } var h struct { At time.Time `json:"at"` Participants []HandbookEntry `json:"participants"` } if json.Unmarshal(b, &h) != nil { return false, false, time.Time{}, 0 } s := rc.Snapshot() code, inn := strings.ToUpper(strings.TrimSpace(orgCode(s))), strings.TrimSpace(orgINN(s)) for _, p := range h.Participants { if (code != "" && strings.EqualFold(p.Code, code)) || (code == "" && inn != "" && p.INN == inn) { return true, true, h.At, len(h.Participants) } } return false, true, h.At, len(h.Participants) } // participantHandbookCheck — после каждого справочника от НРД: появились в нём, а анкета ещё не // отмечена принятой — регистрация подтверждена справочником. func participantHandbookCheck(rc *RuntimeConfig) { in, known, at, _ := nsdHandbookHasUs(rc) if !in || !known { return } r := loadParticipantReg() if r.SentAt.IsZero() || (!r.RespAt.IsZero() && !strings.EqualFold(r.Status, "INFO") && !strings.EqualFold(r.Status, "ERROR")) { return } saveParticipantReg(func(r *participantReg) { r.RespAt, r.Status, r.Codes = at, "OK", nil r.RespText = "организация появилась в справочнике участников M2M НРД" }) }