107 lines
4.7 KiB
JavaScript
107 lines
4.7 KiB
JavaScript
/**
|
||
* @file smsProblemSummary.js
|
||
* @description Складывает неудачные отправки SMS в одну понятную причину.
|
||
*
|
||
* В таблице видно только «SMS не отправлено», а причина в логе — код сервиса.
|
||
* Здесь считаем: сколько сообщений, сколько заказов, за какой период, что
|
||
* случилось чаще всего — и главное, актуальна ли проблема до сих пор (по балансу
|
||
* счёта SMS-сервиса). Пополнили счёт — предупреждение гасим, а не держим месяцами.
|
||
*/
|
||
import { getSmsFailureReason } from "./orderGroupViews";
|
||
|
||
/** Через сколько дней после последней ошибки показываем, что всё наладилось.
|
||
* Короткий срок намеренно: «SMS снова уходят» — сообщение на день-два, а не
|
||
* постоянный баннер на главной (владелец закрывал его, а он возвращался). */
|
||
export const RESOLVED_VISIBLE_DAYS = 2;
|
||
|
||
/**
|
||
* @param {Array} rows строки sms_campaign_log со статусами ошибок
|
||
* @param {Array<string>} visibleIds id заказов, которые сейчас видны в таблице
|
||
* @param {{balance?: number|null, lowBalance?: number}} options баланс счёта SMS-сервиса
|
||
* @returns {null|object} null — говорить не о чем
|
||
*/
|
||
export const summarizeSmsFailures = (rows = [], visibleIds = [], options = {}) => {
|
||
const { balance = null, lowBalance = 100 } = options;
|
||
const balanceKnown = Number.isFinite(balance);
|
||
const list = Array.isArray(rows) ? rows.filter(Boolean) : [];
|
||
|
||
const reasons = new Map();
|
||
const lastByGroup = new Map();
|
||
let firstAt = null;
|
||
let lastAt = null;
|
||
|
||
for (const row of list) {
|
||
const reason = getSmsFailureReason(row) || "причина не указана";
|
||
reasons.set(reason, (reasons.get(reason) || 0) + 1);
|
||
|
||
if (row.order_group_id && !lastByGroup.has(row.order_group_id)) {
|
||
lastByGroup.set(row.order_group_id, row);
|
||
}
|
||
if (row.created_at) {
|
||
if (!firstAt || row.created_at < firstAt) firstAt = row.created_at;
|
||
if (!lastAt || row.created_at > lastAt) lastAt = row.created_at;
|
||
}
|
||
}
|
||
|
||
const [reason, topMessages] = list.length
|
||
? [...reasons.entries()].sort((a, b) => b[1] - a[1])[0]
|
||
: [null, 0];
|
||
|
||
const hasFundsReason = !!reason && /закончились средства/.test(reason);
|
||
const visible = new Set(visibleIds);
|
||
|
||
// Базовая сводка по логу (нужна в любом состоянии).
|
||
const base = {
|
||
reason,
|
||
topMessages,
|
||
messages: list.length,
|
||
groups: lastByGroup.size,
|
||
affectedInTable: [...lastByGroup.keys()].filter((id) => visible.has(id)).length,
|
||
hasVisibleRows: visible.size > 0,
|
||
hasFundsReason,
|
||
firstAt,
|
||
lastAt,
|
||
balance,
|
||
balanceKnown,
|
||
lowBalanceThreshold: lowBalance,
|
||
};
|
||
|
||
const within = (days) => {
|
||
if (!lastAt) return false;
|
||
return Date.now() - new Date(lastAt).getTime() <= days * 86400000;
|
||
};
|
||
|
||
let kind = null;
|
||
if (!list.length) {
|
||
// Ошибок нет, но денег на счёте мало — предупреждаем заранее.
|
||
kind = balanceKnown && balance < lowBalance ? "low_balance" : null;
|
||
} else if (hasFundsReason && balanceKnown && balance >= lowBalance) {
|
||
// Деньги пополнены — проблема снята, показываем это недолго и без нагнетания.
|
||
kind = within(RESOLVED_VISIBLE_DAYS) ? "resolved" : null;
|
||
} else if (hasFundsReason) {
|
||
kind = "funds";
|
||
} else {
|
||
kind = "other";
|
||
}
|
||
|
||
if (!kind) return null;
|
||
|
||
return {
|
||
...base,
|
||
kind,
|
||
// Отпечаток — только то, что делает сообщение НОВЫМ: вид проблемы, причина и
|
||
// время последней ошибки. Баланс сюда НЕ входит: он меняется от каждой отправки,
|
||
// из-за него закрытый баннер возвращался на следующий день.
|
||
signature: [kind, reason || "", lastAt || ""].join("|"),
|
||
};
|
||
};
|
||
|
||
/** «сообщение / сообщения / сообщений», «заказ / заказа / заказов». */
|
||
export const plural = (n, one, few, many) => {
|
||
const mod10 = n % 10;
|
||
const mod100 = n % 100;
|
||
if (mod10 === 1 && mod100 !== 11) return one;
|
||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return few;
|
||
return many;
|
||
};
|