From 4192e44998223d7cbc8ddb8063af0c8c9b27a273 Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 16 Sep 2026 16:19:17 +0200 Subject: [PATCH] =?UTF-8?q?feat(sms):=20=D0=BF=D0=BE=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=BF=D1=80=D0=B8=D1=87=D0=B8=D0=BD=D1=8B?= =?UTF-8?q?=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA=20=D0=B2=20=D0=BA=D0=B0?= =?UTF-8?q?=D1=80=D1=82=D0=BE=D1=87=D0=BA=D0=B5=20=D0=B4=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D0=BA=D0=B8=20=E2=80=94=20=D1=82=D0=B5=D1=85=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=BA=D0=B8=20=D0=BF=D0=BB=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D1=89=D0=B8=D0=BA=D0=B0=20(code=3D106,=20not?= =?UTF-8?q?=20delivered,=20limit=20exceeded)=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D0=BE=D0=B4=D1=8F=D1=82=D1=81=D1=8F=20=D0=B2=20=D1=87?= =?UTF-8?q?=D0=B5=D0=BB=D0=BE=D0=B2=D0=B5=D1=87=D0=B5=D1=81=D0=BA=D0=B8?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/orderGroupViews.js | 18 +++++++++++++++ .../orderGroupViews.smsReasons.test.js | 23 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/services/orderGroupViews.js b/src/services/orderGroupViews.js index c8f3ba4..1347765 100644 --- a/src/services/orderGroupViews.js +++ b/src/services/orderGroupViews.js @@ -323,6 +323,24 @@ export const getSmsFailureReason = (log) => { const text = log.trim(); const onlyCode = text.match(/^(\d{3})$/)?.[1]; if (onlyCode && SMS_ERROR_REASONS[onlyCode]) return SMS_ERROR_REASONS[onlyCode]; + // Технические строки из планировщика вида «Not delivered after 2 attempts + // (code=106)» переводим в человеческие: код объясняем словами. + const embedded = text.match(/code\s*=\s*(\d{3})/i)?.[1]; + if (embedded && SMS_ERROR_REASONS[embedded]) { + return `${SMS_ERROR_REASONS[embedded]} (код ${embedded})`; + } + // «Не доставлено после N попыток» без кода — говорим так, как есть. + const attempts = text.match(/after\s+(\d+)\s+attempts?/i); + if (/not delivered|delivery failed|failed after/i.test(text)) { + return attempts + ? `клиент не получил сообщение за ${attempts[1]} попытки — телефон недоступен или оператор не доставил` + : "клиент не получил сообщение — телефон недоступен или оператор не доставил"; + } + if (/limit exceeded/i.test(text)) { + return embedded && SMS_ERROR_REASONS[embedded] + ? `${SMS_ERROR_REASONS[embedded]} (код ${embedded})` + : "сработал лимит отправки — SMS-сервис временно блокирует рассылку"; + } return text.slice(0, 120) || null; } const code = smsErrorCode(log); diff --git a/src/services/orderGroupViews.smsReasons.test.js b/src/services/orderGroupViews.smsReasons.test.js index ccdb640..4bc7920 100644 --- a/src/services/orderGroupViews.smsReasons.test.js +++ b/src/services/orderGroupViews.smsReasons.test.js @@ -55,6 +55,29 @@ describe("isSmsFundsError", () => { }); }); +describe("технические строки из планировщика переводятся в человеческие", () => { + it("«Second SMS failed after 2 attempts (code=106)» — код объяснён словами", () => { + const reason = getSmsFailureReason("Second SMS failed after 2 attempts (code=106)"); + expect(reason).toContain("сбой на телефоне абонента"); + expect(reason).toContain("код 106"); + }); + + it("«Not delivered after 2 attempts» без кода — говорим прямо", () => { + const reason = getSmsFailureReason("Not delivered after 2 attempts"); + expect(reason).toContain("не получил сообщение"); + expect(reason).toContain("2 попытки"); + }); + + it("«Second SMS limit exceeded (code=232)» — лимит словами", () => { + const reason = getSmsFailureReason("Second SMS limit exceeded (code=232)"); + expect(reason).toContain("лимит одинаковых сообщений в сутки"); + }); + + it("одна цифра 201 с переводом строки по-прежнему расшифровывается", () => { + expect(getSmsFailureReason("201\n")).toBe("на счёте SMS-сервиса закончились средства"); + }); +}); + describe("подсветка SMS в таблице остаётся прежней", () => { it("без отправленных SMS — «не отправлено»", () => { expect(getSmsProblemTone({})).toBe("not_sent");