package lkgateway import ( "encoding/json" "fmt" "log" "net" "os" "strings" ) // incoming_notify.go — уведомление депозитария о входящих ценных бумагах. // Когда по M2M нам приходит перевод (мы — принимающая сторона), кроме показа во // «Входящих переводах» bj шлёт письмо в бэк-офис депозитария: «к вам переведена // такая-то бумага, требуется учесть». Пока это ТОЛЬКО уведомление; позже здесь // же будем формировать пакет для загрузки в учётную систему. // notifyDepositoryIncoming отправляет письмо-уведомление о новом входящем // переводе на адреса депозитария из настроек. Best-effort (ошибки — в лог). func notifyDepositoryIncoming(rc *RuntimeConfig, it *IncomingTransfer) { if rc == nil || it == nil { return } cfg := rc.Snapshot() inc := cfg.Incoming if !inc.Enabled || strings.TrimSpace(notifyRecipients(cfg, "incoming")) == "" { return } smtp := cfg.Alerts.SMTP if !smtp.Enabled || strings.TrimSpace(smtp.Host) == "" { log.Printf("lk-gateway: уведомление депозитарию о входящем GUID=%s не отправлено — SMTP не настроен", it.GUID) return } subject := "MOEX МОСТ (M2M): к вам переведены ценные бумаги — требуется учёт" body := buildIncomingEmailBody(it) s := smtp s.To = notifyRecipients(cfg, "incoming") // получатели по маршруту «входящие переводы» if err := sendSMTP(s, subject, body); err != nil { log.Printf("lk-gateway: уведомление депозитарию о входящем GUID=%s не доставлено: %v", it.GUID, err) return } log.Printf("lk-gateway: уведомление депозитарию о входящем GUID=%s отправлено (%d бумаг) на %s", it.GUID, len(it.Securities), notifyRecipients(cfg, "incoming")) } // notifyDepositoryAccepted отправляет письмо депозитарию о том, что входящий // перевод обработан и ценные бумаги приняты (зачислены). Best-effort. func notifyDepositoryAccepted(rc *RuntimeConfig, it *IncomingTransfer, confirmed, rejected int) { if rc == nil || it == nil { return } cfg := rc.Snapshot() inc := cfg.Incoming if !inc.Enabled || strings.TrimSpace(notifyRecipients(cfg, "incoming")) == "" { return } smtp := cfg.Alerts.SMTP if !smtp.Enabled || strings.TrimSpace(smtp.Host) == "" { return } subject := "MOEX МОСТ (M2M): перевод обработан — ценные бумаги приняты (зачислены)" var b strings.Builder b.WriteString("Перевод по сервису MOEX МОСТ (M2M) обработан нашим депозитарием.\n\n") if it.Auto { b.WriteString("Проверка по учётной системе (Fancy) пройдена — перевод обработан автоматически.\n\n") } senderName := handbookNameByCode(it.SenderCode) from := it.SenderCode if it.SenderINN != "" { from += ", ИНН " + it.SenderINN } if senderName != "" { from = senderName + " (" + from + ")" } b.WriteString("Передающий депозитарий: " + from + "\n") if fio := strings.TrimSpace(it.InvestorLast + " " + it.InvestorFirst + " " + it.InvestorMiddle); fio != "" { b.WriteString("Инвестор: " + fio + "\n") } b.WriteString("GUID перевода: " + it.GUID + "\n") b.WriteString(fmt.Sprintf("Итог: принято %d, отклонено %d.\n\n", confirmed, rejected)) b.WriteString("Ценные бумаги:\n") for i, sec := range it.Securities { code := sec.ISIN if code == "" { code = sec.SecurityCode } st := "принято (зачислено)" if sec.Decision == "rejected" { st = "отклонено (" + sec.RejectCode + ")" } b.WriteString(fmt.Sprintf(" %d) %s — %s шт. — %s\n", i+1, code, sec.Quantity, st)) } b.WriteString("\nПосмотреть перевод:\n " + adminBaseURL() + "/admin/incoming/" + it.ID + "\n") b.WriteString("\nЭто уведомление сформировано автоматически системой Bridge-and-Join-s (bj).\n") m := smtp m.To = notifyRecipients(cfg, "incoming") if err := sendSMTP(m, subject, b.String()); err != nil { log.Printf("lk-gateway: письмо депозитарию о приёме GUID=%s не доставлено: %v", it.GUID, err) return } log.Printf("lk-gateway: письмо депозитарию о приёме GUID=%s отправлено на %s", it.GUID, notifyRecipients(cfg, "incoming")) } // buildIncomingEmailBody собирает текст письма депозитарию. func buildIncomingEmailBody(it *IncomingTransfer) string { var b strings.Builder b.WriteString("Уведомление о переводе ценных бумаг через сервис MOEX МОСТ (M2M).\n\n") b.WriteString("К вам (принимающий депозитарий) переведены ценные бумаги. Требуется учесть\n") b.WriteString("их в учёте депозитария.\n\n") senderName := handbookNameByCode(it.SenderCode) from := it.SenderCode if it.SenderINN != "" { from += ", ИНН " + it.SenderINN } if senderName != "" { from = senderName + " (" + from + ")" } b.WriteString("Передающий депозитарий: " + from + "\n") b.WriteString("Идентификатор перевода (GUID): " + it.GUID + "\n") b.WriteString("Получено: " + it.ReceivedAt.Format("02.01.2006 15:04:05") + " МСК\n\n") b.WriteString(fmt.Sprintf("Ценные бумаги (%d):\n", len(it.Securities))) for i, s := range it.Securities { code := s.ISIN if code == "" { code = s.SecurityCode } b.WriteString(fmt.Sprintf(" %d) %s — %s шт.\n", i+1, code, s.Quantity)) } link := adminBaseURL() + "/admin/incoming/" + it.ID b.WriteString("\nПосмотреть входящее сообщение и ответить (принять/отклонить):\n") b.WriteString(" " + link + "\n") b.WriteString("\nЭто уведомление сформировано автоматически системой Bridge-and-Join-s (bj).\n") b.WriteString("Пакет для загрузки в учётную систему будет сформирован позже.\n") return b.String() } // adminBaseURL — базовый URL веб-админки bj для ссылок в письмах. Приоритет: // переменная BJ_ADMIN_URL; иначе http://:<порт админки>. func adminBaseURL() string { if u := strings.TrimSpace(os.Getenv("BJ_ADMIN_URL")); u != "" { return strings.TrimRight(u, "/") } host := primaryIPv4() if host == "" { host = "localhost" } port := "8080" if a := strings.TrimSpace(os.Getenv("BJ_HTTP_ADDR")); a != "" { if i := strings.LastIndex(a, ":"); i >= 0 && i+1 < len(a) { port = a[i+1:] } } return "http://" + host + ":" + port } // primaryIPv4 возвращает первый непетлевой IPv4-адрес сервера (для ссылок). func primaryIPv4() string { addrs, err := net.InterfaceAddrs() if err != nil { return "" } for _, a := range addrs { if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { if ip4 := ipnet.IP.To4(); ip4 != nil { return ip4.String() } } } return "" } // handbookNameByCode ищет наименование участника по MC-коду в сохранённом // справочнике (m2m-handbook.json). Пусто, если справочника нет/не найдено. func handbookNameByCode(code string) string { if code == "" { return "" } raw, err := readHandbook() if err != nil { return "" } var hb struct { Participants []HandbookEntry `json:"participants"` } if json.Unmarshal(raw, &hb) != nil { return "" } for _, p := range hb.Participants { if p.Code == code { return p.Name } } return "" }