import { describe, expect, it } from "vitest"; import { composeCounterpartyAnalyticsReply } from "../src/services/address_runtime/counterpartyAnalyticsReplyBuilders"; import { composeFactualReply } from "../src/services/address_runtime/composeStage"; import { composeInventoryReply } from "../src/services/address_runtime/inventoryReplyBuilders"; describe("address reply builders regressions", () => { it("starts top customer aggregate reply with a direct business answer", () => { const result = composeCounterpartyAnalyticsReply( "customer_revenue_and_payments", [ { counterparty: "Чапурнов", amount: 250000, period: "2020-03-31", registrator: "Поступление 1" } as any, { counterparty: "Малый клиент", amount: 100000, period: "2020-04-30", registrator: "Поступление 2" } as any ], { userMessage: "кто у нас самый доходный клиент за все время?" }, { formatPercent: () => null, formatDateRu: (value: string) => value, formatMoneyRub: (value: number) => `${value} ₽`, extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null), detectCounterpartyProfileFocus: () => "full_profile", detectCounterpartyLifecycleFocus: () => "active_customers_all_time", hasCounterpartyLifecycleLongevityQuestion: () => false, hasCounterpartyActivityAgeQuestion: () => false, detectRankingLimit: () => 5, detectValueRankingFocus: () => "top_by_total", detectContractValueFocus: () => "top_by_turnover", detectMinOpsForAvgCheck: () => 1, extractRequestedYearFromQuestion: () => null, extractCounterpartyName: (row: any) => row.counterparty ?? "Чапурнов", extractContractName: () => null, counterpartyLookupMatches: () => false, toUtcDayTimestamp: () => null, formatAgeYearsMonthsDays: () => "0 дней", normalizeQuestionText: (value: string | null | undefined) => String(value ?? "") } ); expect(result?.text.split("\n")[0]).toContain("Самый доходный клиент"); expect(result?.text.split("\n")[0]).toContain("Чапурнов"); }); it("keeps canonical single-best customer wording compact instead of returning the default top list", () => { const result = composeCounterpartyAnalyticsReply( "customer_revenue_and_payments", [ { counterparty: "Крупный клиент", amount: 250000, period: "2020-03-31", registrator: "Поступление 1" } as any, { counterparty: "Малый клиент", amount: 100000, period: "2020-04-30", registrator: "Поступление 2" } as any ], { userMessage: "определить самого доходного клиента за весь период" }, { formatPercent: () => null, formatDateRu: (value: string) => value, formatMoneyRub: (value: number) => `${value} руб.`, extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null), detectCounterpartyProfileFocus: () => "full_profile", detectCounterpartyLifecycleFocus: () => "active_customers_all_time", hasCounterpartyLifecycleLongevityQuestion: () => false, hasCounterpartyActivityAgeQuestion: () => false, detectRankingLimit: () => 20, detectValueRankingFocus: () => "top_by_total", detectContractValueFocus: () => "top_by_turnover", detectMinOpsForAvgCheck: () => 1, extractRequestedYearFromQuestion: () => null, extractCounterpartyName: (row: any) => row.counterparty, extractContractName: () => null, counterpartyLookupMatches: () => false, toUtcDayTimestamp: () => null, formatAgeYearsMonthsDays: () => "0 дней", normalizeQuestionText: (value: string | null | undefined) => String(value ?? "").toLowerCase() } ); expect(result?.text.split("\n")[0]).toContain("Крупный клиент"); expect(result?.text).not.toContain("2. Малый клиент"); }); it("keeps direct receivables snapshot answers compact", () => { const result = composeFactualReply( "receivables_confirmed_as_of_date", [ { period: "2020-05-10", registrator: "Поступление 1", account_dt: "62.01", account_kt: "90.01", amount: 100000, analytics: ["Клиент А"], counterparty: "Клиент А" }, { period: "2020-05-11", registrator: "Поступление 2", account_dt: "62.01", account_kt: "90.01", amount: 50000, analytics: ["Клиент Б"], counterparty: "Клиент Б" } ], { userMessage: "кто нам должен денег на май 2020", asOfDate: "2020-05-31" } ); expect(result.text.split("\n")[0]).toContain("Коротко:"); expect(result.text.split("\n")[0]).toContain("Клиент А"); expect(result.text.length).toBeLessThan(1200); expect(result.text).toContain("Крупнейшие позиции к получению"); }); it("does not overclaim a comparative top customer ranking when only one candidate is present", () => { const result = composeCounterpartyAnalyticsReply( "customer_revenue_and_payments", [ { amount: 250000, period: "2021-03-31", registrator: "Поступление 1" } as any ], { userMessage: "кто больше всего принес денег в 2021" }, { formatPercent: () => null, formatDateRu: (value: string) => value, formatMoneyRub: (value: number) => `${value} ₽`, extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null), detectCounterpartyProfileFocus: () => "full_profile", detectCounterpartyLifecycleFocus: () => "active_customers_all_time", hasCounterpartyLifecycleLongevityQuestion: () => false, hasCounterpartyActivityAgeQuestion: () => false, detectRankingLimit: () => 5, detectValueRankingFocus: () => "top_by_total", detectContractValueFocus: () => "top_by_turnover", detectMinOpsForAvgCheck: () => 1, extractRequestedYearFromQuestion: () => null, extractCounterpartyName: () => "Группа СВК", extractContractName: () => null, counterpartyLookupMatches: () => false, toUtcDayTimestamp: () => null, formatAgeYearsMonthsDays: () => "0 дней", normalizeQuestionText: (value: string | null | undefined) => String(value ?? "") } ); expect(result?.text.split("\n")[0]).toContain("найден один клиент"); expect(result?.text.split("\n")[0]).toContain("не полноценный сравнительный рейтинг"); expect(result?.text).not.toContain("Самый доходный клиент"); }); it("starts top year aggregate reply with a direct business answer", () => { const result = composeCounterpartyAnalyticsReply( "customer_revenue_and_payments", [ { amount: 320000, period: "2021-05-12", registrator: "Поступление 2" } as any ], { userMessage: "какой у нас самый доходный год" }, { formatPercent: () => null, formatDateRu: (value: string) => value, formatMoneyRub: (value: number) => `${value} ₽`, extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null), detectCounterpartyProfileFocus: () => "full_profile", detectCounterpartyLifecycleFocus: () => "active_customers_all_time", hasCounterpartyLifecycleLongevityQuestion: () => false, hasCounterpartyActivityAgeQuestion: () => false, detectRankingLimit: () => 5, detectValueRankingFocus: () => "top_years_by_total", detectContractValueFocus: () => "top_by_turnover", detectMinOpsForAvgCheck: () => 1, extractRequestedYearFromQuestion: () => null, extractCounterpartyName: () => "Чапурнов", extractContractName: () => null, counterpartyLookupMatches: () => false, toUtcDayTimestamp: () => null, formatAgeYearsMonthsDays: () => "0 дней", normalizeQuestionText: (value: string | null | undefined) => String(value ?? "") } ); expect(result?.text.split("\n")[0]).toContain("Самый доходный год"); expect(result?.text.split("\n")[0]).toContain("2021"); }); it("keeps very old stock answer free of explicit as-of date in the first line", () => { const result = composeInventoryReply( "inventory_aging_by_purchase_date", [ { amount: 1000, period: "2015-02-05", registrator: "Поступление 3" } as any ], { userMessage: "Есть ли остатки товара, которые закупались очень давно", asOfDate: "2026-04-18" }, { resolvePayablesAsOfDate: () => "2026-04-18", buildInventoryOnHandAggregate: () => [], uniqueStrings: (values: string[]) => values, formatDateRu: (value: string) => value, formatNumberWithDots: (value: number) => String(value), formatMoneyRub: (value: number) => `${value} ₽`, isInventoryPurchaseMovement: () => true, summarizeInventoryTraceRows: () => ({ item: "Рабочая станция", warehouses: ["Основной склад"], organizations: ["ООО Альтернатива Плюс"], counterparties: ["Чапурнов"], documents: ["Поступление 3"], firstPeriod: "2015-02-05", lastPeriod: "2015-02-05", totalAmount: 1000 }), formatInventoryTraceRows: () => [], hasInventoryPurchaseDateActionFocus: () => false, inventoryTraceDateLabel: (value: string | null) => value ?? "дата не указана", extractInventoryCounterpartyCandidates: () => ["Чапурнов"], buildInventoryAgingByItemAggregate: () => [ { item: "Рабочая станция", warehouse: "Основной склад", organization: "ООО Альтернатива Плюс", firstPurchasePeriod: "2015-02-05", lastPurchasePeriod: "2015-02-05", operations: 1, documentCount: 1, counterparties: ["Чапурнов"], ageDays: 4089 } ], formatInventoryAgingRows: () => ["1. Рабочая станция | первая закупка: 2015-02-05"], isInventorySaleMovement: () => false } ); const firstLine = result?.text.split("\n")[0] ?? ""; expect(firstLine).toContain("К самым старым закупкам"); expect(firstLine).not.toContain("2026-04-18"); }); it("excludes mirrored 76 settlements from clean payables", () => { const result = composeFactualReply( "payables_confirmed_as_of_date", [ { period: "2026-05-12", registrator: "", account_dt: "51", account_kt: "76.09", amount: 3677454.14, analytics: ["Комитет государственных услуг г. Москвы", "ООО \\Альтернатива Плюс\\", "Финансовое обеспечение заявки"], counterparty: "Комитет государственных услуг г. Москвы", organization: "ООО \\Альтернатива Плюс\\" }, { period: "2026-05-12", registrator: "Списание с расчетного счета 1", account_dt: "76.09", account_kt: "51", amount: 1000000, analytics: ["Комитет государственных услуг г. Москвы", "ООО \\Альтернатива Плюс\\", "Финансовое обеспечение заявки"], counterparty: "Комитет государственных услуг г. Москвы", organization: "ООО \\Альтернатива Плюс\\" }, { period: "2026-05-12", registrator: "Списание с расчетного счета 2", account_dt: "76.09", account_kt: "51", amount: 2677454.14, analytics: ["Комитет государственных услуг г. Москвы", "ООО \\Альтернатива Плюс\\", "Финансовое обеспечение заявки"], counterparty: "Комитет государственных услуг г. Москвы", organization: "ООО \\Альтернатива Плюс\\" }, { period: "2026-05-12", registrator: "Поступление товаров 1", account_dt: "41.01", account_kt: "60.01", amount: 7271.2, analytics: ["Авант мебель"], counterparty: "Авант мебель", organization: "ООО Альтернатива Плюс" } ], { userMessage: "мы должны кому-то денег на сегодня?", asOfDate: "2026-05-12" } ); const firstLine = result.text.split("\n")[0] ?? ""; expect(firstLine).toContain("7.271,20"); expect(firstLine).toContain("Авант мебель"); expect(firstLine).not.toContain("3.677.454,14"); expect(result.text).toContain("встречных остатков"); expect(result.text).toContain("Комитет государственных услуг г. Москвы"); expect(result.text).toContain("Финансовое обеспечение заявки"); expect(result.text).not.toContain("договор/аналитика: ООО \\Альтернатива Плюс\\"); }); it("excludes mirrored 76 settlements from clean receivables", () => { const result = composeFactualReply( "receivables_confirmed_as_of_date", [ { period: "2026-05-12", registrator: "Реализация 1", account_dt: "62.01", account_kt: "90.01", amount: 9612904.9, analytics: ["Департамент капитального ремонта города Москвы."], counterparty: "Департамент капитального ремонта города Москвы.", organization: "ООО Альтернатива Плюс" }, { period: "2026-05-12", registrator: "", account_dt: "51", account_kt: "76.09", amount: 3677454.14, analytics: ["Комитет государственных услуг г. Москвы", "Финансовое обеспечение заявки"], counterparty: "Комитет государственных услуг г. Москвы", organization: "ООО Альтернатива Плюс" }, { period: "2026-05-12", registrator: "Списание с расчетного счета 1", account_dt: "76.09", account_kt: "51", amount: 3677454.14, analytics: ["Комитет государственных услуг г. Москвы", "Финансовое обеспечение заявки"], counterparty: "Комитет государственных услуг г. Москвы", organization: "ООО Альтернатива Плюс" } ], { userMessage: "а нам кто должен на сегодня?", asOfDate: "2026-05-12" } ); const firstLine = result.text.split("\n")[0] ?? ""; expect(firstLine).toContain("9.612.904,90"); expect(firstLine).toContain("Департамент капитального ремонта города Москвы."); expect(firstLine).not.toContain("13.290.359,04"); expect(result.text).toContain("встречных остатков"); expect(result.text).toContain("Комитет государственных услуг г. Москвы"); }); it("keeps bank-named balance counterparties as counterparties, not settlement analytics", () => { const result = composeFactualReply( "receivables_confirmed_as_of_date", [ { period: "2017-05-31", registrator: "Остатки на дату", account_dt: "76.09", account_kt: null, amount: 39079.12, analytics: ["Сбербанк-АСТ, ЗАО", "Финансовое обеспечение заявки"], organization: "ООО \\Альтернатива Плюс\\" } ], { userMessage: "кто нам должен денег на май 2017?", asOfDate: "2017-05-31" } ); const firstLine = result.text.split("\n")[0] ?? ""; expect(firstLine).toContain("Сбербанк-АСТ, ЗАО"); expect(firstLine).not.toContain("Финансовое обеспечение заявки"); }); it("keeps different 76 contracts separated instead of netting by counterparty only", () => { const result = composeFactualReply( "payables_confirmed_as_of_date", [ { period: "2026-05-12", registrator: "", account_dt: "51", account_kt: "76.09", amount: 1000, analytics: ["Комитет государственных услуг г. Москвы", "Договор А"], counterparty: "Комитет государственных услуг г. Москвы", organization: "ООО Альтернатива Плюс" }, { period: "2026-05-12", registrator: "Списание с расчетного счета 1", account_dt: "76.09", account_kt: "51", amount: 1000, analytics: ["Комитет государственных услуг г. Москвы", "Договор Б"], counterparty: "Комитет государственных услуг г. Москвы", organization: "ООО Альтернатива Плюс" } ], { userMessage: "мы должны кому-то денег на сегодня?", asOfDate: "2026-05-12" } ); expect(result.text.split("\n")[0]).toContain("1.000,00"); expect(result.text).not.toContain("встречных остатков"); }); });