From dc262d2be7dbe6eb97ab7d067718f0f9b29f2ec3 Mon Sep 17 00:00:00 2001 From: Hermes Date: Mon, 14 Sep 2026 15:40:15 +0200 Subject: [PATCH] =?UTF-8?q?feat(orders):=20=D0=BF=D1=80=D0=B8=D1=87=D0=B8?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BD=D0=B5=D1=83=D0=B4=D0=B0=D1=87=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20SMS?= =?UTF-8?q?=20=E2=80=94=20=D0=B2=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BA=D0=B5=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=B0=20=D0=B8=20?= =?UTF-8?q?=D0=B1=D0=BB=D0=BE=D0=BA=D0=BE=D0=BC=20=D0=BD=D0=B0=D0=B4=20?= =?UTF-8?q?=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D0=B5=D0=B9=20=D0=BD=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=80=D0=BE=D0=B2=20(=D0=BA=D0=BE=D0=B4=20201=20?= =?UTF-8?q?=3D=20=D0=BA=D0=BE=D0=BD=D1=87=D0=B8=D0=BB=D0=B8=D1=81=D1=8C=20?= =?UTF-8?q?=D1=81=D1=80=D0=B5=D0=B4=D1=81=D1=82=D0=B2=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../logistics/LogisticsReadinessBoard.jsx | 4 + src/components/logistics/SmsProblemNotice.jsx | 49 ++++++++++++ .../logistics/SmsProblemNotice.test.jsx | 61 +++++++++++++++ .../logistics/SmsProblemNoticeView.jsx | 78 +++++++++++++++++++ src/components/orders/SmsStatusCard.jsx | 21 ++++- src/services/orderGroupViews.js | 59 ++++++++++++++ .../orderGroupViews.smsReasons.test.js | 67 ++++++++++++++++ src/services/smsProblemSummary.js | 61 +++++++++++++++ src/services/smsProblemSummary.test.js | 64 +++++++++++++++ 9 files changed, 461 insertions(+), 3 deletions(-) create mode 100644 src/components/logistics/SmsProblemNotice.jsx create mode 100644 src/components/logistics/SmsProblemNotice.test.jsx create mode 100644 src/components/logistics/SmsProblemNoticeView.jsx create mode 100644 src/services/orderGroupViews.smsReasons.test.js create mode 100644 src/services/smsProblemSummary.js create mode 100644 src/services/smsProblemSummary.test.js diff --git a/src/components/logistics/LogisticsReadinessBoard.jsx b/src/components/logistics/LogisticsReadinessBoard.jsx index 60553ca..90733db 100644 --- a/src/components/logistics/LogisticsReadinessBoard.jsx +++ b/src/components/logistics/LogisticsReadinessBoard.jsx @@ -26,6 +26,7 @@ import { Panel } from "../UI/Panel"; import { SkeletonPage } from "../UI/Loading"; import { Pagination } from "../UI/Pagination"; import { OrderFilters } from "../orders/OrderFilters"; +import { SmsProblemNotice } from "./SmsProblemNotice"; import { CallStatusBadge } from "../UI/CallStatusBadge"; import { formatDate, formatDateTime } from "../../utils/formatters"; @@ -509,6 +510,9 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], totalGroups: totalGr )} + {/* Причина, по которой SMS не уходят — до таблицы, чтобы не искать её в логе */} + g.id)} /> + {!totalGroups ? (
По этому поиску ничего не найдено. diff --git a/src/components/logistics/SmsProblemNotice.jsx b/src/components/logistics/SmsProblemNotice.jsx new file mode 100644 index 0000000..251f75c --- /dev/null +++ b/src/components/logistics/SmsProblemNotice.jsx @@ -0,0 +1,49 @@ +/** + * @file SmsProblemNotice.jsx + * @description Объясняет причину, по которой SMS не уходят. + * + * В таблице наборов видно только «SMS не отправлено», а причина лежит в логе + * отправок цифрой (например, 201 — на счёте SMS-сервиса закончились средства). + * Блок читает последние неудачные отправки, складывает их по причинам + * (summarizeSmsFailures) и показывает причину словами над таблицей. + * + * Если неудачных отправок нет — блок не показывается вообще. + */ +import React, { useEffect, useMemo, useState } from "react"; +import { supabase } from "../../supabaseClient"; +import { summarizeSmsFailures } from "../../services/smsProblemSummary"; +import { SmsProblemNoticeView } from "./SmsProblemNoticeView"; + +const LOOKBACK_DAYS = 30; +const FAILED_STATUSES = ["send_failed", "error", "limit_exceeded"]; + +export function SmsProblemNotice({ orderGroupIds = [], className = "" }) { + const [summary, setSummary] = useState(null); + const visibleKey = useMemo(() => orderGroupIds.join(","), [orderGroupIds]); + + useEffect(() => { + let alive = true; + (async () => { + try { + const since = new Date(Date.now() - LOOKBACK_DAYS * 86400000).toISOString(); + const { data, error } = await supabase + .from("sms_campaign_log") + .select("order_group_id, campaign_type, status, sms_code, error_message, created_at") + .in("status", FAILED_STATUSES) + .gte("created_at", since) + .order("created_at", { ascending: false }) + .limit(1000); + if (error) throw error; + if (alive) setSummary(summarizeSmsFailures(data || [], orderGroupIds)); + } catch { + // Лог может быть недоступен для роли — тогда просто ничего не показываем. + if (alive) setSummary(null); + } + })(); + return () => { + alive = false; + }; + }, [visibleKey]); // eslint-disable-line react-hooks/exhaustive-deps + + return ; +} diff --git a/src/components/logistics/SmsProblemNotice.test.jsx b/src/components/logistics/SmsProblemNotice.test.jsx new file mode 100644 index 0000000..691bbf8 --- /dev/null +++ b/src/components/logistics/SmsProblemNotice.test.jsx @@ -0,0 +1,61 @@ +/** + * @file SmsProblemNotice.test.jsx + * @description Блок над таблицей: причина «закончились средства» написана словами, + * видно период и число заказов; без причины блок не рисуется. + */ +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, it, expect } from "vitest"; +import { SmsProblemNoticeView } from "./SmsProblemNoticeView"; + +const fundsSummary = { + reason: "на счёте SMS-сервиса закончились средства", + messages: 42, + topMessages: 42, + groups: 8, + affectedInTable: 3, + hasVisibleRows: true, + hasFundsReason: true, + firstAt: "2026-09-10T10:06:11Z", + lastAt: "2026-09-13T06:03:03Z", +}; + +describe("SmsProblemNoticeView", () => { + it("пишет, что закончились средства, и что делать", () => { + const html = renderToStaticMarkup(); + + expect(html).toContain("SMS не отправлено: на счёте SMS-сервиса закончились средства"); + expect(html).toContain("8 заказов"); + expect(html).toContain("42 сообщения"); + expect(html).toContain("В таблице ниже — 3 из них."); + expect(html).toContain("Пополните счёт SMS-сервиса"); + }); + + it("для другой причины подсказка другая", () => { + const html = renderToStaticMarkup( + , + ); + + expect(html).toContain("SMS не отправлено: неверный номер получателя"); + expect(html).toContain("1 заказ"); + expect(html).toContain("разделе «SMS-рассылки»"); + expect(html).not.toContain("Пополните счёт"); + }); + + it("одно сообщение — «сообщение», одно число — «заказ»", () => { + const html = renderToStaticMarkup( + , + ); + expect(html).toContain("1 заказ"); + expect(html).toContain("1 сообщение"); + expect(html).toContain("за 10.09"); + }); + + it("без сводки ничего не рисуется", () => { + expect(renderToStaticMarkup()).toBe(""); + }); +}); diff --git a/src/components/logistics/SmsProblemNoticeView.jsx b/src/components/logistics/SmsProblemNoticeView.jsx new file mode 100644 index 0000000..a92d90b --- /dev/null +++ b/src/components/logistics/SmsProblemNoticeView.jsx @@ -0,0 +1,78 @@ +/** + * @file SmsProblemNoticeView.jsx + * @description Разметка блока «почему SMS не уходят». + * + * Отдельно от загрузки данных: сам блок только рисует то, что посчитал + * summarizeSmsFailures. Так разметку можно проверить без сети и Supabase. + */ +import React from "react"; +import { plural } from "../../services/smsProblemSummary"; + +const fmtDay = (iso) => { + if (!iso) return ""; + try { + return new Date(iso).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit" }); + } catch { + return ""; + } +}; + +export const SmsProblemNoticeView = ({ summary, className = "" }) => { + if (!summary) return null; + + const period = summary.firstAt + ? summary.firstAt === summary.lastAt + ? `за ${fmtDay(summary.firstAt)}` + : `с ${fmtDay(summary.firstAt)} по ${fmtDay(summary.lastAt)}` + : ""; + + const groupsText = `${summary.groups} ${plural(summary.groups, "заказ", "заказа", "заказов")}`; + const messagesText = `${summary.messages} ${plural(summary.messages, "сообщение", "сообщения", "сообщений")}`; + const tableText = + summary.affectedInTable > 0 + ? `В таблице ниже — ${summary.affectedInTable} из них.` + : summary.hasVisibleRows + ? "В текущей выборке таблицы таких заказов нет." + : ""; + + return ( +
+
+ {summary.hasFundsReason ? "⛔" : "⚠️"} + + SMS не отправлено: {summary.reason} + + + {groupsText} + +
+ +
+ {messagesText} + {period ? ` · ${period}` : ""} + {tableText ? ` · ${tableText}` : ""} +
+ +
+ {summary.hasFundsReason + ? "Пополните счёт SMS-сервиса — отправка возобновится автоматически, повторять вручную ничего не нужно." + : "Проверьте настройки отправки SMS в разделе «SMS-рассылки»."} +
+
+ ); +}; diff --git a/src/components/orders/SmsStatusCard.jsx b/src/components/orders/SmsStatusCard.jsx index 8fe8010..148d2ba 100644 --- a/src/components/orders/SmsStatusCard.jsx +++ b/src/components/orders/SmsStatusCard.jsx @@ -7,6 +7,7 @@ import React, { useState, useEffect, useCallback } from "react"; import { Badge } from "../UI/Badge"; import { supabase } from "../../supabaseClient"; +import { getSmsFailureReason, isSmsFundsError } from "../../services/orderGroupViews"; // ── Status labels ──────────────────────────────────────────────────────────── const NOTIF_LABELS = { @@ -209,6 +210,13 @@ export const SmsStatusCard = ({ order, userRole }) => { const hasSecondSms = isDelivered(secondSmsLog); const firstSmsFailed = firstSmsLog && ["expired", "error", "send_failed", "limit_exceeded"].includes(firstSmsLog.status); const secondSmsFailed = secondSmsLog && ["expired", "error", "send_failed", "limit_exceeded"].includes(secondSmsLog.status); + // Причина неудачи словами: в логе лежит только код сервиса (201, 202, ...). + const failureReason = (log) => { + if (!log) return null; + if (log.status === "expired") return "срок доставки сообщения истёк"; + return getSmsFailureReason(log) || log.status; + }; + const fundsProblem = [firstSmsLog, secondSmsLog].some((log) => isSmsFundsError(log)); return (
@@ -226,7 +234,7 @@ export const SmsStatusCard = ({ order, userRole }) => { {hasFirstSms ? (
{fmtTime(firstSmsLog.created_at)} ✓ получено клиентом
) : firstSmsFailed ? ( -
{fmtTime(firstSmsLog.created_at)} ⚠ {firstSmsLog.status === "expired" ? "срок доставки истёк" : firstSmsLog.status}
+
{fmtTime(firstSmsLog.created_at)} ⚠ не отправлено: {failureReason(firstSmsLog)}
) : firstSmsAt ? (
{fmtTime(firstSmsAt)} · отправлено, ждём ответ оператора…
) : notifStatus === "link_ready" ? ( @@ -245,7 +253,7 @@ export const SmsStatusCard = ({ order, userRole }) => { {hasSecondSms ? (
{fmtTime(secondSmsLog.created_at)} ✓ получено клиентом
) : secondSmsFailed ? ( -
{fmtTime(secondSmsLog.created_at)} ⚠ {secondSmsLog.status === "expired" ? "срок доставки истёк" : secondSmsLog.status}
+
{fmtTime(secondSmsLog.created_at)} ⚠ не отправлено: {failureReason(secondSmsLog)}
) : notifStatus === "first_sms_sent" && countdown ? (
отправка через {countdown} @@ -267,7 +275,14 @@ export const SmsStatusCard = ({ order, userRole }) => { {/* Error */} {lastError && (
- ⚠ {lastError} + ⚠ {typeof lastError === "string" ? getSmsFailureReason(lastError) || lastError : lastError} +
+ )} + + {/* Нехватка средств — самая частая причина, объясняем прямо в карточке */} + {fundsProblem && ( +
+ ⛔ На счёте SMS-сервиса закончились средства — сообщение не ушло. Пополните счёт: отправка возобновится автоматически.
)} diff --git a/src/services/orderGroupViews.js b/src/services/orderGroupViews.js index 13b81f7..fa2fbc7 100644 --- a/src/services/orderGroupViews.js +++ b/src/services/orderGroupViews.js @@ -277,6 +277,65 @@ export const SMS_STATUS_META = { error: { label: "Ошибка", tone: "danger" }, }; +// ─── Причина неудачной отправки SMS ──────────────────────────────── +// Коды SMS-сервиса (первая строка ответа) → человеческая причина. +// В логе лежит только цифра, поэтому расшифровываем её здесь. +export const SMS_ERROR_REASONS = { + "100": "сообщение в очереди SMS-сервиса", + "101": "сообщение передано оператору", + "102": "сообщение в пути", + "104": "истёк срок доставки сообщения", + "105": "сообщение удалено оператором", + "106": "сбой на телефоне абонента", + "107": "оператор не сообщил причину", + "108": "оператор отклонил сообщение", + "130": "лимит сообщений на этот номер в сутки", + "131": "лимит одинаковых сообщений в минуту", + "132": "лимит одинаковых сообщений в сутки", + "200": "неверный ключ доступа к SMS-сервису", + "201": "на счёте SMS-сервиса закончились средства", + "202": "неверный номер получателя", + "203": "пустой текст сообщения", + "230": "исчерпан общий суточный лимит аккаунта", + "231": "лимит одинаковых сообщений в минуту", + "232": "лимит одинаковых сообщений в сутки", +}; + +// Код нехватки средств — по нему показываем отдельное предупреждение. +export const SMS_FUNDS_ERROR_CODE = "201"; + +const smsErrorCode = (log) => { + if (!log) return ""; + const direct = String(log.sms_code ?? "").trim(); + if (direct) return direct.match(/\d{3}/)?.[0] || direct; + const fromText = String(log.error_message ?? "").trim(); + return fromText.match(/\d{3}/)?.[0] || ""; +}; + +/** + * Причина, по которой SMS не ушла — словами. Принимает строку лога + * (sms_campaign_log) либо значение lastSmsError у группы. + */ +export const getSmsFailureReason = (log) => { + if (!log) return null; + // В некоторых местах причина лежит просто строкой: "201" или "201\n" + if (typeof log === "string") { + const text = log.trim(); + const onlyCode = text.match(/^(\d{3})$/)?.[1]; + if (onlyCode && SMS_ERROR_REASONS[onlyCode]) return SMS_ERROR_REASONS[onlyCode]; + return text.slice(0, 120) || null; + } + const code = smsErrorCode(log); + if (code && SMS_ERROR_REASONS[code]) return SMS_ERROR_REASONS[code]; + const raw = String(log.error_message ?? "").trim(); + if (code) return `код ${code}`; + if (!raw) return null; + return raw.slice(0, 120); +}; + +/** Нехватка средств на счёте SMS-сервиса — самая частая и заметная причина. */ +export const isSmsFundsError = (log) => smsErrorCode(log) === SMS_FUNDS_ERROR_CODE; + // ─── Автодозвон: единый человеческий статус (по аналогии со статусом SMS) ── // Принимает массив autocall_log (отсортированный по created_at desc) или null. export const AUTOCALL_STATUS_META = { diff --git a/src/services/orderGroupViews.smsReasons.test.js b/src/services/orderGroupViews.smsReasons.test.js new file mode 100644 index 0000000..ccdb640 --- /dev/null +++ b/src/services/orderGroupViews.smsReasons.test.js @@ -0,0 +1,67 @@ +/** + * @file orderGroupViews.smsReasons.test.js + * @description Причина неудачной отправки SMS должна быть понятна человеку: + * в логе лежит код сервиса (201), а показывать нужно «закончились средства». + */ +import { describe, it, expect } from "vitest"; +import { + getSmsFailureReason, + isSmsFundsError, + SMS_ERROR_REASONS, + getSmsProblemTone, +} from "./orderGroupViews"; + +describe("getSmsFailureReason", () => { + it("расшифровывает код 201 — закончились средства", () => { + expect(getSmsFailureReason({ sms_code: "201", error_message: "201" })) + .toBe("на счёте SMS-сервиса закончились средства"); + }); + + it("понимает код, пришедший строкой с переводом строки", () => { + expect(getSmsFailureReason("201\n")).toBe("на счёте SMS-сервиса закончились средства"); + }); + + it("берёт код из текста ошибки, если колонка кода пуста", () => { + expect(getSmsFailureReason({ sms_code: null, error_message: "202" })) + .toBe("неверный номер получателя"); + }); + + it("неизвестный код показывает как код, а не как пустоту", () => { + expect(getSmsFailureReason({ sms_code: "999" })).toBe("код 999"); + }); + + it("без данных возвращает null", () => { + expect(getSmsFailureReason(null)).toBeNull(); + expect(getSmsFailureReason({})).toBeNull(); + }); + + it("все коды из таблицы расшифрованы человеческими словами", () => { + for (const [code, text] of Object.entries(SMS_ERROR_REASONS)) { + expect(text).not.toMatch(/^\d{3}$/); + expect(getSmsFailureReason({ sms_code: code })).toBe(text); + } + }); +}); + +describe("isSmsFundsError", () => { + it("распознаёт нехватку средств", () => { + expect(isSmsFundsError({ sms_code: "201" })).toBe(true); + expect(isSmsFundsError({ error_message: "201\n" })).toBe(true); + }); + + it("не считает нехваткой средств другие ошибки", () => { + expect(isSmsFundsError({ sms_code: "202" })).toBe(false); + expect(isSmsFundsError(null)).toBe(false); + }); +}); + +describe("подсветка SMS в таблице остаётся прежней", () => { + it("без отправленных SMS — «не отправлено»", () => { + expect(getSmsProblemTone({})).toBe("not_sent"); + }); + + it("отправлено с ошибкой — «не доставлено»", () => { + expect(getSmsProblemTone({ firstSmsSentAt: "2026-09-13T06:00:00Z", lastSmsError: "201" })) + .toBe("not_delivered"); + }); +}); diff --git a/src/services/smsProblemSummary.js b/src/services/smsProblemSummary.js new file mode 100644 index 0000000..d66f8a6 --- /dev/null +++ b/src/services/smsProblemSummary.js @@ -0,0 +1,61 @@ +/** + * @file smsProblemSummary.js + * @description Складывает неудачные отправки SMS в одну понятную причину. + * + * В таблице видно только «SMS не отправлено», а причина в логе — код сервиса. + * Здесь считаем: сколько сообщений, сколько заказов, за какой период и что + * именно случилось чаще всего (например, что на счёте закончились средства). + */ +import { getSmsFailureReason } from "./orderGroupViews"; + +/** + * @param {Array} rows строки sms_campaign_log со статусами ошибок + * @param {Array} visibleIds id заказов, которые сейчас видны в таблице + * @returns {null|{reason, messages, groups, affectedInTable, hasFundsReason, firstAt, lastAt}} + */ +export const summarizeSmsFailures = (rows = [], visibleIds = []) => { + const list = Array.isArray(rows) ? rows.filter(Boolean) : []; + if (!list.length) return null; + + 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] = [...reasons.entries()].sort((a, b) => b[1] - a[1])[0]; + const visible = new Set(visibleIds); + + return { + reason, + messages: list.length, + topMessages, + groups: lastByGroup.size, + affectedInTable: [...lastByGroup.keys()].filter((id) => visible.has(id)).length, + hasVisibleRows: visible.size > 0, + hasFundsReason: /закончились средства/.test(reason), + firstAt, + lastAt, + }; +}; + +/** «сообщение / сообщения / сообщений», «заказ / заказа / заказов». */ +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; +}; diff --git a/src/services/smsProblemSummary.test.js b/src/services/smsProblemSummary.test.js new file mode 100644 index 0000000..fcda378 --- /dev/null +++ b/src/services/smsProblemSummary.test.js @@ -0,0 +1,64 @@ +/** + * @file smsProblemSummary.test.js + * @description Причина неудачных SMS для блока над таблицей: считаем по логу, + * чаще всего встречающуюся причину пишем словами, пустой лог — ничего не показываем. + */ +import { describe, it, expect } from "vitest"; +import { summarizeSmsFailures } from "./smsProblemSummary"; + +const row = (over = {}) => ({ + order_group_id: "g1", + campaign_type: "first_sms", + status: "send_failed", + sms_code: "201", + error_message: "201\n", + created_at: "2026-09-10T10:06:11Z", + ...over, +}); + +describe("summarizeSmsFailures", () => { + it("объясняет нехватку средств и считает заказы с сообщениями", () => { + const summary = summarizeSmsFailures([ + row({ order_group_id: "g1", created_at: "2026-09-10T10:06:11Z" }), + row({ order_group_id: "g2", created_at: "2026-09-13T06:03:03Z" }), + row({ order_group_id: "g2", created_at: "2026-09-13T06:03:04Z" }), + ], ["g1", "g2"]); + + expect(summary.reason).toBe("на счёте SMS-сервиса закончились средства"); + expect(summary.hasFundsReason).toBe(true); + expect(summary.groups).toBe(2); + expect(summary.messages).toBe(3); + expect(summary.affectedInTable).toBe(2); + expect(summary.firstAt).toBe("2026-09-10T10:06:11Z"); + expect(summary.lastAt).toBe("2026-09-13T06:03:03Z".replace("06:03:03", "06:03:04")); + }); + + it("не считает заказ в таблице, если его там нет", () => { + const summary = summarizeSmsFailures([row({ order_group_id: "g9" })], ["g1"]); + expect(summary.affectedInTable).toBe(0); + expect(summary.hasVisibleRows).toBe(true); + }); + + it("берёт самую частую причину, когда их несколько", () => { + const summary = summarizeSmsFailures([ + row({ order_group_id: "g1", sms_code: "202", error_message: "202" }), + row({ order_group_id: "g2" }), + row({ order_group_id: "g3" }), + ], []); + + expect(summary.reason).toBe("на счёте SMS-сервиса закончились средства"); + expect(summary.topMessages).toBe(2); + }); + + it("пустой лог — нет повода что-то показывать", () => { + expect(summarizeSmsFailures([], ["g1"])).toBeNull(); + expect(summarizeSmsFailures(null, [])).toBeNull(); + }); + + it("без id заказов причина всё равно объясняется", () => { + const summary = summarizeSmsFailures([row()], []); + expect(summary.hasFundsReason).toBe(true); + expect(summary.hasVisibleRows).toBe(false); + expect(summary.affectedInTable).toBe(0); + }); +});