import { describe, expect, it } from "vitest"; import { detectAddressQuestionMode } from "../src/services/addressQueryClassifier"; import { resolveAddressIntent } from "../src/services/addressIntentResolver"; import { classifyAddressQueryShape } from "../src/services/addressQueryShapeClassifier"; import { extractAddressFilters } from "../src/services/addressFilterExtractor"; import { AddressQueryService } from "../src/services/addressQueryService"; import { buildAddressRecipePlan, selectAddressRecipe } from "../src/services/addressRecipeCatalog"; import { runAddressDecomposeStage } from "../src/services/address_runtime/decomposeStage"; import { composeFactualReply } from "../src/services/address_runtime/composeStage"; describe("address query shape classifier", () => { it("classifies explain question as deep-shape", () => { const result = classifyAddressQueryShape("Why VAT chain does not match?"); expect(result.shape).toBe("EXPLAIN_OR_REASON"); expect(result.confidence).toBe("high"); }); it("classifies aggregate lookup question", () => { const result = classifyAddressQueryShape("who owes us today?"); expect(result.shape).toBe("AGGREGATE_LOOKUP"); }); it("classifies compound factual question", () => { const result = classifyAddressQueryShape("who owes us and who we owe today?"); expect(result.shape).toBe("COMPOUND_FACTUAL_QUERY"); }); it("keeps company lookup phrasing in address lane", () => { const result = detectAddressQuestionMode("какие компании есть в базе"); expect(result.mode).toBe("address_query"); }); it("keeps loose by-anchor follow-up phrasing in address lane", () => { const result = detectAddressQuestionMode("за любой период есть что-то по свк?"); expect(result.mode).toBe("address_query"); }); it("keeps slang transaction phrasing in address lane", () => { const result = detectAddressQuestionMode("транзакции по свк за 2020"); expect(result.mode).toBe("address_query"); }); it("keeps short balance slang with compact account token in address lane", () => { const result = detectAddressQuestionMode("скока по 60.02 на конец 2020-12"); expect(result.mode).toBe("address_query"); }); it("keeps management period profile question in address lane", () => { const result = detectAddressQuestionMode("За какие годы в базе есть данные?"); expect(result.mode).toBe("address_query"); }); it("keeps management document/section profile question in address lane", () => { const result = detectAddressQuestionMode("Какие разделы учета наиболее заполнены и какие почти не используются?"); expect(result.mode).toBe("address_query"); }); it("keeps management counterparty population question in address lane", () => { const result = detectAddressQuestionMode("Сколько всего уникальных контрагентов в базе?"); expect(result.mode).toBe("address_query"); }); it("keeps slang supplier count question in address lane", () => { const result = detectAddressQuestionMode("скока поставщиков в базе"); expect(result.mode).toBe("address_query"); }); it("keeps slang client count question in address lane", () => { const result = detectAddressQuestionMode("скок клиентов"); expect(result.mode).toBe("address_query"); }); it("keeps customer activity lifecycle question in address lane", () => { const result = detectAddressQuestionMode("Какие заказчики работали с нами в 2020 году?"); expect(result.mode).toBe("address_query"); }); it("keeps customer list all-time question in address lane", () => { const result = detectAddressQuestionMode("выведи список заказчиков за все время"); expect(result.mode).toBe("address_query"); }); it("keeps customer list short-year question in address lane", () => { const result = detectAddressQuestionMode("покажи список заказчиков за 20год"); expect(result.mode).toBe("address_query"); }); it("keeps noisy management phrase about years alive in address lane", () => { const result = detectAddressQuestionMode("за какие года база ваще живая?"); expect(result.mode).toBe("address_query"); }); it("keeps noisy month-peak phrase in address lane", () => { const result = detectAddressQuestionMode("а теперь месяц-пик по операциям"); expect(result.mode).toBe("address_query"); }); it("keeps management contract usage overview question in address lane", () => { const result = detectAddressQuestionMode("Сколько всего договоров заведено и сколько из них реально использовались?"); expect(result.mode).toBe("address_query"); }); it("keeps customer value ranking question in address lane", () => { const result = detectAddressQuestionMode("какие клиенты самые доходные, выдай топ-20"); expect(result.mode).toBe("address_query"); }); it("keeps highest inflow slang question in address lane", () => { const result = detectAddressQuestionMode("какие приходы самые высокие за все время"); expect(result.mode).toBe("address_query"); }); it("keeps typo customer highest-check question in address lane", () => { const result = detectAddressQuestionMode("с каких кликентов самый высокий чек"); expect(result.mode).toBe("address_query"); }); it("keeps supplier payout ranking question in address lane", () => { const result = detectAddressQuestionMode("кому мы больше всего сгрузили денег, топ-20 поставщиков"); expect(result.mode).toBe("address_query"); }); it("keeps contract turnover ranking question in address lane", () => { const result = detectAddressQuestionMode("договоры по обороту ранкни и дай топ-20"); expect(result.mode).toBe("address_query"); }); it("keeps top contract wording with 'контракт' in address lane", () => { const result = detectAddressQuestionMode("по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?"); expect(result.mode).toBe("address_query"); }); it("extracts item anchor for inventory provenance questions", () => { const filters = extractAddressFilters( "От какого поставщика куплен товар Шкаф картотечный?", "inventory_purchase_provenance_for_item" ).extracted_filters; expect(filters.item).toBe("Шкаф картотечный"); }); it("cuts inventory item anchor before current-stock tail", () => { const filters = extractAddressFilters( "От какого поставщика куплен товар Диван трехместный из текущего остатка на складе Основной склад", "inventory_purchase_provenance_for_item" ).extracted_filters; expect(filters.item).toBe("Диван трехместный"); expect(filters.warehouse).toBe("Основной склад"); }); it("cuts inventory item anchor before chain suffix and ignores chain pseudo-warehouse", () => { const filters = extractAddressFilters( "Через какие документы прошел путь товара Шкаф картотечный 1000*400*2100: закупка -> склад -> продажа", "inventory_purchase_to_sale_chain" ).extracted_filters; expect(filters.item).toBe("Шкаф картотечный 1000*400*2100"); expect(filters.warehouse).toBeUndefined(); }); it("cuts inventory item anchor before purchase-doc residue tail", () => { const filters = extractAddressFilters( "По каким документам был куплен товар Диван трехместный для остатка на складе Основной склад", "inventory_purchase_documents_for_item" ).extracted_filters; expect(filters.item).toBe("Диван трехместный"); expect(filters.warehouse).toBe("Основной склад"); }); it("extracts item anchor from selected-object inventory row for provenance follow-up", () => { const filters = extractAddressFilters( 'По выбранному объекту "Кромка с клеем 33 альмандин 137 м | склад: Основной склад | количество: 1,000 | стоимость: 165,83 ₽ | организация: ООО \\\\Альтернатива Плюс\\\\ | дата строки: 2021-03-31T23:59:59Z": От какого поставщика куплен товар', "inventory_purchase_provenance_for_item" ).extracted_filters; expect(filters.item).toBe("Кромка с клеем 33 альмандин 137 м"); }); it("extracts item anchor from selected-object purchase-doc follow-up without explicit word товар", () => { const filters = extractAddressFilters( 'По выбранному объекту "Столешница 600*3050*26 дуб ниагара": по каким документам это купили', "inventory_purchase_documents_for_item" ).extracted_filters; expect(filters.item).toBe("Столешница 600*3050*26 дуб ниагара"); }); it("keeps colloquial selected-object supplier follow-up in inventory provenance intent", () => { const mode = detectAddressQuestionMode( 'По выбранному объекту "Кромка с клеем 33 альмандин 137 м": кто поставил этот товар' ); const result = resolveAddressIntent( 'По выбранному объекту "Кромка с клеем 33 альмандин 137 м": кто поставил этот товар' ); expect(mode.mode).toBe("address_query"); expect(result.intent).toBe("inventory_purchase_provenance_for_item"); }); it("keeps selected-object supplier slang with 'кто это поставил нам' in inventory provenance intent", () => { const mode = detectAddressQuestionMode( 'По выбранному объекту "Столешница 600*3050*26 дуб ниагара": кто это поставил нам' ); const result = resolveAddressIntent( 'По выбранному объекту "Столешница 600*3050*26 дуб ниагара": кто это поставил нам' ); expect(mode.mode).toBe("address_query"); expect(result.intent).toBe("inventory_purchase_provenance_for_item"); }); it("keeps selected-object colloquial supplier wording 'у кого купили' in inventory provenance intent", () => { const mode = detectAddressQuestionMode( 'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": у кого купили' ); const result = resolveAddressIntent( 'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": у кого купили' ); expect(mode.mode).toBe("address_query"); expect(result.intent).toBe("inventory_purchase_provenance_for_item"); }); it("keeps selected-object colloquial supplier wording 'где мы купили это' in inventory provenance intent", () => { const mode = detectAddressQuestionMode( 'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": где мы купили это' ); const result = resolveAddressIntent( 'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": где мы купили это' ); expect(mode.mode).toBe("address_query"); expect(result.intent).toBe("inventory_purchase_provenance_for_item"); }); it("keeps selected-object terse supplier wording 'где куплено!!' in inventory provenance intent", () => { const mode = detectAddressQuestionMode( 'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": где куплено!!' ); const result = resolveAddressIntent( 'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": где куплено!!' ); expect(mode.mode).toBe("address_query"); expect(result.intent).toBe("inventory_purchase_provenance_for_item"); }); it("keeps selected-object purchase-doc slang with 'по каким документам это купили' in purchase-doc intent", () => { const mode = detectAddressQuestionMode( 'По выбранному объекту "Столешница 600*3050*26 дуб ниагара": по каким документам это купили' ); const result = resolveAddressIntent( 'По выбранному объекту "Столешница 600*3050*26 дуб ниагара": по каким документам это купили' ); expect(mode.mode).toBe("address_query"); expect(result.intent).toBe("inventory_purchase_documents_for_item"); }); it("keeps full supplier anchor with comma suffix for stock-overlap questions", () => { const filters = extractAddressFilters( "Какие товары от поставщика Гамма-мебель, ООО сейчас еще лежат на складе Основной склад?", "inventory_supplier_stock_overlap_as_of_date" ).extracted_filters; expect(filters.counterparty).toBe("Гамма-мебель, ООО"); }); it("does not capture organization wording as supplier anchor in overlap questions", () => { const filters = extractAddressFilters( "У какого поставщика были куплены товары, которые сейчас лежат на складе Основной склад организации ООО \\Альтернатива Плюс\\", "inventory_supplier_stock_overlap_as_of_date" ).extracted_filters; expect(filters.counterparty).toBeUndefined(); expect(filters.warehouse).toBe("Основной склад"); }); it("extracts item anchor for inventory aging questions", () => { const filters = extractAddressFilters( "Относится ли товар Шкаф картотечный 1000*400*2100 в остатке на дату 2020-03-31 к старым закупкам", "inventory_aging_by_purchase_date" ).extracted_filters; expect(filters.item).toBe("Шкаф картотечный 1000*400*2100"); }); it("builds dedicated inventory purchase-documents query plan", () => { const selected = selectAddressRecipe("inventory_purchase_documents_for_item", { item: "Шкаф картотечный", as_of_date: "2020-03-31" }); expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_purchase_documents_for_item_v1"); const plan = buildAddressRecipePlan(selected.selected_recipe!, { item: "Шкаф картотечный", as_of_date: "2020-03-31" }); expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто"); expect(plan.query).toContain("Движения.СчетДт"); expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Движения.СубконтоДт1) КАК СубконтоДт1"); expect(plan.query).toContain("41.01"); }); it("builds overlap recipe for supplier-linked stock slice", () => { const selected = selectAddressRecipe("inventory_supplier_stock_overlap_as_of_date", { counterparty: "Гамма-мебель, ООО", warehouse: "Основной склад", as_of_date: "2020-03-31" }); expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_supplier_stock_overlap_as_of_date_v1"); const plan = buildAddressRecipePlan(selected.selected_recipe!, { counterparty: "Гамма-мебель, ООО", warehouse: "Основной склад", as_of_date: "2020-03-31" }); expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто"); expect(plan.query).toContain("Движения.СчетДт"); expect(plan.query).toContain("41.01"); }); it("renders inventory purchase documents from purchase-side 41.01 movements", () => { const reply = composeFactualReply( "inventory_purchase_documents_for_item", [ { period: "2020-03-15T12:00:00Z", registrator: "Поступление товаров и услуг 0001", account_dt: "41.01", account_kt: "60.01", amount: 150000, analytics: ["Шкаф картотечный", "Основной склад", "ООО Ромашка", "Гамма-мебель, ООО"], item: "Шкаф картотечный", warehouse: "Основной склад", organization: "ООО Ромашка" } ], { asOfDate: "2020-03-31", useRubCurrency: true } ); expect(reply.text.split("\n")[0]).toContain("документов закупки"); expect(reply.text).toContain("Шкаф картотечный"); expect(reply.text).toContain("Поступление товаров и услуг 0001"); expect(reply.semantics?.result_mode).toBe("confirmed_balance"); }); it("renders inventory provenance summary from purchase-side 41.01 movements", () => { const reply = composeFactualReply( "inventory_purchase_provenance_for_item", [ { period: "2020-03-15T12:00:00Z", registrator: "Поступление товаров и услуг 0001", account_dt: "41.01", account_kt: "60.01", amount: 150000, analytics: ["Шкаф картотечный", "Основной склад", "ООО Ромашка", "Гамма-мебель, ООО"], item: "Шкаф картотечный", warehouse: "Основной склад", organization: "ООО Ромашка" } ], { asOfDate: "2020-03-31", useRubCurrency: true } ); expect(reply.text.split("\n")[0]).toContain("поставщиком"); expect(reply.text).toContain("Подтверждение"); expect(reply.text).not.toContain("Блок 1"); expect(reply.text).toContain("Гамма-мебель, ООО"); expect(reply.semantics?.balance_confirmed).toBe(true); }); it("renders inventory sale trace from credit-side 41.01 movements", () => { const reply = composeFactualReply( "inventory_sale_trace_for_item", [ { period: "2020-04-10T12:00:00Z", registrator: "Реализация товаров и услуг 0007", account_dt: "90.02", account_kt: "41.01", amount: 210000, analytics: ["Шкаф картотечный", "Основной склад", "ООО Ромашка", "Департамент капитального ремонта города Москвы"], item: "Шкаф картотечный", warehouse: "Основной склад", organization: "ООО Ромашка" } ], { asOfDate: "2020-04-30", useRubCurrency: true } ); expect(reply.text.split("\n")[0]).toContain("покупатель"); expect(reply.text).toContain("Документы выбытия"); expect(reply.text).not.toContain("Блок 1"); expect(reply.text).toContain("Реализация товаров и услуг 0007"); expect(reply.text).toContain("Департамент капитального ремонта города Москвы"); }); it("renders purchase-to-sale chain from both sides of 41.01", () => { const reply = composeFactualReply( "inventory_purchase_to_sale_chain", [ { period: "2020-03-15T12:00:00Z", registrator: "Поступление товаров и услуг 0001", account_dt: "41.01", account_kt: "60.01", amount: 150000, analytics: ["Шкаф картотечный", "Основной склад", "ООО Ромашка", "Гамма-мебель, ООО"], item: "Шкаф картотечный", warehouse: "Основной склад", organization: "ООО Ромашка" }, { period: "2020-04-10T12:00:00Z", registrator: "Реализация товаров и услуг 0007", account_dt: "90.02", account_kt: "41.01", amount: 210000, analytics: ["Шкаф картотечный", "Основной склад", "ООО Ромашка", "Департамент капитального ремонта города Москвы"], item: "Шкаф картотечный", warehouse: "Основной склад", organization: "ООО Ромашка" } ], { asOfDate: "2020-04-30", useRubCurrency: true } ); expect(reply.text.split("\n")[0]).toContain("цепочка поставки и продажи"); expect(reply.text).toContain("Поступление товаров и услуг 0001"); expect(reply.text).toContain("Реализация товаров и услуг 0007"); expect(reply.semantics?.result_mode).toBe("confirmed_balance"); }); }); describe("address compose stage utf8 headers", () => { it("renders readable russian header for contract document list", () => { const reply = composeFactualReply("list_documents_by_contract", [ { period: "2020-10-15T13:34:53Z", registrator: "Списание с расчетного счета 00000000246", account_dt: "66.02", account_kt: "51", amount: 30819.47, analytics: [] } ]); expect(reply.text).toContain("Собран список документов по договору (live address lane)."); }); it("renders readable russian header for contract bank operations", () => { const reply = composeFactualReply("bank_operations_by_contract", [ { period: "2020-10-15T13:34:53Z", registrator: "Списание с расчетного счета 00000000246", account_dt: "66.02", account_kt: "51", amount: 30819.47, analytics: [] } ]); expect(reply.text).toContain("Собран список банковских операций по договору (live address lane)."); }); it("renders readable russian header for contracts-by-counterparty list", () => { const reply = composeFactualReply("list_contracts_by_counterparty", [ { period: "2000-01-01T00:00:00Z", registrator: "Договор №19/15", account_dt: null, account_kt: null, amount: 0, analytics: ["Жуковка 51"] } ]); expect(reply.text).toContain("Собран список договоров по контрагенту (catalog address lane)."); expect(reply.text).toContain("Уникальных договоров: 1."); expect(reply.text).toContain("Договор №19/15"); }); it("renders explicit heuristic contract-candidates reply for open-contracts intent", () => { const reply = composeFactualReply( "list_open_contracts", [ { period: "2020-03-31T23:59:59Z", registrator: "Поступление товаров и услуг 00000000022", account_dt: "60.01", account_kt: "51", amount: 150000, analytics: ["ООО Ромашка", "Договор №19/15"] } ], { periodFrom: "2020-03-01", periodTo: "2020-03-31", asOfDate: "2020-03-31", useRubCurrency: true } ); expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.text).toContain("Итого по предварительному срезу открытых договоров"); expect(reply.text).toContain("Блок 1. Статус результата"); expect(reply.text).toContain("Результат: предварительный список договоров с возможными незакрытыми расчетами."); expect(reply.text).toContain("Блок 4. Основной список (коммерческие договоры)"); expect(reply.semantics?.result_mode).toBe("heuristic_candidates"); expect(reply.semantics?.balance_confirmed).toBe(false); }); it("renders confirmed open-contracts snapshot for exact contract-settlements intent", () => { const reply = composeFactualReply( "open_contracts_confirmed_as_of_date", [ { period: "2020-03-31T23:59:59Z", registrator: "Остатки на дату", account_dt: "", account_kt: "60.01", amount: 150000, analytics: ["ООО Ромашка", "Договор №19/15"] } ], { periodFrom: "2020-03-01", periodTo: "2020-03-31", asOfDate: "2020-03-31", useRubCurrency: true } ); expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.text).toContain("Собран подтвержденный срез открытых договоров"); expect(reply.text).toContain("Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату."); expect(reply.text).toContain("Управленческий вид: по каждому договору показаны чистый остаток и состав по типам открытых расчетов."); expect(reply.text).toContain("Базовая единица детализации: одна строка = один договор, один контрагент и один тип открытого остатка."); expect(reply.text).toContain("Блок 4. Чистый открытый остаток по договорам"); expect(reply.text).toContain("Блок 6. Коммерческие кредиторские компоненты"); expect(reply.semantics?.result_mode).toBe("confirmed_balance"); expect(reply.semantics?.balance_confirmed).toBe(true); }); it("splits confirmed open-contracts output by balance type and hides technical account placeholders", () => { const reply = composeFactualReply( "open_contracts_confirmed_as_of_date", [ { period: "2020-03-31T23:59:59Z", registrator: "Остатки на дату", account_dt: "62.01", account_kt: "0", amount: 100000, analytics: ["ООО Ромашка", "Договор №19/15"] }, { period: "2020-03-31T23:59:59Z", registrator: "Остатки на дату", account_dt: "", account_kt: "60.01", amount: 50000, analytics: ["ООО Ромашка", "Договор №19/15"] }, { period: "2020-03-31T23:59:59Z", registrator: "Остатки на дату", account_dt: "76.09", account_kt: "", amount: 25000, analytics: ["Комитет госуслуг", "ООО /Альтернатива Плюс/"] } ], { periodFrom: "2020-03-01", periodTo: "2020-03-31", asOfDate: "2020-03-31", useRubCurrency: true } ); expect(reply.text).toContain("Блок 4. Чистый открытый остаток по договорам"); expect(reply.text).toContain("Блок 5. Коммерческие дебиторские компоненты"); expect(reply.text).toContain("Блок 6. Коммерческие кредиторские компоненты"); expect(reply.text).toContain("Блок 10. Спорные/некачественно нормализованные позиции"); expect(reply.text).toContain("брутто компонентов"); expect(reply.text).not.toContain("счета: 62.01; 0"); expect(reply.text).toContain("договор не похож на устойчивый договорный реквизит"); }); it("renders period coverage summary for management profile intent", () => { const reply = composeFactualReply("period_coverage_profile", [ { period: "2014-05-27T12:00:00Z", registrator: "MIN_DATE", account_dt: null, account_kt: null, amount: 0, analytics: [] }, { period: "2030-08-03T12:00:00Z", registrator: "MAX_DATE", account_dt: null, account_kt: null, amount: 0, analytics: [] }, { period: "2019-01-01T00:00:00Z", registrator: "YEAR_DOCS", account_dt: null, account_kt: null, amount: 1004, analytics: [] }, { period: "2015-02-01T00:00:00Z", registrator: "MONTH_OPS", account_dt: null, account_kt: null, amount: 1249, analytics: [] } ]); expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.text).toContain("Профиль периодов базы собран"); expect(reply.text).toContain("Самый активный год по документам: 2019 (1004)."); expect(reply.text).toContain("Самый активный месяц по операциям: 2015-02 (1249)."); }); it("renders document type + account section profile summary", () => { const reply = composeFactualReply("document_type_and_account_section_profile", [ { period: "2000-01-01T00:00:00Z", registrator: "DOC_TYPE_DOCS", account_dt: "Списание с расчетного счета", account_kt: "", amount: 2352, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "DOC_TYPE_DOCS", account_dt: "Поступление товаров и услуг", account_kt: "", amount: 486, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "SECTION_DT_OPS", account_dt: "90", account_kt: "DT", amount: 1800, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "SECTION_KT_OPS", account_dt: "90", account_kt: "KT", amount: 1173, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "SECTION_DT_OPS", account_dt: "58", account_kt: "DT", amount: 1, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "SECTION_KT_OPS", account_dt: "58", account_kt: "KT", amount: 1, analytics: [] } ]); expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.text).toContain("Профиль типов документов и разделов учета собран"); expect(reply.text).toContain("Списание с расчетного счета: 2352"); expect(reply.text).toContain("90 (Продажи): 2973"); expect(reply.text).toContain("58 (Финансовые вложения): 2"); }); it("returns focused answer for active year question (without month block)", () => { const reply = composeFactualReply( "period_coverage_profile", [ { period: "2014-05-27T12:00:00Z", registrator: "MIN_DATE", account_dt: null, account_kt: null, amount: 0, analytics: [] }, { period: "2026-03-31T00:00:00Z", registrator: "MAX_DATE", account_dt: null, account_kt: null, amount: 0, analytics: [] }, { period: "2019-01-01T00:00:00Z", registrator: "YEAR_DOCS", account_dt: null, account_kt: null, amount: 1004, analytics: [] }, { period: "2026-01-01T00:00:00Z", registrator: "YEAR_DOCS", account_dt: null, account_kt: null, amount: 3, analytics: [] }, { period: "2015-02-01T00:00:00Z", registrator: "MONTH_OPS", account_dt: null, account_kt: null, amount: 1249, analytics: [] } ], { userMessage: "Какой год самый активный по количеству документов?" } ); expect(reply.text).toContain("Самый активный год по документам: 2019 (1004)."); expect(reply.text).not.toContain("Самый активный месяц по операциям"); expect(reply.text).not.toContain("Покрытие по датам"); }); it("returns focused answer for active month question (without year block)", () => { const reply = composeFactualReply( "period_coverage_profile", [ { period: "2019-01-01T00:00:00Z", registrator: "YEAR_DOCS", account_dt: null, account_kt: null, amount: 1004, analytics: [] }, { period: "2015-02-01T00:00:00Z", registrator: "MONTH_OPS", account_dt: null, account_kt: null, amount: 1249, analytics: [] } ], { userMessage: "Какой месяц самый активный по количеству операций?" } ); expect(reply.text).toContain("Самый активный месяц по операциям: 2015-02 (1249)."); expect(reply.text).not.toContain("Самый активный год по документам"); }); it("returns focused answer for passive year question (and ignores low-activity tail year)", () => { const reply = composeFactualReply( "period_coverage_profile", [ { period: "2014-05-27T12:00:00Z", registrator: "MIN_DATE", account_dt: null, account_kt: null, amount: 0, analytics: [] }, { period: "2026-03-31T00:00:00Z", registrator: "MAX_DATE", account_dt: null, account_kt: null, amount: 0, analytics: [] }, { period: "2019-01-01T00:00:00Z", registrator: "YEAR_DOCS", account_dt: null, account_kt: null, amount: 1004, analytics: [] }, { period: "2020-01-01T00:00:00Z", registrator: "YEAR_DOCS", account_dt: null, account_kt: null, amount: 112, analytics: [] }, { period: "2026-01-01T00:00:00Z", registrator: "YEAR_DOCS", account_dt: null, account_kt: null, amount: 3, analytics: [] } ], { userMessage: "Какой год самый пассивный по количеству документов?" } ); expect(reply.text).toContain("Самый пассивный год по документам: 2020 (112)."); expect(reply.text).not.toContain("Самый активный год по документам"); expect(reply.text).not.toContain("Покрытие по датам"); }); it("returns focused answer for passive month question (without year block)", () => { const reply = composeFactualReply( "period_coverage_profile", [ { period: "2019-01-01T00:00:00Z", registrator: "YEAR_DOCS", account_dt: null, account_kt: null, amount: 1004, analytics: [] }, { period: "2020-01-01T00:00:00Z", registrator: "YEAR_DOCS", account_dt: null, account_kt: null, amount: 112, analytics: [] }, { period: "2026-01-01T00:00:00Z", registrator: "YEAR_DOCS", account_dt: null, account_kt: null, amount: 3, analytics: [] }, { period: "2019-10-01T00:00:00Z", registrator: "MONTH_OPS", account_dt: null, account_kt: null, amount: 1400, analytics: [] }, { period: "2020-05-01T00:00:00Z", registrator: "MONTH_OPS", account_dt: null, account_kt: null, amount: 44, analytics: [] }, { period: "2026-01-01T00:00:00Z", registrator: "MONTH_OPS", account_dt: null, account_kt: null, amount: 2, analytics: [] } ], { userMessage: "Какой месяц самый пассивный по количеству операций?" } ); expect(reply.text).toContain("Самый пассивный месяц по операциям: 2020-05 (44)."); expect(reply.text).not.toContain("Самый активный месяц по операциям"); expect(reply.text).not.toContain("Самый активный год по документам"); }); it("shows operational range and low-activity tail for coverage question", () => { const reply = composeFactualReply( "period_coverage_profile", [ { period: "2014-05-27T12:00:00Z", registrator: "MIN_DATE", account_dt: null, account_kt: null, amount: 0, analytics: [] }, { period: "2026-03-31T00:00:00Z", registrator: "MAX_DATE", account_dt: null, account_kt: null, amount: 0, analytics: [] }, { period: "2019-01-01T00:00:00Z", registrator: "YEAR_DOCS", account_dt: null, account_kt: null, amount: 1004, analytics: [] }, { period: "2026-01-01T00:00:00Z", registrator: "YEAR_DOCS", account_dt: null, account_kt: null, amount: 3, analytics: [] } ], { userMessage: "За какие годы в базе есть данные?" } ); expect(reply.text).toContain("Операционный период с выраженной активностью: 2019..2019."); expect(reply.text).toContain("Низкоактивный хвост (единичные записи): 2026."); }); it("returns focused document-type answer without account sections", () => { const reply = composeFactualReply( "document_type_and_account_section_profile", [ { period: "2000-01-01T00:00:00Z", registrator: "DOC_TYPE_DOCS", account_dt: "Списание с расчетного счета", account_kt: "", amount: 2352, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "SECTION_DT_OPS", account_dt: "90", account_kt: "DT", amount: 1800, analytics: [] } ], { userMessage: "Какие типы документов используются чаще всего в базе?" } ); expect(reply.text).toContain("Топ типов документов"); expect(reply.text).not.toContain("Наиболее заполненные разделы учета"); }); it("returns focused account-sections answer without document types", () => { const reply = composeFactualReply( "document_type_and_account_section_profile", [ { period: "2000-01-01T00:00:00Z", registrator: "DOC_TYPE_DOCS", account_dt: "Списание с расчетного счета", account_kt: "", amount: 2352, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "SECTION_DT_OPS", account_dt: "90", account_kt: "DT", amount: 1800, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "SECTION_KT_OPS", account_dt: "58", account_kt: "KT", amount: 2, analytics: [] } ], { userMessage: "Какие разделы учета наиболее заполнены и какие почти не используются?" } ); expect(reply.text).toContain("Наиболее заполненные разделы учета"); expect(reply.text).not.toContain("Топ типов документов"); }); it("returns focused answer for rare document types question", () => { const reply = composeFactualReply( "document_type_and_account_section_profile", [ { period: "2000-01-01T00:00:00Z", registrator: "DOC_TYPE_DOCS", account_dt: "Списание с расчетного счета", account_kt: "", amount: 2352, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "DOC_TYPE_DOCS", account_dt: "Поступление на расчетный счет", account_kt: "", amount: 124, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "SECTION_DT_OPS", account_dt: "90", account_kt: "DT", amount: 1800, analytics: [] } ], { userMessage: "Какие типы документов используются реже всего в базе?" } ); expect(reply.text).toContain("Наименее используемые типы документов"); expect(reply.text).toContain("Поступление на расчетный счет: 124"); expect(reply.text).not.toContain("Топ типов документов"); expect(reply.text).not.toContain("Наиболее заполненные разделы учета"); }); it("returns focused answer for least-filled account sections question", () => { const reply = composeFactualReply( "document_type_and_account_section_profile", [ { period: "2000-01-01T00:00:00Z", registrator: "SECTION_DT_OPS", account_dt: "90", account_kt: "DT", amount: 1800, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "SECTION_KT_OPS", account_dt: "90", account_kt: "KT", amount: 1173, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "SECTION_DT_OPS", account_dt: "58", account_kt: "DT", amount: 1, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "SECTION_KT_OPS", account_dt: "58", account_kt: "KT", amount: 1, analytics: [] } ], { userMessage: "Какие разделы учета наименее заполнены?" } ); expect(reply.text).toContain("Наименее заполненные разделы учета"); expect(reply.text).toContain("58 (Финансовые вложения): 2"); expect(reply.text).not.toContain("Наиболее заполненные разделы учета"); expect(reply.text).not.toContain("Топ типов документов"); }); it("returns focused answer for total counterparties question", () => { const reply = composeFactualReply( "counterparty_population_and_roles", [ { period: "2000-01-01T00:00:00Z", registrator: "CP_TOTAL", account_dt: "", account_kt: "", amount: 412, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_CUSTOMER_ACTIVE", account_dt: "", account_kt: "", amount: 145, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_SUPPLIER_ACTIVE", account_dt: "", account_kt: "", amount: 94, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_MIXED_ACTIVE", account_dt: "", account_kt: "", amount: 23, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_ACTIVE_UNION", account_dt: "", account_kt: "", amount: 216, analytics: [] } ], { userMessage: "Сколько всего уникальных контрагентов в базе?" } ); expect(reply.text).toContain("Всего уникальных контрагентов в базе: 412."); expect(reply.text).not.toContain("Роли контрагентов по активности"); }); it("returns focused answer for counterparty roles split question", () => { const reply = composeFactualReply( "counterparty_population_and_roles", [ { period: "2000-01-01T00:00:00Z", registrator: "CP_TOTAL", account_dt: "", account_kt: "", amount: 412, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_CUSTOMER_ACTIVE", account_dt: "", account_kt: "", amount: 145, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_SUPPLIER_ACTIVE", account_dt: "", account_kt: "", amount: 94, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_MIXED_ACTIVE", account_dt: "", account_kt: "", amount: 23, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_ACTIVE_UNION", account_dt: "", account_kt: "", amount: 216, analytics: [] } ], { userMessage: "Сколько у нас заказчиков, поставщиков и смешанных контрагентов?" } ); expect(reply.text).toContain("Роли контрагентов по активности:"); expect(reply.text).toContain("Заказчики (только customer-роль): 122."); expect(reply.text).toContain("Поставщики (только supplier-роль): 71."); expect(reply.text).toContain("Смешанные (и покупатель, и поставщик): 23."); expect(reply.text).not.toContain("Всего уникальных контрагентов в базе"); }); it("returns focused answer for slang supplier count question", () => { const reply = composeFactualReply( "counterparty_population_and_roles", [ { period: "2000-01-01T00:00:00Z", registrator: "CP_TOTAL", account_dt: "", account_kt: "", amount: 412, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_CUSTOMER_ACTIVE", account_dt: "", account_kt: "", amount: 145, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_SUPPLIER_ACTIVE", account_dt: "", account_kt: "", amount: 94, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_MIXED_ACTIVE", account_dt: "", account_kt: "", amount: 23, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_ACTIVE_UNION", account_dt: "", account_kt: "", amount: 216, analytics: [] } ], { userMessage: "скока поставщиков в базе" } ); expect(reply.text).toContain("Поставщиков (только supplier-роль): 71."); expect(reply.text).not.toContain("Роли контрагентов по активности:"); expect(reply.text).not.toContain("Всего уникальных контрагентов в базе"); }); it("returns focused answer for slang client count question", () => { const reply = composeFactualReply( "counterparty_population_and_roles", [ { period: "2000-01-01T00:00:00Z", registrator: "CP_TOTAL", account_dt: "", account_kt: "", amount: 412, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_CUSTOMER_ACTIVE", account_dt: "", account_kt: "", amount: 145, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_SUPPLIER_ACTIVE", account_dt: "", account_kt: "", amount: 94, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_MIXED_ACTIVE", account_dt: "", account_kt: "", amount: 23, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CP_ACTIVE_UNION", account_dt: "", account_kt: "", amount: 216, analytics: [] } ], { userMessage: "скок клиентов" } ); expect(reply.text).toContain("Заказчиков (только customer-роль): 122."); expect(reply.text).not.toContain("Роли контрагентов по активности:"); expect(reply.text).not.toContain("Всего уникальных контрагентов в базе"); }); it("returns customer activity lifecycle list for year question", () => { const reply = composeFactualReply( "counterparty_activity_lifecycle", [ { period: "2020-12-16T16:20:52Z", registrator: "CP_CUSTOMER_ACTIVITY", account_dt: "", account_kt: "", amount: 15, analytics: ["НОРТОН"] }, { period: "2020-11-19T12:00:04Z", registrator: "CP_CUSTOMER_ACTIVITY", account_dt: "", account_kt: "", amount: 11, analytics: ["Группа"] } ], { userMessage: "Какие заказчики работали с нами в 2020 году?" } ); expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.text).toContain("Собран профиль активности заказчиков"); expect(reply.text).toContain("Активные заказчики в 2020 году: 2."); expect(reply.text).toContain("НОРТОН"); expect(reply.text).toContain("Группа"); }); it("returns explicit 2020 year label for short-year lifecycle question", () => { const reply = composeFactualReply( "counterparty_activity_lifecycle", [ { period: "2020-12-16T16:20:52Z", registrator: "CP_CUSTOMER_ACTIVITY", account_dt: "", account_kt: "", amount: 15, analytics: ["НОРТОН"] } ], { userMessage: "покажи список заказчиков за 20год" } ); expect(reply.text).toContain("Активные заказчики в 2020 году: 1."); }); it("returns top-10 lifecycle ranking by years for longest-collaboration customer question", () => { const reply = composeFactualReply( "counterparty_activity_lifecycle", [ { period: "2019-01-01T00:00:00Z", registrator: "CP_CUSTOMER_ACTIVITY_YEAR", account_dt: "", account_kt: "", amount: 5, analytics: ["НОРТОН"] }, { period: "2020-01-01T00:00:00Z", registrator: "CP_CUSTOMER_ACTIVITY_YEAR", account_dt: "", account_kt: "", amount: 7, analytics: ["НОРТОН"] }, { period: "2021-01-01T00:00:00Z", registrator: "CP_CUSTOMER_ACTIVITY_YEAR", account_dt: "", account_kt: "", amount: 4, analytics: ["Группа"] }, { period: "2022-05-12T12:00:00Z", registrator: "CP_CUSTOMER_ACTIVITY", account_dt: "", account_kt: "", amount: 12, analytics: ["НОРТОН"] } ], { userMessage: "Какие заказчики работают с нами дольше всего?" } ); expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.text).toContain("Заказчиков с самым длинным горизонтом сотрудничества (по годам)"); expect(reply.text).toContain("Топ-"); expect(reply.text).toContain("лет в базе"); expect(reply.text).toContain("НОРТОН"); }); it("renders debt-aging ranking by as-of date for receivables debt-longevity question", () => { const reply = composeFactualReply( "list_receivables_counterparties", [ { period: "2022-01-01T00:00:00Z", registrator: "Реализация 1", account_dt: "62.01", account_kt: "90.01", amount: 1000, analytics: ["Контрагент А", "Договор №A-01 от 10.02.2020"] }, { period: "2024-01-01T00:00:00Z", registrator: "Реализация 2", account_dt: "62.01", account_kt: "90.01", amount: 1500, analytics: ["Контрагент Б", "Договор №B-01 от 15.03.2023"] } ], { userMessage: "Сколько заказчиков у нас на этот момент могут считаться долгожителями по своим задолженностям?", asOfDate: "2026-04-11" } ); expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.text).toContain("Проверил должников по сроку жизни задолженности"); expect(reply.text).toContain("Дата среза: 11.04.2026."); expect(reply.text).toContain("Приоритет ручной проверки (по возрасту долга, по убыванию):"); expect(reply.text).toContain("1. Контрагент А | договоры:"); expect(reply.text).toContain("2. Контрагент Б | договоры:"); }); it("returns contract usage overview summary", () => { const reply = composeFactualReply("contract_usage_overview", [ { period: "2000-01-01T00:00:00Z", registrator: "CT_TOTAL", account_dt: "", account_kt: "", amount: 520, analytics: [] }, { period: "2000-01-01T00:00:00Z", registrator: "CT_USED", account_dt: "", account_kt: "", amount: 148, analytics: [] } ]); expect(reply.text).toContain("Профиль договорной базы собран"); expect(reply.text).toContain("Всего договоров в базе: 520."); expect(reply.text).toContain("Использованных договоров (есть factual связь с операциями): 148."); expect(reply.text).toContain("Неиспользуемых договоров: 372."); }); it("renders customer value top list with explicit top-2 limit", () => { const reply = composeFactualReply( "customer_revenue_and_payments", [ { period: "2020-03-01T00:00:00Z", registrator: "Поступление 1", account_dt: "", account_kt: "", amount: 500, analytics: ["Клиент А", "Договор А-1"] }, { period: "2020-03-02T00:00:00Z", registrator: "Поступление 2", account_dt: "", account_kt: "", amount: 700, analytics: ["Клиент Б", "Договор Б-1"] }, { period: "2020-03-03T00:00:00Z", registrator: "Поступление 3", account_dt: "", account_kt: "", amount: 300, analytics: ["Клиент А", "Договор А-1"] } ], { userMessage: "покажи топ-2 заказчиков по сумме поступлений" } ); expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.text).toContain("Топ-2 заказчиков по сумме поступлений:"); expect(reply.text).toContain("1. Клиент А | сумма: 800"); expect(reply.text).toContain("2. Клиент Б | сумма: 700"); }); it("renders top incoming deals for highest inflow wording", () => { const reply = composeFactualReply( "customer_revenue_and_payments", [ { period: "2020-03-01T00:00:00Z", registrator: "Поступление 1", account_dt: "", account_kt: "", amount: 500, analytics: ["Клиент А", "Договор А-1"] }, { period: "2020-03-02T00:00:00Z", registrator: "Поступление 2", account_dt: "", account_kt: "", amount: 700, analytics: ["Клиент Б", "Договор Б-1"] } ], { userMessage: "какие приходы самые высокие за все время" } ); expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.text).toContain("самых крупных разовых сделок по поступлениям"); expect(reply.text).toContain("Поступление 2"); }); it("renders max-single ranking for highest-check typo wording", () => { const reply = composeFactualReply( "customer_revenue_and_payments", [ { period: "2020-03-01T00:00:00Z", registrator: "Поступление 1", account_dt: "", account_kt: "", amount: 500, analytics: ["Клиент А", "Договор А-1"] }, { period: "2020-03-02T00:00:00Z", registrator: "Поступление 2", account_dt: "", account_kt: "", amount: 1200, analytics: ["Клиент Б", "Договор Б-1"] }, { period: "2020-03-03T00:00:00Z", registrator: "Поступление 3", account_dt: "", account_kt: "", amount: 300, analytics: ["Клиент А", "Договор А-1"] } ], { userMessage: "с каких кликентов самый высокий чек" } ); expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.text).toContain("по максимальной сумме одной входящей операции"); expect(reply.text).toContain("1. Клиент Б | max single: 1200"); }); it("renders supplier payout list by operations count", () => { const reply = composeFactualReply( "supplier_payouts_profile", [ { period: "2020-03-01T00:00:00Z", registrator: "Списание 1", account_dt: "", account_kt: "", amount: 100, analytics: ["Поставщик А", "Договор А-1"] }, { period: "2020-03-02T00:00:00Z", registrator: "Списание 2", account_dt: "", account_kt: "", amount: 120, analytics: ["Поставщик А", "Договор А-2"] }, { period: "2020-03-03T00:00:00Z", registrator: "Списание 3", account_dt: "", account_kt: "", amount: 500, analytics: ["Поставщик Б", "Договор Б-1"] } ], { userMessage: "топ-20 поставщиков по количеству исходящих платежных операций" } ); expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.text).toContain("Топ-2 поставщиков по количеству исходящих платежных операций:"); expect(reply.text).toContain("1. Поставщик А | операций: 2"); }); it("renders contract value list for minimal active budgets", () => { const reply = composeFactualReply( "contract_usage_and_value", [ { period: "2020-03-01T00:00:00Z", registrator: "CT_VALUE_IN", account_dt: "", account_kt: "", amount: 900, analytics: ["Клиент А", "Договор 01/20"] }, { period: "2020-03-02T00:00:00Z", registrator: "CT_VALUE_OUT", account_dt: "", account_kt: "", amount: 100, analytics: ["Поставщик Б", "Договор 02/20"] }, { period: "2020-03-03T00:00:00Z", registrator: "CT_VALUE_IN", account_dt: "", account_kt: "", amount: 150, analytics: ["Клиент В", "Договор 03/20"] } ], { userMessage: "покажи топ-20 договоров с минимальным бюджетом среди активных договоров" } ); expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.text).toContain("активных договоров с минимальным бюджетом"); expect(reply.text).toContain("1. Договор 02/20 | оборот: 100"); }); it("adds deterministic why-zero explanation for VAT forecast follow-up wording", () => { const reply = composeFactualReply( "vat_payable_forecast", [ { period: "2020-03-01T00:00:00Z", registrator: "VAT_68_CREDIT", account_dt: "68", account_kt: "", amount: 9126, analytics: [] }, { period: "2020-03-01T00:00:00Z", registrator: "VAT_68_DEBIT", account_dt: "68", account_kt: "", amount: 115342, analytics: [] }, { period: "2020-03-01T00:00:00Z", registrator: "VAT_19_DEBIT", account_dt: "19", account_kt: "", amount: 1602384, analytics: [] }, { period: "2020-03-01T00:00:00Z", registrator: "VAT_19_CREDIT", account_dt: "19", account_kt: "", amount: 0, analytics: [] } ], { userMessage: "почему прогноз к уплате 0?" } ); expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.text).toContain("Почему прогноз к уплате 0"); expect(reply.text).toContain("max(0, 68 Кт - 68 Дт)"); expect(reply.text).toContain("Собран прогноз НДС к уплате: 0.00."); expect(reply.text).toContain("За период 68 Кт = 9126.00, 68 Дт = 115342.00, разница = -106216.00."); expect(reply.text).toContain("Разница неположительная"); expect(reply.text).toContain("оперативный прогноз по оборотам НДС-субсчетов 68.02*/19*"); }); it("adds VAT declaration and payment deadlines for as-of-date forecast window", () => { const reply = composeFactualReply( "vat_payable_forecast", [ { period: "2020-03-01T00:00:00Z", registrator: "VAT_68_CREDIT", account_dt: "68", account_kt: "", amount: 300, analytics: [] }, { period: "2020-03-01T00:00:00Z", registrator: "VAT_68_DEBIT", account_dt: "68", account_kt: "", amount: 0, analytics: [] } ], { userMessage: "какие сроки уплаты и сдачи декларации по НДС по состоянию на 15 марта 2020 года", periodFrom: "2020-01-01", periodTo: "2020-03-15" } ); expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.text).toContain("Период расчета (срез обязательств): 01.01.2020..15.03.2020."); expect(reply.text).toContain("Налоговый период: 1 кв. 2020."); expect(reply.text).toContain("Срок сдачи декларации: до 27.04.2020."); expect(reply.text).toContain("Сроки уплаты: 28.04.2020, 28.05.2020, 29.06.2020."); expect(reply.text).toContain("Ориентир по долям к уплате: 100.00 / 100.00 / 100.00."); }); it("builds VAT deadlines correctly for Q4 with next-year rollover", () => { const reply = composeFactualReply( "vat_payable_forecast", [ { period: "2020-12-31T00:00:00Z", registrator: "VAT_68_CREDIT", account_dt: "68", account_kt: "", amount: 90, analytics: [] } ], { userMessage: "когда платить НДС за 4 квартал 2020", periodFrom: "2020-10-01", periodTo: "2020-12-31" } ); expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.text).toContain("Налоговый период: 4 кв. 2020."); expect(reply.text).toContain("Срок сдачи декларации: до 25.01.2021."); expect(reply.text).toContain("Сроки уплаты: 28.01.2021, 01.03.2021, 29.03.2021."); expect(reply.text).toContain("Ориентир по долям к уплате: 30.00 / 30.00 / 30.00."); }); it("explains zero VAT as no-movements case when VAT turnovers are absent in window", () => { const reply = composeFactualReply( "vat_payable_forecast", [ { period: "2019-04-01T00:00:00Z", registrator: "VAT_68_CREDIT", account_dt: "68", account_kt: "", amount: 0, analytics: [] }, { period: "2019-04-01T00:00:00Z", registrator: "VAT_68_DEBIT", account_dt: "68", account_kt: "", amount: 0, analytics: [] }, { period: "2019-04-01T00:00:00Z", registrator: "VAT_19_DEBIT", account_dt: "19", account_kt: "", amount: 0, analytics: [] }, { period: "2019-04-01T00:00:00Z", registrator: "VAT_19_CREDIT", account_dt: "19", account_kt: "", amount: 0, analytics: [] } ], { userMessage: "какой прогноз оплаты ндс на 12-05-2019", periodFrom: "2019-04-01", periodTo: "2019-05-12" } ); expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.text).toContain("Собран прогноз НДС к уплате: 0.00."); expect(reply.text).toContain("не найдено движений по НДС-субсчетам 68.02*/19*"); expect(reply.text).toContain("Чеклист проверки в 1С (почему к уплате 0):"); expect(reply.text).toContain("Проверьте наличие движений в РегистрБухгалтерии.Хозрасчетный"); }); it("explains zero VAT as offset case when VAT turnovers exist but net is near zero", () => { const reply = composeFactualReply( "vat_payable_forecast", [ { period: "2020-03-01T00:00:00Z", registrator: "VAT_68_CREDIT", account_dt: "68", account_kt: "", amount: 1000, analytics: [] }, { period: "2020-03-01T00:00:00Z", registrator: "VAT_68_DEBIT", account_dt: "68", account_kt: "", amount: 1000, analytics: [] } ], { userMessage: "какой прогноз оплаты ндс на 12-05-2020", periodFrom: "2020-04-01", periodTo: "2020-05-12" } ); expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.text).toContain("Собран прогноз НДС к уплате: 0.00."); expect(reply.text).toContain("обороты по 68* взаимно перекрылись"); expect(reply.text).toContain("Чеклист проверки в 1С (почему к уплате 0):"); }); it("adds MCP VAT source coverage block for VAT forecast response", () => { const reply = composeFactualReply( "vat_payable_forecast", [ { period: "2019-12-31T23:59:59Z", registrator: "VAT_68_CREDIT", account_dt: "68", account_kt: "", amount: 1000, analytics: [] } ], { userMessage: "прикинь ндс за декабрь 2019", periodFrom: "2019-12-01", periodTo: "2019-12-31", vatDirectSourceProbe: { status: "ok", objectsTotal: 5, documentsTotal: 2, registersTotal: 3, probedSources: [ { fullName: "РегистрНакопления.НДСПродажи", objectType: "register", status: "ok", rowsFetched: 1, lastPeriod: "2019-12-31T23:59:59Z" }, { fullName: "РегистрНакопления.НДСПокупки", objectType: "register", status: "empty", rowsFetched: 0 } ], errors: [] } } ); expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.text).toContain("Покрытие VAT-источников через MCP"); expect(reply.text).toContain("Найдено VAT-объектов: 5"); expect(reply.text).toContain("РегистрНакопления.НДСПродажи"); }); it("builds confirmed VAT tax-period reply from sales and purchase book markers", () => { const reply = composeFactualReply( "vat_liability_confirmed_for_tax_period", [ { period: "2019-12-31T23:59:59Z", registrator: "VAT_BOOK_SALES", account_dt: "68.02", account_kt: "", amount: 120000, analytics: [] }, { period: "2019-12-31T23:59:59Z", registrator: "VAT_BOOK_PURCHASES", account_dt: "19", account_kt: "", amount: 70000, analytics: [] } ], { userMessage: "сколько платить ндс в налоговую за декабрь 2019", periodFrom: "2019-10-01", periodTo: "2019-12-31", useRubCurrency: true } ); expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.text).toContain("Собран подтвержденный расчет НДС к уплате за налоговый период"); expect(reply.text).toContain("50.000,00 ₽"); expect(reply.semantics?.result_mode).toBe("confirmed_balance"); expect(reply.semantics?.balance_confirmed).toBe(true); }); it("formats VAT forecast amounts in rubles and emphasizes numbers when requested", () => { const reply = composeFactualReply( "vat_payable_forecast", [ { period: "2019-12-31T23:59:59Z", registrator: "VAT_68_CREDIT", account_dt: "68", account_kt: "", amount: 1234567.89, analytics: [] } ], { userMessage: "прикинь ндс", useRubCurrency: true, emphasizeNumbers: true } ); expect(reply.text).toContain("**1.234.567,89** ₽"); expect(reply.text).toContain("Собран прогноз НДС к уплате:"); }); it("does not split dates and list numbering when numeric emphasis is enabled", () => { const reply = composeFactualReply( "vat_payable_forecast", [ { period: "2019-12-31T23:59:59Z", registrator: "VAT_68_CREDIT", account_dt: "68", account_kt: "", amount: 0, analytics: [] }, { period: "2019-12-31T23:59:59Z", registrator: "VAT_68_DEBIT", account_dt: "68", account_kt: "", amount: 0, analytics: [] } ], { userMessage: "почему по ндс ноль", periodFrom: "2019-12-01", periodTo: "2019-12-31", emphasizeNumbers: true } ); expect(reply.text).toContain("Период оценки: 01.12.2019..31.12.2019."); expect(reply.text).not.toContain("**01**.**12**.**2019**"); expect(reply.text).toContain("1) Проверьте ОСВ/анализ счета"); expect(reply.text).not.toContain("**1**)"); }); it("keeps VAT probe timestamps intact when numeric emphasis is enabled", () => { const reply = composeFactualReply( "vat_payable_forecast", [ { period: "2019-12-31T23:59:59Z", registrator: "VAT_68_CREDIT", account_dt: "68", account_kt: "", amount: 1000, analytics: [] } ], { userMessage: "прикинь ндс", emphasizeNumbers: true, vatDirectSourceProbe: { status: "ok", objectsTotal: 1, documentsTotal: 0, registersTotal: 1, probedSources: [ { fullName: "РегистрНакопления.НДСПредъявленный", objectType: "register", status: "ok", rowsFetched: 1, lastPeriod: "2019-12-31T23:59:59Z" } ], errors: [] } } ); expect(reply.text).toContain("последнее движение: 2019-12-31T23:59:59Z"); expect(reply.text).not.toContain("2019****-12**-31T23:**59**:59Z"); }); it("adds MCP VAT source probe block for confirmed VAT as-of response", () => { const reply = composeFactualReply( "vat_payable_confirmed_as_of_date", [ { period: "2020-03-31T23:59:59Z", registrator: "Остатки на дату", account_dt: null, account_kt: "68.02", amount: 123456.78, analytics: [] } ], { asOfDate: "2020-03-31", vatDirectSourceProbe: { status: "ok", objectsTotal: 3, documentsTotal: 1, registersTotal: 2, probedSources: [ { fullName: "РегистрНакопления.НДСНачисленный", objectType: "register", status: "ok", rowsFetched: 1, lastPeriod: "2020-03-31T23:59:59Z", sampleRegistrator: "Отражение начисления НДС 0001" }, { fullName: "Документ.РегистрацияОплатыНДСВБюджет", objectType: "document", status: "empty", rowsFetched: 0 } ], errors: [] } } ); expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.text).toContain("Блок 2.1. MCP-проверка VAT-источников"); expect(reply.text).toContain("VAT-объектов в метаданных 1С: 3"); expect(reply.text).toContain("Источников с движениями до даты среза: 1"); expect(reply.text).toContain("РегистрНакопления.НДСНачисленный"); }); it("adds VAT probe error note for confirmed VAT as-of response", () => { const reply = composeFactualReply( "vat_payable_confirmed_as_of_date", [ { period: "2020-03-31T23:59:59Z", registrator: "Остатки на дату", account_dt: null, account_kt: "68.02", amount: 1000, analytics: [] } ], { asOfDate: "2020-03-31", vatDirectSourceProbe: { status: "error", objectsTotal: 0, documentsTotal: 0, registersTotal: 0, probedSources: [], errors: ["metadata timeout"] } } ); expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.text).toContain("Probe VAT-источников завершился ошибкой"); }); }); describe("address intent resolver expansion (M2.3a)", () => { it("resolves documents by counterparty intent", () => { const result = resolveAddressIntent("show documents by counterparty Alfa from 2020-07-01 to 2020-07-31"); expect(result.intent).toBe("list_documents_by_counterparty"); }); it("resolves bank operations by counterparty intent", () => { const result = resolveAddressIntent("show bank operations by counterparty Alfa"); expect(result.intent).toBe("bank_operations_by_counterparty"); }); it("resolves documents forming balance intent", () => { const result = resolveAddressIntent("which documents form balance for account 62 as of 2020-07-31"); expect(result.intent).toBe("documents_forming_balance"); }); it("resolves documents forming balance for russian participle phrasing", () => { const result = resolveAddressIntent("Показать документы, формирующие остаток по счету 60.01 на дату 2020-07-31"); expect(result.intent).toBe("documents_forming_balance"); }); it("resolves documents forming balance for slang phrase with compact account token", () => { const result = resolveAddressIntent("раскрой остаток 60.01 по документам на конец июля 2020"); expect(result.intent).toBe("documents_forming_balance"); }); it("resolves documents forming balance for 'доки под остатком' slang phrase", () => { const result = resolveAddressIntent("доки под остатком 60.01 на 2020-07-31"); expect(result.intent).toBe("documents_forming_balance"); }); it("resolves documents by company phrase as counterparty intent", () => { const result = resolveAddressIntent("Какие документы доступны по компании СВК за 2021 год?"); expect(result.intent).toBe("list_documents_by_counterparty"); }); it("resolves transliterated docy slang as documents by counterparty intent", () => { const result = resolveAddressIntent("svk poka docy za 2020"); expect(result.intent).toBe("list_documents_by_counterparty"); }); it("resolves bank operations by supplier phrase", () => { const result = resolveAddressIntent("Покажи платежи по поставщику Альфа за июль 2020"); expect(result.intent).toBe("bank_operations_by_counterparty"); }); it("resolves documents by contract intent", () => { const result = resolveAddressIntent("Покажи документы по договору 19/15 за 2020"); expect(result.intent).toBe("list_documents_by_contract"); }); it("resolves bank operations by contract intent", () => { const result = resolveAddressIntent("Покажи банковские операции по договору 19/15"); expect(result.intent).toBe("bank_operations_by_contract"); }); it("resolves shorthand bank-by-contract slang intent", () => { const result = resolveAddressIntent("покажи банк опер по дог 19/15 пж"); expect(result.intent).toBe("bank_operations_by_contract"); }); it("resolves debt-by-contract query to open items intent", () => { const result = resolveAddressIntent("Есть ли долг по договору 19/15 на 2020-07-31"); expect(result.intent).toBe("open_items_by_counterparty_or_contract"); }); it("resolves unclosed contracts list query without specific anchor", () => { const result = resolveAddressIntent("Покажи незакрытые договоры на 2020-12-31"); expect(result.intent).toBe("open_contracts_confirmed_as_of_date"); }); it("resolves bank operations by contract for normalized phrase with linked contract wording", () => { const result = resolveAddressIntent( "Показать банковские операции (счета 51, 60, 62) связанные с договором 19/15." ); expect(result.intent).toBe("bank_operations_by_contract"); }); it("keeps bank_operations_by_counterparty even when account hints are present", () => { const result = resolveAddressIntent("Показать банковские операции (счета 51, 62) для контрагента СВК за 2020 год"); expect(result.intent).toBe("bank_operations_by_counterparty"); }); it("resolves documents by client phrase", () => { const result = resolveAddressIntent("Выведи документы по клиенту Бета за 2020-07"); expect(result.intent).toBe("list_documents_by_counterparty"); }); it("resolves short slang docs phrase with loose by-anchor", () => { const result = resolveAddressIntent("какие доки есть по свк за 2021"); expect(result.intent).toBe("list_documents_by_counterparty"); }); it("resolves typo slang docs phrase with implicit anchor", () => { const result = resolveAddressIntent("свк доки за 20год покеж"); expect(result.intent).toBe("list_documents_by_counterparty"); }); it("resolves noisy docs phrase with slang tail", () => { const result = resolveAddressIntent("свк 20 год - покажи доки плс"); expect(result.intent).toBe("list_documents_by_counterparty"); }); it("resolves slang transactions phrase by counterparty", () => { const result = resolveAddressIntent("транзакции по свк за 2020"); expect(result.intent).toBe("bank_operations_by_counterparty"); }); it("resolves short balance slang with compact account token", () => { const result = resolveAddressIntent("скока по 60.02 на конец 2020-12"); expect(result.intent).toBe("account_balance_snapshot"); }); it("resolves colloquial 'что на счете' phrasing as account balance snapshot", () => { const result = resolveAddressIntent("что на счете 60 на 2020.05"); expect(result.intent).toBe("account_balance_snapshot"); }); it("resolves mixed ru/en balance phrasing with account token", () => { const result = resolveAddressIntent("баланс account 60.01 as of 2020-07-31"); expect(result.intent).toBe("account_balance_snapshot"); }); it("resolves 'по докам' slang as documents forming balance", () => { const result = resolveAddressIntent("раскидай остаток 62.01 по докам на 2020-12-31"); expect(result.intent).toBe("documents_forming_balance"); }); it("resolves english compact docs-forming phrasing", () => { const result = resolveAddressIntent("docs forming balance 60.01 as of 2020-07-31"); expect(result.intent).toBe("documents_forming_balance"); }); it("resolves loose by-anchor follow-up as documents by counterparty fallback", () => { const result = resolveAddressIntent("за любой период есть что-то по свк?"); expect(result.intent).toBe("list_documents_by_counterparty"); }); it("resolves period coverage profile for years-in-database question", () => { const result = resolveAddressIntent("За какие годы в базе есть данные?"); expect(result.intent).toBe("period_coverage_profile"); }); it("resolves period coverage profile for top active year by documents question", () => { const result = resolveAddressIntent("Какой год самый активный по количеству документов?"); expect(result.intent).toBe("period_coverage_profile"); }); it("resolves period coverage profile for top active month by operations question", () => { const result = resolveAddressIntent("Какой месяц самый активный по количеству операций?"); expect(result.intent).toBe("period_coverage_profile"); }); it("resolves period coverage profile for passive year by documents question", () => { const result = resolveAddressIntent("Какой год самый пассивный по количеству документов?"); expect(result.intent).toBe("period_coverage_profile"); }); it("resolves period coverage profile for passive month by operations question", () => { const result = resolveAddressIntent("Какой месяц самый пассивный по количеству операций?"); expect(result.intent).toBe("period_coverage_profile"); }); it("resolves period coverage profile for noisy active-year-by-docs phrase", () => { const result = resolveAddressIntent("какой год тут самый движовый по докам"); expect(result.intent).toBe("period_coverage_profile"); }); it("resolves period coverage profile for month-peak follow-up phrase", () => { const result = resolveAddressIntent("а теперь месяц-пик по операциям"); expect(result.intent).toBe("period_coverage_profile"); }); it("resolves document+section profile for document type usage question", () => { const result = resolveAddressIntent("Какие типы документов используются чаще всего в базе?"); expect(result.intent).toBe("document_type_and_account_section_profile"); }); it("resolves document+section profile for account section fill question", () => { const result = resolveAddressIntent("Какие разделы учета наиболее заполнены и какие почти не используются?"); expect(result.intent).toBe("document_type_and_account_section_profile"); }); it("resolves document+section profile for rare document types question", () => { const result = resolveAddressIntent("Какие типы документов используются реже всего в базе?"); expect(result.intent).toBe("document_type_and_account_section_profile"); }); it("resolves document+section profile for least-filled account sections question", () => { const result = resolveAddressIntent("Какие разделы учета наименее заполнены?"); expect(result.intent).toBe("document_type_and_account_section_profile"); }); it("resolves document+section profile for noisy docs usage phrase", () => { const result = resolveAddressIntent("каких доков у нас больше всего крутится?"); expect(result.intent).toBe("document_type_and_account_section_profile"); }); it("resolves document+section profile for summary by doc types and share phrase", () => { const result = resolveAddressIntent("Сформируй сводку по типам документов и их доле в общем объеме."); expect(result.intent).toBe("document_type_and_account_section_profile"); }); it("resolves counterparty population intent for total unique counterparties question", () => { const result = resolveAddressIntent("Сколько всего уникальных контрагентов в базе?"); expect(result.intent).toBe("counterparty_population_and_roles"); }); it("resolves counterparty population intent for roles split question", () => { const result = resolveAddressIntent("Сколько у нас заказчиков, поставщиков и смешанных контрагентов?"); expect(result.intent).toBe("counterparty_population_and_roles"); }); it("resolves counterparty population intent for slang supplier count question", () => { const result = resolveAddressIntent("скока поставщиков"); expect(result.intent).toBe("counterparty_population_and_roles"); }); it("resolves counterparty population intent for slang supplier count in base question", () => { const result = resolveAddressIntent("скока поставщиков в базе"); expect(result.intent).toBe("counterparty_population_and_roles"); }); it("resolves counterparty population intent for slang client count question", () => { const result = resolveAddressIntent("скок клиентов"); expect(result.intent).toBe("counterparty_population_and_roles"); }); it("resolves counterparty population intent for slang client count in base question", () => { const result = resolveAddressIntent("скок клиентов в базе"); expect(result.intent).toBe("counterparty_population_and_roles"); }); it("resolves counterparty lifecycle intent for active customers in year question", () => { const result = resolveAddressIntent("Какие заказчики работали с нами в 2020 году?"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("resolves counterparty lifecycle intent for active customers all-time question", () => { const result = resolveAddressIntent("Какие клиенты работали с нами за все время?"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("resolves counterparty lifecycle intent for customer list all-time question", () => { const result = resolveAddressIntent("выведи список заказчиков за все время"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("resolves counterparty lifecycle intent for customer list short-year question", () => { const result = resolveAddressIntent("покажи список заказчиков за 20год"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("resolves counterparty lifecycle intent for roster wording without explicit period", () => { const result = resolveAddressIntent("кто у нас заказчики вообще"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("resolves counterparty lifecycle intent for one-time counterparties wording", () => { const result = resolveAddressIntent("Какие контрагенты работали с нами только один раз за все время?"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("resolves counterparty lifecycle intent for longest-running counterparties wording", () => { const result = resolveAddressIntent("Какие контрагенты работают с нами дольше всех?"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("routes debt-longevity wording into receivables intent", () => { const result = resolveAddressIntent( "Сколько заказчиков у нас на этот момент могут считаться долгожителями по своим задолженностям?" ); expect(result.intent).toBe("list_receivables_counterparties"); expect(result.reasons).toContain("receivables_debt_lifecycle_signal_detected"); }); it("resolves supplier lifecycle segmentation wording into lifecycle intent", () => { const result = resolveAddressIntent("Раздели поставщиков на регулярных и эпизодических по активности."); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("resolves stale suppliers wording into lifecycle intent", () => { const result = resolveAddressIntent("Какие поставщики давно не использовались?"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("keeps supplier lifecycle segmentation with operations wording in lifecycle intent", () => { const result = resolveAddressIntent("Раздели поставщиков на регулярных и эпизодических по частоте операций."); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("keeps stale supplier operations wording in lifecycle intent", () => { const result = resolveAddressIntent("Какие поставщики давно не использовались в операционной активности?"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("keeps slang all-customers-all-time wording in lifecycle intent", () => { const result = resolveAddressIntent("выведи всех заков за все время"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("keeps churn wording with year in lifecycle intent", () => { const result = resolveAddressIntent("кто был активен в 2020 и потом отвалился"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("keeps one-time-plus-churn wording in lifecycle intent", () => { const result = resolveAddressIntent("кто с нами был ровно один раз и пропал"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("keeps oldest-collaboration slang wording in lifecycle intent", () => { const result = resolveAddressIntent("самые старые по сотрудничеству кто"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("keeps regular-vs-one-off supplier slang in lifecycle intent (not population)", () => { const result = resolveAddressIntent("разбей поставщиков на регуляр и разовые"); expect(result.intent).toBe("counterparty_activity_lifecycle"); }); it("resolves contract usage overview intent", () => { const result = resolveAddressIntent("Сколько всего договоров заведено и сколько из них реально использовались?"); expect(result.intent).toBe("contract_usage_overview"); }); it("resolves stale contracts wording into contract usage overview intent", () => { const result = resolveAddressIntent("Какие договоры давно не использовались?"); expect(result.intent).toBe("contract_usage_overview"); }); it("resolves customer revenue/payout ranking intent", () => { const result = resolveAddressIntent("какие клиенты самые доходные, выдай топ-20"); expect(result.intent).toBe("customer_revenue_and_payments"); }); it("resolves colloquial 'кто нам больше денег принес' wording into customer revenue intent", () => { const result = resolveAddressIntent("\u043a\u0442\u043e \u043d\u0430\u043c \u0431\u043e\u043b\u044c\u0448\u0435 \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0435\u0441"); expect(result.intent).toBe("customer_revenue_and_payments"); }); it("resolves typo 'ликвидних заказчиков' wording into customer revenue intent", () => { const result = resolveAddressIntent( "\u043f\u043e\u043a\u0430\u0436\u0438 \u0441\u0430\u043c\u044b\u0445 \u043b\u0438\u043a\u0432\u0438\u0434\u043d\u0438\u0445 \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a\u043e\u0432" ); expect(result.intent).toBe("customer_revenue_and_payments"); }); it("resolves yearly profitability wording into customer revenue intent", () => { const result = resolveAddressIntent( "\u043a\u0430\u043a\u0438\u0435 \u0441\u0430\u043c\u044b\u0435 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0435 \u0433\u043e\u0434\u0430 \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u044b" ); expect(result.intent).toBe("customer_revenue_and_payments"); }); it("resolves major-share revenue wording into customer revenue intent", () => { const result = resolveAddressIntent("какие контрагенты принесли основную часть нашей выручки за отчетный период?"); expect(result.intent).toBe("customer_revenue_and_payments"); }); it("resolves customer revenue intent from highest inflow slang wording", () => { const result = resolveAddressIntent("какие приходы самые высокие за все время"); expect(result.intent).toBe("customer_revenue_and_payments"); }); it("resolves customer revenue intent from small deals by budget slang wording", () => { const result = resolveAddressIntent("покажи топ-20 самых маленьких сделок по бюджету"); expect(result.intent).toBe("customer_revenue_and_payments"); }); it("resolves customer revenue intent from typo highest-check wording", () => { const result = resolveAddressIntent("с каких кликентов самый высокий чек"); expect(result.intent).toBe("customer_revenue_and_payments"); }); it("resolves top counterparty slang wording into customer revenue intent", () => { const result = resolveAddressIntent("какой самый жирный контрагент у нее? кто больше платит денег"); expect(result.intent).toBe("customer_revenue_and_payments"); }); it("resolves supplier payouts profile intent from slang wording", () => { const result = resolveAddressIntent("кому мы больше всего сгрузили денег, топ-20 поставщиков"); expect(result.intent).toBe("supplier_payouts_profile"); }); it("resolves contract usage and value intent", () => { const result = resolveAddressIntent("договоры по обороту ранкни и дай топ-20"); expect(result.intent).toBe("contract_usage_and_value"); }); it("resolves top contract wording with 'контракт' into contract usage and value intent", () => { const result = resolveAddressIntent("по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?"); expect(result.intent).toBe("contract_usage_and_value"); }); it("resolves revenue-total slang wording into customer revenue intent", () => { const result = resolveAddressIntent("скока денег альтернатива заработала за 22 год"); expect(result.intent).toBe("customer_revenue_and_payments"); }); it("resolves overall-turnover wording into customer revenue intent", () => { const result = resolveAddressIntent("какие общие обороты за все время"); expect(result.intent).toBe("customer_revenue_and_payments"); }); it("resolves VAT payment forecast wording into dedicated VAT forecast intent", () => { const result = resolveAddressIntent("какой прогноз оплаты ндс за 12 мая 2020"); expect(result.intent).toBe("vat_payable_forecast"); expect(result.reasons).toContain("forecast_tax_signal_detected"); }); it("keeps colloquial VAT payment wording in forecast intent when tax-authority cue is absent", () => { const result = resolveAddressIntent("мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года"); expect(result.intent).toBe("vat_payable_forecast"); expect(result.reasons).toContain("forecast_tax_signal_detected"); }); it("resolves multi-contract counterparties wording into contract usage and value intent", () => { const result = resolveAddressIntent("Покажи контрагентов с несколькими договорами и какие из договоров активны."); expect(result.intent).toBe("contract_usage_and_value"); }); it("resolves VAT wording with debt-phrase as confirmed VAT payable intent", () => { const result = resolveAddressIntent( "\u0441\u043a\u043e\u043a\u0430 \u043d\u0434\u0441\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017" ); expect(result.intent).toBe("vat_payable_confirmed_as_of_date"); expect(result.reasons).toContain("vat_payable_confirmed_signal_detected"); }); it("resolves contracts-by-counterparty intent from list wording", () => { const result = resolveAddressIntent("покажи договора все по жуковке 51"); expect(result.intent).toBe("list_contracts_by_counterparty"); }); it("prefers documents-by-contract intent for explicit document follow-up wording", () => { const result = resolveAddressIntent("покажи документы по этому же договору"); expect(result.intent).toBe("list_documents_by_contract"); }); it("routes supplier tail-risk wording into payables intent", () => { const result = resolveAddressIntent( "Кто из поставщиков имеет хвосты с документами на конец месяца, которые уже больше похожи на систематическую проблему, а не на обычную задержку?" ); expect(result.intent).toBe("list_payables_counterparties"); }); it("marks 'кому мы должны заплатить' as payables debt lifecycle intent", () => { const result = resolveAddressIntent("каму мы должны заплатить за май 2020"); expect(result.intent).toBe("payables_confirmed_as_of_date"); expect(result.reasons).toContain("payables_debt_lifecycle_signal_detected"); }); it("resolves repair phrasing 'кто нам в целом должен' as receivables debt lifecycle intent", () => { const result = resolveAddressIntent("нет вопрос кто нам в целом должен на денег на эту дату"); expect(result.intent).toBe("receivables_confirmed_as_of_date"); expect(result.reasons).toContain("receivables_debt_lifecycle_signal_detected"); }); it("keeps out-of-scope supplier control wording as unknown intent", () => { const result = resolveAddressIntent( "Какие поставщики у нас уже пару месяцев сдают акты без приходок. Может, их надо проконтролировать отдельно чтоб не засорять бухгалтерию дальше?" ); expect(result.intent).toBe("unknown"); }); it("routes long shipment-to-payment lag wording into receivables intent", () => { const result = resolveAddressIntent( "Где у нас висят покупатели со слишком длинным периодом между отправкой товара и его оплатой, и это уже вызывает тревогу?" ); expect(result.intent).toBe("list_receivables_counterparties"); }); it("routes non-paying counterparties month-risk wording into receivables intent", () => { const result = resolveAddressIntent( "какие контрагенты пока вообще не платят за текущий месяц и это уже тревожный знак для нас?" ); expect(result.intent).toBe("list_receivables_counterparties"); }); it("routes overdue unpaid buyers wording into receivables intent", () => { const result = resolveAddressIntent("какие покупатели пока не оплатили свои товары или услуги, хотя сроки давно прошли?"); expect(result.intent).toBe("list_receivables_counterparties"); }); it("routes reconciliation mismatch wording into open contracts intent", () => { const result = resolveAddressIntent( "Покажи контрагентов, по которым сальдо скорее всего не совпадет с их актом сверки. Может, стоит поторопиться и запросить сверку?" ); expect(result.intent).toBe("list_open_contracts"); }); it("routes reconciliation mismatch wording without explicit lookup verb into open contracts intent", () => { const result = resolveAddressIntent( "По каким поставщикам у нас сальдо явно расходится с тем, что они сами указывают в своих актах сверок?" ); expect(result.intent).toBe("list_open_contracts"); }); it("routes payments-without-closing-docs wording into open contracts intent", () => { const result = resolveAddressIntent( "Где у нас есть платежи, но нет документов для закрытия взаиморасчетов? Это уже требует ручной проверки." ); expect(result.intent).toBe("list_open_contracts"); }); it("routes payments-without-settlement-closure wording into open contracts intent", () => { const result = resolveAddressIntent("где у нас есть оплаты без закрытия взаиморасчетов, и это уже требует ручной проверки?"); expect(result.intent).toBe("list_open_contracts"); }); it("routes shipments-without-closing-docs wording into open contracts intent", () => { const result = resolveAddressIntent("где у нас есть отгрузки без документов для их закрытия и это уже требует внимания?"); expect(result.intent).toBe("list_open_contracts"); }); it("routes closing-without-supporting-docs wording into open contracts intent", () => { const result = resolveAddressIntent( "где у нас есть закрытие счетов без подтверждающих документов и это уже требует ручной проверки?" ); expect(result.intent).toBe("list_open_contracts"); }); it("routes documents-without-payments wording into open contracts intent", () => { const result = resolveAddressIntent( "По каким контрагентам документы есть, а оплат нет. Может, стоит взять на карандаш такие ситуации чтоб не тянуть дальше?" ); expect(result.intent).toBe("list_open_contracts"); }); it("routes stale advances without closing docs wording into open contracts intent", () => { const result = resolveAddressIntent( "по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?" ); expect(result.intent).toBe("list_open_contracts"); }); it("routes buyers with open debt wording into open-items intent", () => { const result = resolveAddressIntent("по каким покупателям у нас есть открытые задолженности на конец месяца?"); expect(result.intent).toBe("open_items_by_counterparty_or_contract"); }); }); describe("address filter extraction for balance drilldown", () => { it("does not force default limit=20 for management aggregate intents", () => { const periodProfile = extractAddressFilters("За какие годы в базе есть данные?", "period_coverage_profile"); const docSectionProfile = extractAddressFilters( "Какие типы документов используются чаще всего в базе?", "document_type_and_account_section_profile" ); const counterpartyProfile = extractAddressFilters( "Сколько всего уникальных контрагентов в базе?", "counterparty_population_and_roles" ); const counterpartyLifecycle = extractAddressFilters( "Какие заказчики работали с нами в 2020 году?", "counterparty_activity_lifecycle" ); const contractOverview = extractAddressFilters( "Сколько всего договоров заведено и сколько из них реально использовались?", "contract_usage_overview" ); const customerValue = extractAddressFilters( "какие клиенты самые доходные, выдай топ-20", "customer_revenue_and_payments" ); const supplierValue = extractAddressFilters( "кому мы больше всего сгрузили денег, топ-20 поставщиков", "supplier_payouts_profile" ); const contractValue = extractAddressFilters( "договоры по обороту ранкни и дай топ-20", "contract_usage_and_value" ); const vatForecast = extractAddressFilters("какой прогноз оплаты ндс за 12 мая 2020", "vat_payable_forecast"); expect(periodProfile.extracted_filters.limit).toBeUndefined(); expect(docSectionProfile.extracted_filters.limit).toBeUndefined(); expect(counterpartyProfile.extracted_filters.limit).toBeUndefined(); expect(counterpartyLifecycle.extracted_filters.limit).toBeUndefined(); expect(contractOverview.extracted_filters.limit).toBeUndefined(); expect(customerValue.extracted_filters.limit).toBe(20); expect(supplierValue.extracted_filters.limit).toBe(20); expect(contractValue.extracted_filters.limit).toBe(20); expect(vatForecast.extracted_filters.limit).toBeUndefined(); expect(periodProfile.extracted_filters.period_to).toBeDefined(); expect(docSectionProfile.extracted_filters.period_to).toBeDefined(); expect(counterpartyProfile.extracted_filters.period_to).toBeDefined(); expect(counterpartyLifecycle.extracted_filters.period_to).toBeDefined(); expect(contractOverview.extracted_filters.period_to).toBeDefined(); expect(customerValue.extracted_filters.period_to).toBeDefined(); expect(supplierValue.extracted_filters.period_to).toBeDefined(); expect(contractValue.extracted_filters.period_to).toBeDefined(); expect(vatForecast.extracted_filters.period_from).toBe("2020-04-01"); expect(vatForecast.extracted_filters.period_to).toBe("2020-05-12"); expect(periodProfile.warnings).toContain("period_to_defaulted_today_for_management_profile"); expect(docSectionProfile.warnings).toContain("period_to_defaulted_today_for_management_profile"); expect(counterpartyProfile.warnings).toContain("period_to_defaulted_today_for_management_profile"); expect(counterpartyLifecycle.warnings).not.toContain("period_to_defaulted_today_for_management_profile"); expect(contractOverview.warnings).toContain("period_to_defaulted_today_for_management_profile"); expect(customerValue.warnings).toContain("period_to_defaulted_today_for_management_profile"); expect(supplierValue.warnings).toContain("period_to_defaulted_today_for_management_profile"); expect(contractValue.warnings).toContain("period_to_defaulted_today_for_management_profile"); expect(vatForecast.warnings).toContain("period_derived_from_month_phrase"); expect(vatForecast.warnings).toContain("period_from_derived_from_quarter_for_vat_forecast"); expect(vatForecast.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast"); expect(vatForecast.warnings).not.toContain("period_to_defaulted_today_for_management_profile"); }); it("extracts short-year period for lifecycle customer list question", () => { const lifecycleShortYear = extractAddressFilters( "покажи список заказчиков за 20год", "counterparty_activity_lifecycle" ); expect(lifecycleShortYear.extracted_filters.period_from).toBe("2020-01-01"); expect(lifecycleShortYear.extracted_filters.period_to).toBe("2020-12-31"); }); it("drops noisy counterparty anchor in ranking question for customer revenue profile", () => { const extracted = extractAddressFilters( "какой самый жирный контрагент у нее? кто больше платит денег", "customer_revenue_and_payments" ); expect(extracted.extracted_filters.counterparty).toBeUndefined(); expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality"); }); it("drops pseudo-counterparty 'деньги на данную дату' from diagnostic rewrite phrase", () => { const extracted = extractAddressFilters( "Неясно, кто должен компании деньги на данную дату.", "unknown" ); expect(extracted.extracted_filters.counterparty).toBeUndefined(); expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality"); }); it("does not capture narrative filler as counterparty in broad docs-vs-money question", () => { const extracted = extractAddressFilters( "В каких случаях мы видим ситуацию, когда документы есть, а денег нет и пока не предвидится?", "list_documents_by_counterparty" ); expect(extracted.extracted_filters.counterparty).toBeUndefined(); expect(String(extracted.extracted_filters.counterparty ?? "")).not.toContain("случая"); }); it("does not derive fake counterparty anchor for open-contracts stale-advance wording", () => { const extracted = extractAddressFilters( "по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?", "list_open_contracts" ); expect(extracted.extracted_filters.counterparty).toBeUndefined(); }); it("derives VAT forecast quarter-to-date window when plain date phrase is present", () => { const extracted = extractAddressFilters( "мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года", "vat_payable_forecast" ); expect(extracted.extracted_filters.period_from).toBe("2020-01-01"); expect(extracted.extracted_filters.period_to).toBe("2020-03-15"); expect(extracted.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast"); }); it("derives full tax quarter window for confirmed VAT tax-period intent from month phrase", () => { const extracted = extractAddressFilters( "сколько ндс надо заплатить в налоговую за декабрь 2019", "vat_liability_confirmed_for_tax_period" ); expect(extracted.extracted_filters.period_from).toBe("2019-10-01"); expect(extracted.extracted_filters.period_to).toBe("2019-12-31"); expect(extracted.warnings).toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability"); }); it("derives VAT forecast quarter-to-date window for explicit day+month+year phrase", () => { const extracted = extractAddressFilters( "сколько НДС нужно заплатить за 5 марта 2017 года", "vat_payable_forecast" ); expect(extracted.extracted_filters.period_from).toBe("2017-01-01"); expect(extracted.extracted_filters.period_to).toBe("2017-03-05"); expect(extracted.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast"); }); it("derives VAT forecast quarter-to-date window when strict as-of cue is present", () => { const extracted = extractAddressFilters( "сколько НДС нужно заплатить по состоянию на 15 марта 2020 года", "vat_payable_forecast" ); expect(extracted.extracted_filters.period_from).toBe("2020-01-01"); expect(extracted.extracted_filters.period_to).toBe("2020-03-15"); expect(extracted.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast"); }); it("defaults as_of_date for documents_forming_balance when date is omitted", () => { const result = extractAddressFilters("which documents form balance for account 62", "documents_forming_balance"); expect(result.extracted_filters.account).toBe("62"); expect(result.extracted_filters.as_of_date).toBeDefined(); expect(result.missing_required_filters).toEqual([]); }); it("cuts period tail from counterparty anchor", () => { const result = extractAddressFilters( "Покажи документы по контрагенту test_cp с 2020-07-01 по 2020-07-31", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("test_cp"); expect(result.extracted_filters.period_from).toBe("2020-07-01"); expect(result.extracted_filters.period_to).toBe("2020-07-31"); }); it("cuts all-time tail from counterparty anchor and skips 90-day default window", () => { const result = extractAddressFilters( "Покажи документы по контрагенту тестовый за все время", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("тестовый"); expect(result.extracted_filters.period_from).toBeUndefined(); expect(result.extracted_filters.period_to).toBeUndefined(); expect(result.warnings).not.toContain("period_defaulted_last_90_days"); }); it("keeps all-time period by default for counterparty docs query without explicit window", () => { const result = extractAddressFilters( "Покажи документы по контрагенту тестовый", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("тестовый"); expect(result.extracted_filters.period_from).toBeUndefined(); expect(result.extracted_filters.period_to).toBeUndefined(); expect(result.warnings).not.toContain("period_defaulted_last_90_days"); }); it("extracts counterparty from company phrase and derives year period", () => { const result = extractAddressFilters( "Какие документы доступны по компании СВК за 2021 год?", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("СВК"); expect(result.extracted_filters.period_from).toBe("2021-01-01"); expect(result.extracted_filters.period_to).toBe("2021-12-31"); expect(result.warnings).toContain("period_derived_from_year_phrase"); }); it("extracts counterparty from supplier phrase and derives month period", () => { const result = extractAddressFilters( "Покажи документы по поставщику Альфа за июль 2020", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("Альфа"); expect(result.extracted_filters.period_from).toBe("2020-07-01"); expect(result.extracted_filters.period_to).toBe("2020-07-31"); expect(result.warnings).toContain("period_derived_from_month_phrase"); }); it("cuts period-end tail from counterparty anchor and keeps as_of for open-items query", () => { const result = extractAddressFilters( "Покажи хвосты по контрагенту СВК на конец периода 2020-12-31", "open_items_by_counterparty_or_contract" ); expect(result.extracted_filters.counterparty).toBe("СВК"); expect(result.extracted_filters.as_of_date).toBe("2020-12-31"); expect(result.extracted_filters.period_from).toBeUndefined(); expect(result.extracted_filters.period_to).toBeUndefined(); expect(result.warnings).toContain("period_window_cleared_for_as_of_intent"); }); it("cuts report-date tail from counterparty anchor and keeps clean as_of filter", () => { const result = extractAddressFilters( "Показать незакрытые записи для контрагента 'СВК' на дату отчетности 2020-12-31", "open_items_by_counterparty_or_contract" ); expect(result.extracted_filters.counterparty).toBe("СВК"); expect(result.extracted_filters.as_of_date).toBe("2020-12-31"); expect(String(result.extracted_filters.counterparty ?? "").toLowerCase()).not.toContain("отчетности"); }); it("derives month period for balance snapshot from 'на май 2020'", () => { const result = extractAddressFilters("Какой остаток по счету 60 на май 2020", "account_balance_snapshot"); expect(result.extracted_filters.account).toBe("60"); expect(result.extracted_filters.period_from).toBe("2020-05-01"); expect(result.extracted_filters.period_to).toBe("2020-05-31"); expect(result.extracted_filters.as_of_date).toBe("2020-05-31"); expect(result.warnings).toContain("period_derived_from_month_phrase"); expect(result.warnings).toContain("as_of_date_derived_from_period_to"); }); it("derives month period for balance snapshot from 'на 2020.05'", () => { const result = extractAddressFilters("Какой остаток по счету 60 на 2020.05", "account_balance_snapshot"); expect(result.extracted_filters.account).toBe("60"); expect(result.extracted_filters.period_from).toBe("2020-05-01"); expect(result.extracted_filters.period_to).toBe("2020-05-31"); expect(result.extracted_filters.as_of_date).toBe("2020-05-31"); expect(result.warnings).toContain("period_derived_from_month_phrase"); expect(result.warnings).toContain("as_of_date_derived_from_period_to"); }); it("derives month period for balance snapshot from 'на 2020 май'", () => { const result = extractAddressFilters("Какой остаток по счету 60 на 2020 май", "account_balance_snapshot"); expect(result.extracted_filters.account).toBe("60"); expect(result.extracted_filters.period_from).toBe("2020-05-01"); expect(result.extracted_filters.period_to).toBe("2020-05-31"); expect(result.extracted_filters.as_of_date).toBe("2020-05-31"); expect(result.warnings).toContain("period_derived_from_month_phrase"); expect(result.warnings).toContain("as_of_date_derived_from_period_to"); }); it("derives month period for inventory snapshot from prepositional month wording 'в мае 2016'", () => { const result = extractAddressFilters("Что лежит на складе в мае 2016 года?", "inventory_on_hand_as_of_date"); expect(result.extracted_filters.period_from).toBe("2016-05-01"); expect(result.extracted_filters.period_to).toBe("2016-05-31"); expect(result.extracted_filters.as_of_date).toBe("2016-05-31"); expect(result.warnings).toContain("period_derived_from_month_phrase"); }); it("extracts dotted account by heuristic for docs-forming phrasing without 'счет' keyword", () => { const result = extractAddressFilters( "раскрой остаток 60.01 по документам на конец июля 2020", "documents_forming_balance" ); expect(result.extracted_filters.account).toBe("60.01"); expect(result.extracted_filters.as_of_date).toBe("2020-07-31"); expect(result.warnings).toContain("account_anchor_derived_from_heuristic_token"); }); it("extracts dotted account by heuristic for short balance slang", () => { const result = extractAddressFilters("скока по 60.02 на конец 2020-12", "account_balance_snapshot"); expect(result.extracted_filters.account).toBe("60.02"); expect(result.extracted_filters.as_of_date).toBe("2020-12-31"); expect(result.warnings).toContain("account_anchor_derived_from_heuristic_token"); }); it("does not derive counterparty from follow-up filler token in bank phrase", () => { const result = extractAddressFilters("а теперь банковские операции", "bank_operations_by_counterparty"); expect(result.extracted_filters.counterparty).toBeUndefined(); }); it("keeps compact account for docs-forming follow-up and avoids fake counterparty anchor", () => { const result = extractAddressFilters("раскрой 62.01 документами на ту же дату", "documents_forming_balance"); expect(result.extracted_filters.account).toBe("62.01"); expect(result.extracted_filters.counterparty).toBeUndefined(); }); it("drops accidental account for non-account intent without explicit account cue", () => { const result = extractAddressFilters("покажи банк операции по свк за 2020", "bank_operations_by_counterparty"); expect(result.extracted_filters.account).toBeUndefined(); }); it("extracts leading counterparty token for short bank phrase", () => { const result = extractAddressFilters("свк списания/поступления за 2020", "bank_operations_by_counterparty"); expect(result.extracted_filters.counterparty).toBe("свк"); expect(result.warnings).toContain("counterparty_anchor_derived_from_leading_token"); }); it("treats 'за весь период' as all-time hint and does not force 90-day default", () => { const result = extractAddressFilters( "Покажи банковские операции по клиенту Бета за весь период", "bank_operations_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("Бета"); expect(result.extracted_filters.period_from).toBeUndefined(); expect(result.extracted_filters.period_to).toBeUndefined(); expect(result.warnings).not.toContain("period_defaulted_last_90_days"); }); it("extracts loose by-anchor and year period for short slang docs phrase", () => { const result = extractAddressFilters( "какие доки есть по свк за 2021", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("свк"); expect(result.extracted_filters.period_from).toBe("2021-01-01"); expect(result.extracted_filters.period_to).toBe("2021-12-31"); expect(result.warnings).toContain("counterparty_anchor_derived_from_loose_by_phrase"); expect(result.warnings).toContain("period_derived_from_year_phrase"); }); it("extracts implicit counterparty and short-year period for typo slang docs phrase", () => { const result = extractAddressFilters( "свк доки за 20год покеж", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("свк"); expect(result.extracted_filters.period_from).toBe("2020-01-01"); expect(result.extracted_filters.period_to).toBe("2020-12-31"); expect(result.warnings).toContain("counterparty_anchor_derived_from_implicit_phrase"); expect(result.warnings).toContain("period_derived_from_year_phrase"); }); it("does not use filler token 'есть' as counterparty when explicit shorthand anchor exists", () => { const result = extractAddressFilters("какие у свк есть доки за 2020?", "list_documents_by_counterparty"); expect(result.extracted_filters.counterparty).toBe("свк"); expect(result.extracted_filters.counterparty).not.toBe("есть"); expect(result.extracted_filters.period_from).toBe("2020-01-01"); expect(result.extracted_filters.period_to).toBe("2020-12-31"); }); it("extracts compact counterparty and relaxed short-year period from noisy phrase", () => { const result = extractAddressFilters( "свк 20 год - покажи доки плс", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("свк"); expect(result.extracted_filters.period_from).toBe("2020-01-01"); expect(result.extracted_filters.period_to).toBe("2020-12-31"); expect(result.warnings).toContain("counterparty_anchor_derived_from_leading_token"); expect(result.warnings).toContain("period_derived_from_year_phrase"); expect(result.extracted_filters.counterparty).not.toBe("плс"); }); it("extracts short ordinal year period from noisy docs phrase", () => { const result = extractAddressFilters( "бля епт покажи доки по свк за 20-й", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("свк"); expect(result.extracted_filters.period_from).toBe("2020-01-01"); expect(result.extracted_filters.period_to).toBe("2020-12-31"); expect(result.warnings).toContain("period_derived_from_year_phrase"); }); it("extracts short bare year period from follow-up phrase", () => { const result = extractAddressFilters("теперь за 21", "counterparty_activity_lifecycle"); expect(result.extracted_filters.period_from).toBe("2021-01-01"); expect(result.extracted_filters.period_to).toBe("2021-12-31"); expect(result.warnings).toContain("period_derived_from_year_phrase"); }); it("does not use action verb as counterparty when phrase is 'Показать документы '", () => { const result = extractAddressFilters( "Показать документы СВК за 2020 год.", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("СВК"); expect(result.extracted_filters.counterparty).not.toBe("Показать"); expect(result.extracted_filters.period_from).toBe("2020-01-01"); expect(result.extracted_filters.period_to).toBe("2020-12-31"); }); it("extracts counterparty and short year from transliterated noisy phrase", () => { const result = extractAddressFilters( "svk doki za 20 god pokezh", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("svk"); expect(result.extracted_filters.period_from).toBe("2020-01-01"); expect(result.extracted_filters.period_to).toBe("2020-12-31"); expect( result.warnings.some( (warning) => warning === "counterparty_anchor_derived_from_implicit_phrase" || warning === "counterparty_anchor_derived_from_leading_token" ) ).toBe(true); expect(result.warnings).toContain("period_derived_from_year_phrase"); }); it("does not treat transliterated filler verb as counterparty in docy phrase", () => { const result = extractAddressFilters( "svk poka docy za 2020", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("svk"); expect(result.extracted_filters.counterparty).not.toBe("poka"); expect(result.extracted_filters.period_from).toBe("2020-01-01"); expect(result.extracted_filters.period_to).toBe("2020-12-31"); }); it("repairs mojibake phrase before extracting counterparty filters", () => { const result = extractAddressFilters( "Показать документы РЎР’Рљ Р·Р° 2020 РіРѕРґ.", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("СВК"); expect(result.extracted_filters.period_from).toBe("2020-01-01"); expect(result.extracted_filters.period_to).toBe("2020-12-31"); }); it("extracts explicit year range period from phrase", () => { const result = extractAddressFilters( "Какие документы по СВК за 2000 - 2025 год?", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("СВК"); expect(result.extracted_filters.period_from).toBe("2000-01-01"); expect(result.extracted_filters.period_to).toBe("2025-12-31"); expect(result.warnings).toContain("period_derived_from_year_range_phrase"); }); it("extracts contract and year period for contract document list", () => { const result = extractAddressFilters( "Покажи документы по договору 19/15 за 2020 год", "list_documents_by_contract" ); expect(result.extracted_filters.contract).toBe("19/15"); expect(result.extracted_filters.period_from).toBe("2020-01-01"); expect(result.extracted_filters.period_to).toBe("2020-12-31"); expect(result.warnings).toContain("period_derived_from_year_phrase"); }); it("extracts contracts-by-counterparty anchor with numeric suffix from loose 'по ...' phrase", () => { const result = extractAddressFilters( "покажи договора все по жуковке 51", "list_contracts_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("жуковке 51"); expect(result.extracted_filters.contract).toBeUndefined(); expect(result.missing_required_filters).toEqual([]); expect(result.warnings).toContain("counterparty_anchor_derived_from_loose_by_phrase"); }); it("cuts trailing as-of date from contract anchor", () => { const result = extractAddressFilters( "Покажи документы по договору 1-ПМ/2020 на дату 31.07.2020", "list_documents_by_contract" ); expect(result.extracted_filters.contract).toBe("1-ПМ/2020"); expect(result.extracted_filters.period_from).toBe("2020-07-01"); expect(result.extracted_filters.period_to).toBe("2020-07-31"); }); it("does not force 90-day default window for by-contract query without explicit period", () => { const result = extractAddressFilters( "Покажи документы по договору 19/15", "list_documents_by_contract" ); expect(result.extracted_filters.contract).toBe("19/15"); expect(result.extracted_filters.period_from).toBeUndefined(); expect(result.extracted_filters.period_to).toBeUndefined(); expect(result.warnings).not.toContain("period_defaulted_last_90_days"); }); it("extracts heuristic contract token for noisy contract phrase", () => { const result = extractAddressFilters( "доки 19/15 за 2020", "list_documents_by_contract" ); expect(result.extracted_filters.contract).toBe("19/15"); expect(result.warnings).toContain("contract_anchor_derived_from_heuristic_token"); expect(result.extracted_filters.period_from).toBe("2020-01-01"); expect(result.extracted_filters.period_to).toBe("2020-12-31"); }); it("trims english year tail from contract anchor", () => { const result = extractAddressFilters( "docs by contract 19/15 year 2020", "list_documents_by_contract" ); expect(result.extracted_filters.contract).toBe("19/15"); expect(result.extracted_filters.period_from).toBe("2020-01-01"); expect(result.extracted_filters.period_to).toBe("2020-12-31"); }); it("trims trailing separated year from contract anchor", () => { const result = extractAddressFilters( "docs by contract 19/15 2020", "list_documents_by_contract" ); expect(result.extracted_filters.contract).toBe("19/15"); expect(result.extracted_filters.period_from).toBe("2020-01-01"); expect(result.extracted_filters.period_to).toBe("2020-12-31"); }); it("trims explanatory tail after contract token", () => { const result = extractAddressFilters( "документы по договору 19/15 выведите связанные документы", "list_documents_by_contract" ); expect(result.extracted_filters.contract).toBe("19/15"); }); it("extracts multiline year range period from phrase", () => { const result = extractAddressFilters( "Какие документы по СВК за 2000 - 2025\n год?", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("СВК"); expect(result.extracted_filters.period_from).toBe("2000-01-01"); expect(result.extracted_filters.period_to).toBe("2025-12-31"); expect(result.warnings).toContain("period_derived_from_year_range_phrase"); expect(result.warnings).not.toContain("period_derived_from_year_phrase"); }); it("extracts russian year range period from 'с ... по ...' phrase", () => { const result = extractAddressFilters( "какие есть доки по свк с 2020 по 2025 год", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("свк"); expect(result.extracted_filters.period_from).toBe("2020-01-01"); expect(result.extracted_filters.period_to).toBe("2025-12-31"); expect(result.warnings).toContain("period_derived_from_year_range_phrase"); expect(result.warnings).not.toContain("period_defaulted_last_90_days"); }); it("treats 'за любой период' as all-time hint and keeps loose by-anchor", () => { const result = extractAddressFilters( "за любой период есть что-то по свк?", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("свк"); expect(result.extracted_filters.period_from).toBeUndefined(); expect(result.extracted_filters.period_to).toBeUndefined(); expect(result.warnings).toContain("counterparty_anchor_derived_from_loose_by_phrase"); expect(result.warnings).not.toContain("period_defaulted_last_90_days"); }); }); describe("address query limited taxonomy and stage diagnostics", { timeout: 15000 }, () => { it("does not default standalone item provenance questions to today without explicit temporal cue", () => { const result = extractAddressFilters( "От какого поставщика куплен товар Столешница 600*3050*26 дуб ниагара", "inventory_purchase_provenance_for_item" ); expect(result.extracted_filters.item).toBe("Столешница 600*3050*26 дуб ниагара"); expect(result.extracted_filters.as_of_date).toBeUndefined(); expect(result.warnings).not.toContain("as_of_date_defaulted_today"); }); it("injects as_of_date from analysis context when user message has no explicit period", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Покажи контрагентов с незакрытыми хвостами", { analysisDateHint: "2020-07-31" }); expect(result?.handled).toBe(true); expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-07-31"); expect(Array.isArray(result?.debug.reasons)).toBe(true); expect(result?.debug.reasons).toContain("as_of_date_from_analysis_context"); }); it("returns soft out-of-scope reply without technical jargon for unsupported supplier-control wording", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "Какие поставщики у нас уже пару месяцев сдают акты без приходок. Может, их надо проконтролировать отдельно чтоб не засорять бухгалтерию дальше?" ); expect(result?.handled).toBe(true); expect(result?.response_type).toBe("LIMITED_WITH_REASON"); expect(result?.debug.detected_intent).toBe("unknown"); expect(result?.debug.limited_reason_category).toBe("unsupported"); const reply = String(result?.reply_text ?? ""); expect(reply.toLowerCase()).toContain("адресн"); expect(reply).toContain("Что могу сделать сейчас:"); expect(reply).not.toMatch(/address_query|V1|lookup|materialized|якор/iu); }); it("routes supplier tail-risk wording without forcing missing-anchor fallback", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "Кто из поставщиков имеет хвосты с документами на конец месяца, которые уже больше похожи на систематическую проблему, а не на обычную задержку?" ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_payables_counterparties"); expect(result?.debug.limited_reason_category).not.toBe("missing_anchor"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); }); it("routes 'каму мы должны заплатить за май 2020' into confirmed payables flow with controlled fallback on schema limits", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("каму мы должны заплатить за май 2020"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("payables_confirmed_as_of_date"); expect(result?.debug.requested_result_mode).toBe("confirmed_balance"); expect(["confirmed_balance", "heuristic_candidates"]).toContain(result?.debug.result_mode); expect(result?.debug.as_of_date_basis).toBe("explicit_as_of_date"); expect(["address_payables_confirmed_as_of_date_v1", "address_open_items_by_party_or_contract_v1"]).toContain(result?.debug.selected_recipe); expect(result?.debug.route_expectation_status).toBe("matched"); expect(result?.debug.route_expectation_reason).toBe("route_expectation_matched"); expect(Array.isArray(result?.debug.reasons)).toBe(true); if (result?.debug.result_mode === "heuristic_candidates") { expect(result?.debug.reasons).toContain("confirmed_balance_unavailable_fallback_to_heuristic_candidates"); expect(result?.debug.reasons).toContain("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items"); expect(result?.debug.balance_confirmed).toBe(false); } else { expect(result?.debug.reasons).not.toContain("confirmed_balance_unavailable_fallback_to_heuristic_candidates"); } expect(["FACTUAL_LIST", "FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type); const reply = String(result?.reply_text ?? ""); if (result?.response_type === "LIMITED_WITH_REASON") { expect(result?.debug.balance_confirmed).toBe(false); expect(result?.debug.reasons).toContain("exact_payables_mode_limited_response"); expect(reply.toLowerCase()).not.toContain("эвристич"); } else if (result?.debug.result_mode === "heuristic_candidates") { expect(result?.debug.balance_confirmed).toBe(false); expect(reply).toContain("shortlist"); expect(reply.toLowerCase()).toContain("shortlist"); } else { expect(result?.debug.balance_confirmed).toBe(true); expect(reply).toContain("Итого подтвержденный долг"); expect(reply).toMatch(/Блок\s+(?:\*\*1\*\*|1)\.\s+Статус результата/u); expect(reply).toMatch(/\n\nБлок\s+(?:\*\*2\*\*|2)\.\s+Что учтено/u); expect(reply).toMatch(/\n\nБлок\s+(?:\*\*3\*\*|3)\.\s+Сводка/u); expect(reply).toMatch(/\n\nБлок\s+(?:\*\*4\*\*|4)\.\s+Категории обязательств/u); expect(reply).toMatch(/\n\nБлок\s+(?:\*\*5\*\*|5)\.\s+Подтвержденные позиции к оплате/u); expect(reply).not.toContain("эвристический"); } }); it("routes shipment-to-payment lag wording into receivables lane without missing-anchor fallback", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "Где у нас висят покупатели со слишком длинным периодом между отправкой товара и его оплатой, и это уже вызывает тревогу?" ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_receivables_counterparties"); expect(result?.debug.limited_reason_category).not.toBe("missing_anchor"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); }); it("keeps strict account scope for receivables risk replies and excludes far-future leakage", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "Покажи контрагентов, чьи заказы на отгрузку еще не оплачены, но сальдо уже отрицательное." ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_receivables_counterparties"); expect(result?.debug.account_scope_mode).toBe("strict"); expect(result?.debug.account_scope_fallback_applied).toBe(false); if (result?.response_type === "FACTUAL_LIST") { const reply = String(result?.reply_text ?? ""); expect(reply).not.toMatch(/68\.0?2\s*\/\s*19\.0?4/); expect(reply).not.toContain("2030-"); } }); it("routes payments-without-closing-docs wording into open contracts lane", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "Где у нас есть платежи, но нет документов для закрытия взаиморасчетов? Это уже требует ручной проверки." ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_open_contracts"); expect(result?.debug.limited_reason_category).not.toBe("missing_anchor"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); }); it("routes payments-without-settlement-closure wording into open contracts lane", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("где у нас есть оплаты без закрытия взаиморасчетов, и это уже требует ручной проверки?"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_open_contracts"); expect(["address_open_contracts_candidates_v1", "address_open_items_by_party_or_contract_v1"]).toContain( result?.debug.selected_recipe ); expect(result?.debug.limited_reason_category).not.toBe("missing_anchor"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); }); it("routes shipments-without-closing-docs wording into open contracts lane", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("где у нас есть отгрузки без документов для их закрытия и это уже требует внимания?"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_open_contracts"); expect(["address_open_contracts_candidates_v1", "address_open_items_by_party_or_contract_v1"]).toContain( result?.debug.selected_recipe ); expect(result?.debug.limited_reason_category).not.toBe("missing_anchor"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); }); it("routes closing-without-supporting-docs wording into open contracts lane", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "где у нас есть закрытие счетов без подтверждающих документов и это уже требует ручной проверки?" ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_open_contracts"); expect(["address_open_contracts_candidates_v1", "address_open_items_by_party_or_contract_v1"]).toContain( result?.debug.selected_recipe ); expect(result?.debug.limited_reason_category).not.toBe("missing_anchor"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); }); it("keeps strict account scope for confirmed open-contract scans", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Какие незакрытые документы по договорам у нас уже давно пора проверить?"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("open_contracts_confirmed_as_of_date"); expect(result?.debug.selected_recipe).toBe("address_open_contracts_confirmed_as_of_date_v1"); expect(result?.debug.account_scope_mode).toBe("strict"); expect(result?.debug.account_scope_fallback_applied).toBe(false); }); it("routes stale advances wording into open contracts lane without missing-anchor fallback", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "по каким поставщикам мы видим проблемные авансы, которые давно не закрыты документами?" ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_open_contracts"); expect(result?.debug.limited_reason_category).not.toBe("missing_anchor"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); }); it("does not return execution_error for confirmed open-contracts month query", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "\u043a\u0430\u043a\u0438\u0435 \u0435\u0441\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430 \u043d\u0430 \u043c\u0430\u0440\u0442 2020" ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("open_contracts_confirmed_as_of_date"); expect(result?.debug.limited_reason_category).not.toBe("execution_error"); }); it("routes direct open-contract month query into exact confirmed mode", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("какие есть открытые договора на март 2020"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("open_contracts_confirmed_as_of_date"); expect(result?.debug.extracted_filters.period_from).toBe("2020-03-01"); expect(result?.debug.extracted_filters.period_to).toBe("2020-03-31"); expect(result?.debug.extracted_filters.as_of_date).toBe("2020-03-31"); expect(result?.debug.route_expectation_status).toBe("matched"); expect(result?.debug.route_expectation_reason).toBe("route_expectation_matched"); expect(result?.debug.route_expectation_expected_result_modes).toContain("confirmed_balance"); expect(result?.debug.requested_result_mode).toBe("confirmed_balance"); expect(result?.debug.result_mode).toBe("confirmed_balance"); expect(result?.debug.selected_recipe).toBe("address_open_contracts_confirmed_as_of_date_v1"); expect(result?.debug.capability_route_mode).toBe("exact"); const reply = String(result?.reply_text ?? ""); if (result?.response_type !== "LIMITED_WITH_REASON") { expect(reply).toContain("Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату."); } }); it("keeps preferred account-scope mode for heuristic open-contract fallback recipe and avoids zeroing rows", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "Где у нас есть платежи, но нет документов для закрытия взаиморасчетов? Это уже требует ручной проверки." ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_open_contracts"); if (result?.debug.selected_recipe === "address_open_items_by_party_or_contract_v1") { expect(result?.debug.account_scope_mode).toBe("preferred"); expect(result?.debug.account_scope_fallback_applied).toBe(true); expect(result?.debug.rows_after_account_scope).toBeGreaterThan(0); expect(result?.debug.limited_reason_category).not.toBe("empty_match"); } }); it("routes non-paying counterparties month-risk wording into receivables lane", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "какие контрагенты пока вообще не платят за текущий месяц и это уже тревожный знак для нас?" ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_receivables_counterparties"); expect(["address_movements_receivables_v1", "address_open_items_by_party_or_contract_v1"]).toContain( result?.debug.selected_recipe ); expect(result?.debug.limited_reason_category).not.toBe("missing_anchor"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); }); it("routes overdue unpaid buyers wording into receivables lane without missing-anchor fallback", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "какие покупатели пока не оплатили свои товары или услуги, хотя сроки давно прошли?" ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_receivables_counterparties"); expect(["address_movements_receivables_v1", "address_open_items_by_party_or_contract_v1"]).toContain( result?.debug.selected_recipe ); expect(result?.debug.limited_reason_category).not.toBe("missing_anchor"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); }); it("routes documents-without-payments wording into open contracts lane", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "По каким контрагентам документы есть, а оплат нет. Может, стоит взять на карандаш такие ситуации чтоб не тянуть дальше?" ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_open_contracts"); expect(result?.debug.limited_reason_category).not.toBe("missing_anchor"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); }); it("routes period coverage profile question into dedicated aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("За какие годы в базе есть данные?"); expect(result?.handled).toBe(true); expect(result?.debug.detected_mode).toBe("address_query"); expect(result?.debug.detected_intent).toBe("period_coverage_profile"); expect(result?.debug.selected_recipe).toBe("address_period_coverage_profile_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type); }); it("does not rewrite active-month management question into bank-ops counterparty lane", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Какой месяц самый активный по количеству операций?"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("period_coverage_profile"); expect(result?.debug.selected_recipe).toBe("address_period_coverage_profile_v1"); expect(result?.debug.extracted_filters.counterparty).toBeUndefined(); expect(["FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type); }); it("routes document+section profile question into dedicated aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Какие типы документов используются чаще всего в базе?"); expect(result?.handled).toBe(true); expect(result?.debug.detected_mode).toBe("address_query"); expect(result?.debug.detected_intent).toBe("document_type_and_account_section_profile"); expect(result?.debug.selected_recipe).toBe("address_document_type_and_account_section_profile_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type); }); it("routes counterparty population question into dedicated aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Сколько всего уникальных контрагентов в базе?"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("counterparty_population_and_roles"); expect(result?.debug.selected_recipe).toBe("address_counterparty_population_roles_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type); }); it("routes contract usage overview question into dedicated aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Сколько всего договоров заведено и сколько из них реально использовались?"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("contract_usage_overview"); expect(result?.debug.selected_recipe).toBe("address_contract_usage_overview_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type); }); it("routes customer value question into dedicated aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("какие клиенты самые доходные, выдай топ-20"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments"); expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes highest inflow slang wording into customer value aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("какие приходы самые высокие за все время"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments"); expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes 'кто больше всего принес денег в 2020' into customer value aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("кто больше всего принес денег в 2020"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments"); expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes colloquial 'кто нам больше денег принес' into customer value aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("\u043a\u0442\u043e \u043d\u0430\u043c \u0431\u043e\u043b\u044c\u0448\u0435 \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0435\u0441"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments"); expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes typo 'ликвидних заказчиков' into customer value aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "\u043f\u043e\u043a\u0430\u0436\u0438 \u0441\u0430\u043c\u044b\u0445 \u043b\u0438\u043a\u0432\u0438\u0434\u043d\u0438\u0445 \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a\u043e\u0432" ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments"); expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes yearly profitability wording into customer value aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "\u043a\u0430\u043a\u0438\u0435 \u0441\u0430\u043c\u044b\u0435 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0435 \u0433\u043e\u0434\u0430 \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u044b" ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments"); expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes typo highest-check wording into customer value aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("с каких кликентов самый высокий чек"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments"); expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes top counterparty slang wording into customer value aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("какой самый жирный контрагент у нее? кто больше платит денег"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments"); expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1"); expect(result?.debug.extracted_filters.counterparty).toBeUndefined(); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes supplier payout question into dedicated aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("кому мы больше всего сгрузили денег, топ-20 поставщиков"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("supplier_payouts_profile"); expect(result?.debug.selected_recipe).toBe("address_supplier_payouts_profile_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes contract value question into dedicated aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("договоры по обороту ранкни и дай топ-20"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("contract_usage_and_value"); expect(result?.debug.selected_recipe).toBe("address_contract_usage_and_value_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes top contract wording with 'контракт' into contract value aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("contract_usage_and_value"); expect(result?.debug.selected_recipe).toBe("address_contract_usage_and_value_v1"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes revenue-total slang wording into customer value aggregate recipe (no account-missing fallback)", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("скока денег альтернатива заработала за 22 год"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments"); expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1"); expect(result?.debug.limited_reason_category).not.toBe("missing_anchor"); expect(result?.debug.missing_required_filters).not.toContain("account"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes overall-turnover wording into customer value aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("какие общие обороты за все время"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments"); expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes VAT payment forecast wording into dedicated VAT forecast recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("какой прогноз оплаты ндс за 12 мая 2020"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("vat_payable_forecast"); expect(result?.debug.selected_recipe).toBe("address_vat_payable_forecast_v1"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(result?.debug.route_expectation_status).toBe("matched"); expect(result?.debug.extracted_filters.counterparty).toBeUndefined(); }); it("routes colloquial VAT payment wording without tax-authority cue into VAT forecast recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("vat_payable_forecast"); expect(result?.debug.selected_recipe).toBe("address_vat_payable_forecast_v1"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(result?.debug.route_expectation_status).toBe("matched"); }); it("routes 'в налоговую за декабрь' VAT wording into confirmed tax-period route", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("прикинь скок надо заплатить ндс в налоговую на декабрь 2019"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("vat_liability_confirmed_for_tax_period"); expect(result?.debug.selected_recipe).toBe("address_vat_liability_confirmed_tax_period_v1"); expect(result?.debug.extracted_filters.period_from).toBe("2019-10-01"); expect(result?.debug.extracted_filters.period_to).toBe("2019-12-31"); expect(result?.debug.route_expectation_status).toBe("matched"); }); it("routes customer lifecycle question into dedicated aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Какие заказчики работали с нами в 2020 году?"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle"); expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes roster-style customer wording into lifecycle aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("кто у нас заказчики вообще"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle"); expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes one-time counterparties wording into lifecycle aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Какие контрагенты работали с нами только один раз за все время?"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle"); expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes longest-collaboration customer wording into lifecycle aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "Какие заказчики работают с нами дольше всего?" ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle"); expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); if (result?.response_type === "FACTUAL_LIST") { expect(String(result.reply_text)).toContain("лет в базе"); expect(String(result.reply_text)).toContain("Топ-"); } }); it("routes debt-longevity wording into receivables lane with factual reply", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "Сколько заказчиков у нас на этот момент могут считаться долгожителями по своим задолженностям?" ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_receivables_counterparties"); expect(result?.debug.selected_recipe).toBe("address_open_items_by_party_or_contract_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); if (result?.response_type === "FACTUAL_LIST") { expect(String(result.reply_text)).toContain("сроку жизни задолженности"); expect(String(result.reply_text)).toContain("Дата среза"); } }); it("routes stale contracts wording into contract usage overview recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Какие договоры давно не использовались?"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("contract_usage_overview"); expect(result?.debug.selected_recipe).toBe("address_contract_usage_overview_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("routes multi-contract counterparties wording into contract usage and value recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Покажи контрагентов с несколькими договорами и какие из договоров активны."); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("contract_usage_and_value"); expect(result?.debug.selected_recipe).toBe("address_contract_usage_and_value_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type); }); it("allows broad open items scan without forcing missing_anchor", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("show open items by contract"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("open_items_by_counterparty_or_contract"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(result?.debug.limited_reason_category).not.toBe("missing_anchor"); }); it("does not return fallback factual rows for unmatched open-items contract anchor", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Покажи открытые позиции по договору 9999/NOPE"); expect(result?.handled).toBe(true); expect(result?.response_type).toBe("LIMITED_WITH_REASON"); expect(result?.reply_type).toBe("partial_coverage"); expect(result?.debug.detected_intent).toBe("open_items_by_counterparty_or_contract"); expect(result?.debug.rows_matched).toBe(0); expect(["empty_match", "missing_anchor"]).toContain(result?.debug.limited_reason_category); expect(String(result?.assistant_reply ?? "")).not.toContain("Собраны открытые позиции"); }); it("does not return broad fallback document list when counterparty anchor is not matched", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("покажи документы все по жуковке 51"); expect(result?.handled).toBe(true); expect(String(result?.assistant_reply ?? "")).not.toContain("Точный якорь не подтвердился"); expect(String(result?.assistant_reply ?? "")).not.toContain("якорь не подтвердился"); if (result?.reply_type === "partial_coverage") { expect(result?.debug.rows_matched).toBe(0); if (String(result?.debug.match_failure_reason ?? "").includes("counterparty_anchor_not_matched")) { expect(String(result?.assistant_reply ?? "")).toContain("уточните точное имя контрагента"); } } }); it("does not keep report-date phrase inside open-items counterparty anchor", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("покажи хвосты по контрагенту СВК на 2020-12-31"); expect(result?.handled).toBe(true); expect(String(result?.debug.extracted_filters.counterparty ?? "").toLowerCase()).toContain("свк"); expect(String(result?.debug.extracted_filters.counterparty ?? "").toLowerCase()).not.toContain("дата отчетности"); expect(String(result?.debug.anchor_value_raw ?? "").toLowerCase()).not.toContain("дата отчетности"); if (result?.reply_type === "partial_coverage") { expect(result?.debug.limited_reason_category).not.toBe("missing_anchor"); } }); it("routes contract document list intent into address recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("show documents by contract 19/15"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_documents_by_contract"); expect(result?.debug.selected_recipe).toBe("address_documents_by_contract_v1"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); }); it("returns factual confirmed VAT snapshot instead of partial when payable rows are absent", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("скок ндс платить надо на март 2020"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("vat_payable_confirmed_as_of_date"); expect(["FACTUAL_LIST", "FACTUAL_SUMMARY"]).toContain(result?.response_type); expect(result?.reply_type).not.toBe("partial_coverage"); expect(result?.debug.result_mode).toBe("confirmed_balance"); expect(result?.debug.balance_confirmed).toBe(true); }); it("keeps mixed VAT + debt wording in VAT lane (not payables/contracts)", async () => { const service = new AddressQueryService(); const result = await service.tryHandle( "\u0441\u043a\u043e\u043a\u0430 \u043d\u0434\u0441\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017" ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("vat_payable_confirmed_as_of_date"); expect(result?.debug.selected_recipe).toBe("address_vat_payable_confirmed_as_of_date_v1"); expect(result?.debug.result_mode).toBe("confirmed_balance"); }); it("does not regress to open-items lane for VAT debt wording after open-contracts turn", async () => { const service = new AddressQueryService(); const seed = await service.tryHandle("\u043a\u0430\u043a\u0438\u0435 \u0435\u0441\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430 \u043d\u0430 \u043c\u0430\u0440\u0442 2020"); expect(seed?.handled).toBe(true); const result = await service.tryHandle( "\u0441\u043a\u043e\u043a\u0430 \u043d\u0434\u0441\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017", { followupContext: { previous_intent: (seed?.debug.detected_intent as any) ?? "open_contracts_confirmed_as_of_date", previous_filters: seed?.debug.extracted_filters, previous_anchor_type: (seed?.debug.anchor_type as any) ?? "unknown", previous_anchor_value: seed?.debug.anchor_value_resolved ?? seed?.debug.anchor_value_raw ?? null } } ); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("vat_payable_confirmed_as_of_date"); expect(result?.debug.selected_recipe).toBe("address_vat_payable_confirmed_as_of_date_v1"); expect(result?.debug.reasons).not.toContain("open_items_from_followup_context"); }, 35000); it("routes contracts-by-counterparty intent into dedicated catalog recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("покажи договора все по жуковке 51"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("list_contracts_by_counterparty"); expect(result?.debug.selected_recipe).toBe("address_contracts_by_counterparty_v1"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON"]).toContain(result?.response_type); }); it("routes bank operations by contract intent into address recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Покажи банковские операции по договору 19/15"); expect(result?.handled).toBe(true); expect(result?.debug.detected_intent).toBe("bank_operations_by_contract"); expect(result?.debug.selected_recipe).toBe("address_bank_operations_by_contract_v1"); expect(result?.debug.limited_reason_category).not.toBe("unsupported"); expect(result?.debug.mcp_call_status).not.toBe("skipped"); }); it("includes resolver and row-stage diagnostics", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("which documents form balance for account 62 as of 2020-07-31"); expect(result?.handled).toBe(true); expect(["LIMITED_WITH_REASON", "FACTUAL_LIST"]).toContain(result?.response_type); expect(result?.debug.anchor_type).toBe("account"); expect(result?.debug.rows_fetched).toBeTypeOf("number"); expect(result?.debug.raw_rows_received).toBeTypeOf("number"); expect(result?.debug.rows_after_account_scope).toBeTypeOf("number"); expect(result?.debug.rows_materialized).toBeTypeOf("number"); expect(result?.debug.rows_after_recipe_filter).toBeTypeOf("number"); expect(result?.debug.rows_matched).toBeTypeOf("number"); expect(["strict", "preferred"]).toContain(result?.debug.account_scope_mode); expect(result?.debug.account_scope_fallback_applied).toBeTypeOf("boolean"); expect(result?.debug.mcp_call_status_legacy).toBeDefined(); expect(result?.debug.match_failure_stage).toBeDefined(); expect([ "error", "no_raw_rows", "raw_rows_received_but_not_materialized", "materialized_but_not_anchor_matched", "materialized_but_filtered_out_by_recipe", "materialized_but_not_matched", "matched_non_empty" ]).toContain(result?.debug.mcp_call_status); expect(result?.debug.raw_row_keys_sample).toBeDefined(); expect(result?.debug.materialization_drop_reason).toBeDefined(); expect(result?.debug.account_scope_fields_checked).toBeDefined(); expect(result?.debug.account_scope_match_strategy).toBe("account_code_regex_plus_alias_map_v1"); expect(result?.debug.account_scope_drop_reason).toBeDefined(); }); it("keeps short slang docs request in address lane (no deep fallback)", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("какие доки есть по свк за 2021"); expect(result?.handled).toBe(true); expect(result?.debug.detected_mode).toBe("address_query"); expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty"); expect(result?.debug.extracted_filters.counterparty).toBe("свк"); expect(result?.debug.extracted_filters.period_from).toBe("2021-01-01"); expect(result?.debug.extracted_filters.period_to).toBe("2021-12-31"); }); it("keeps typo slang docs request in address lane and extracts implicit anchor", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("свк доки за 20год покеж"); expect(result?.handled).toBe(true); expect(result?.debug.detected_mode).toBe("address_query"); expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty"); expect(result?.debug.extracted_filters.counterparty).toBe("свк"); expect(result?.debug.extracted_filters.period_from).toBe("2020-01-01"); expect(result?.debug.extracted_filters.period_to).toBe("2020-12-31"); }); it("keeps noisy docs request in address lane and ignores slang tail token", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("свк 20 год - покажи доки плс"); expect(result?.handled).toBe(true); expect(result?.debug.detected_mode).toBe("address_query"); expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty"); expect(result?.debug.extracted_filters.counterparty).toBe("свк"); expect(result?.debug.extracted_filters.counterparty).not.toBe("плс"); expect(result?.debug.extracted_filters.period_from).toBe("2020-01-01"); expect(result?.debug.extracted_filters.period_to).toBe("2020-12-31"); }); it("auto-broadens out-of-window period and returns available factual rows", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Какие документы по СВК за 2000 год?"); expect(result?.handled).toBe(true); expect(["FACTUAL_LIST", "LIMITED_WITH_REASON"]).toContain(result?.response_type); if (result?.response_type === "FACTUAL_LIST") { expect(result?.debug.limited_reason_category).toBeNull(); } }); }); describe("address decompose stage follow-up carryover", () => { it("promotes selected-object supplier slang follow-up into inventory provenance with inherited date context", () => { const result = runAddressDecomposeStage('По выбранному объекту "Столешница 600*3050*26 дуб ниагара": кто это поставил нам', { previous_intent: "inventory_on_hand_as_of_date", previous_filters: { as_of_date: "2019-03-31", period_from: "2019-03-01", period_to: "2019-03-31", warehouse: "Основной склад" }, previous_anchor_type: "unknown", previous_anchor_value: null }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("inventory_purchase_provenance_for_item"); expect(result?.filters.extracted_filters.as_of_date).toBe("2019-03-31"); expect( result?.baseReasons?.includes("intent_adjusted_to_inventory_followup_context") || result?.intent.reasons.includes("inventory_selected_object_provenance_signal_detected") ).toBe(true); }); it("promotes selected-object wording 'у кого купили' into inventory provenance with inherited date context", () => { const result = runAddressDecomposeStage( 'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": у кого купили', { previous_intent: "inventory_on_hand_as_of_date", previous_filters: { as_of_date: "2016-07-31", period_from: "2016-07-01", period_to: "2016-07-31", warehouse: "Основной склад" }, previous_anchor_type: "unknown", previous_anchor_value: null } ); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("inventory_purchase_provenance_for_item"); expect(result?.filters.extracted_filters.as_of_date).toBe("2016-07-31"); expect( result?.baseReasons?.includes("intent_adjusted_to_inventory_followup_context") || result?.intent.reasons.includes("inventory_selected_object_provenance_signal_detected") ).toBe(true); }); it("promotes selected-object wording 'где мы купили это' into inventory provenance with inherited date context", () => { const result = runAddressDecomposeStage( 'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": где мы купили это', { previous_intent: "inventory_on_hand_as_of_date", previous_filters: { as_of_date: "2016-05-31", period_from: "2016-05-01", period_to: "2016-05-31", warehouse: "Основной склад" }, previous_anchor_type: "unknown", previous_anchor_value: null } ); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("inventory_purchase_provenance_for_item"); expect(result?.filters.extracted_filters.as_of_date).toBe("2016-05-31"); expect( result?.baseReasons?.includes("intent_adjusted_to_inventory_followup_context") || result?.intent.reasons.includes("inventory_selected_object_provenance_signal_detected") ).toBe(true); }); it("promotes selected-object wording 'где куплено!!' into inventory provenance with inherited date context", () => { const result = runAddressDecomposeStage( 'По выбранному объекту "Рабочая станция универсального специалиста (индивидуальное изготовление)": где куплено!!', { previous_intent: "inventory_on_hand_as_of_date", previous_filters: { as_of_date: "2016-05-31", period_from: "2016-05-01", period_to: "2016-05-31", warehouse: "Основной склад" }, previous_anchor_type: "unknown", previous_anchor_value: null } ); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("inventory_purchase_provenance_for_item"); expect(result?.filters.extracted_filters.as_of_date).toBe("2016-05-31"); expect( result?.baseReasons?.includes("intent_adjusted_to_inventory_followup_context") || result?.intent.reasons.includes("inventory_selected_object_provenance_signal_detected") ).toBe(true); }); it("promotes selected-object purchase-doc slang follow-up into inventory purchase documents with inherited date context", () => { const result = runAddressDecomposeStage('По выбранному объекту "Столешница 600*3050*26 дуб ниагара": по каким документам это купили', { previous_intent: "inventory_purchase_provenance_for_item", previous_filters: { as_of_date: "2019-03-31", period_from: "2019-03-01", period_to: "2019-03-31", item: "Столешница 600*3050*26 дуб ниагара" }, previous_anchor_type: "unknown", previous_anchor_value: null }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("inventory_purchase_documents_for_item"); expect(result?.filters.extracted_filters.item).toBe("Столешница 600*3050*26 дуб ниагара"); expect(result?.filters.extracted_filters.as_of_date).toBe("2019-03-31"); expect( result?.baseReasons?.includes("intent_adjusted_to_inventory_followup_context") || result?.intent.reasons.includes("inventory_selected_object_purchase_documents_signal_detected") ).toBe(true); }); it("promotes pronoun selected-item purchase-doc follow-up into inventory purchase documents with inherited date context", () => { const result = runAddressDecomposeStage('покажи документы по этой позиции', { previous_intent: "inventory_purchase_provenance_for_item", previous_filters: { as_of_date: "2019-03-31", period_from: "2019-03-01", period_to: "2019-03-31", item: "Столешница 600*3050*26 дуб ниагара" }, previous_anchor_type: "unknown", previous_anchor_value: null }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("inventory_purchase_documents_for_item"); expect(result?.filters.extracted_filters.item).toBe("Столешница 600*3050*26 дуб ниагара"); expect(result?.filters.extracted_filters.as_of_date).toBe("2019-03-31"); expect( result?.baseReasons?.includes("intent_adjusted_to_inventory_followup_context") || result?.intent.reasons.includes("inventory_selected_object_purchase_documents_signal_detected") ).toBe(true); }); it("promotes conversational buyer follow-up into inventory sale trace with inherited date context", () => { const result = runAddressDecomposeStage("кому в итоге мы продали этот товар?", { previous_intent: "inventory_purchase_provenance_for_item", previous_filters: { as_of_date: "2020-03-31", period_from: "2020-03-01", period_to: "2020-03-31", item: "Четки Пост (84*117)" }, previous_anchor_type: "unknown", previous_anchor_value: null }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("inventory_sale_trace_for_item"); expect(result?.filters.extracted_filters.item).toBe("Четки Пост (84*117)"); expect(result?.filters.extracted_filters.as_of_date).toBe("2020-03-31"); }); it("keeps slang all-customers-all-time wording in address lane via resolved intent fallback", () => { const result = runAddressDecomposeStage("выведи всех заков за все время", null); expect(result).not.toBeNull(); expect(result?.mode.mode).toBe("address_query"); expect(result?.intent.intent).toBe("counterparty_activity_lifecycle"); }); it("keeps churn wording with year in address lane via resolved intent fallback", () => { const result = runAddressDecomposeStage("кто был активен в 2020 и потом отвалился", null); expect(result).not.toBeNull(); expect(result?.mode.mode).toBe("address_query"); expect(result?.intent.intent).toBe("counterparty_activity_lifecycle"); expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01"); expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31"); }); it("uses short bare year in follow-up period switch", () => { const result = runAddressDecomposeStage("теперь за 21", { previous_intent: "counterparty_activity_lifecycle", previous_filters: { period_from: "2020-01-01", period_to: "2020-12-31" }, previous_anchor_type: "unknown", previous_anchor_value: null }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("counterparty_activity_lifecycle"); expect(result?.filters.extracted_filters.period_from).toBe("2021-01-01"); expect(result?.filters.extracted_filters.period_to).toBe("2021-12-31"); }); it("keeps lifecycle follow-up phrasing with referential pointer and inherits period", () => { const result = runAddressDecomposeStage("А кто из них новые?", { previous_intent: "counterparty_activity_lifecycle", previous_filters: { period_from: "2020-01-01", period_to: "2020-12-31" }, previous_anchor_type: "unknown", previous_anchor_value: null }); expect(result).not.toBeNull(); expect(result?.mode.mode).toBe("address_query"); expect(result?.intent.intent).toBe("counterparty_activity_lifecycle"); expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01"); expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31"); expect(result?.baseReasons).toContain("address_followup_context_applied"); }); it("keeps short period follow-up in address lane and preserves previous counterparty anchor", () => { const result = runAddressDecomposeStage("а теперь только за май 2020", { previous_intent: "list_documents_by_counterparty", previous_filters: { counterparty: "свк", period_from: "2020-01-01", period_to: "2020-12-31" }, previous_anchor_type: "counterparty", previous_anchor_value: "Группа СВК" }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("list_documents_by_counterparty"); expect(result?.filters.extracted_filters.counterparty).toBe("свк"); expect(result?.filters.extracted_filters.period_from).toBe("2020-05-01"); expect(result?.filters.extracted_filters.period_to).toBe("2020-05-31"); expect(result?.baseReasons).toContain("address_followup_context_applied"); }); it("inherits organization scope from follow-up context when organization is omitted in user text", () => { const result = runAddressDecomposeStage("покажи документы по свк за 2020", { previous_intent: "list_documents_by_counterparty", previous_filters: { organization: "ООО Альтернатива Плюс", counterparty: "свк", period_from: "2020-01-01", period_to: "2020-12-31" }, previous_anchor_type: "counterparty", previous_anchor_value: "свк" }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("list_documents_by_counterparty"); expect(result?.filters.extracted_filters.organization).toBe("ООО Альтернатива Плюс"); expect(result?.baseReasons).toContain("organization_from_followup_context"); }); it("inherits as_of_date from previous period for same-date balance follow-up", () => { const result = runAddressDecomposeStage("а по счету 60.01 на ту же дату", { previous_intent: "list_documents_by_counterparty", previous_filters: { counterparty: "свк", period_from: "2020-05-01", period_to: "2020-05-31" }, previous_anchor_type: "counterparty", previous_anchor_value: "Группа СВК" }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("account_balance_snapshot"); expect(result?.filters.extracted_filters.account).toBe("60.01"); expect(result?.filters.extracted_filters.as_of_date).toBe("2020-05-31"); expect(result?.baseReasons).toContain("as_of_date_from_followup_context"); expect(result?.baseReasons).toContain("address_followup_context_applied"); }); it("inherits as_of_date for receivables follow-up without explicit period", () => { const result = runAddressDecomposeStage("\u0430 \u043d\u0430\u043c \u043a\u0442\u043e \u0434\u043e\u043b\u0436\u0435\u043d?.", { previous_intent: "receivables_confirmed_as_of_date", previous_filters: { period_from: "2017-09-01", period_to: "2017-09-30", as_of_date: "2017-09-30" }, previous_anchor_type: "unknown", previous_anchor_value: null }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("receivables_confirmed_as_of_date"); expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01"); expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30"); expect(result?.filters.extracted_filters.as_of_date).toBe("2017-09-30"); expect(result?.baseReasons).toContain("as_of_date_from_followup_context"); expect(result?.baseReasons).toContain("address_followup_context_applied"); }); it("keeps contract scope when follow-up asks for bank operations without explicit anchor", () => { const result = runAddressDecomposeStage("а теперь банковские операции", { previous_intent: "list_documents_by_contract", previous_filters: { contract: "19/15", period_from: "2020-01-01", period_to: "2020-12-31" }, previous_anchor_type: "contract", previous_anchor_value: "19/15" }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("bank_operations_by_contract"); expect(result?.filters.extracted_filters.contract).toBe("19/15"); expect(result?.baseReasons).toContain("intent_adjusted_to_contract_followup_context"); }); it("replaces noisy follow-up contract anchor with previous contract from context", () => { const result = runAddressDecomposeStage("а документы по этому же договору за тот же период", { previous_intent: "bank_operations_by_contract", previous_filters: { contract: "19/15", period_from: "2020-01-01", period_to: "2020-12-31" }, previous_anchor_type: "contract", previous_anchor_value: "19/15" }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("list_documents_by_contract"); expect(result?.filters.extracted_filters.contract).toBe("19/15"); expect( result?.baseReasons?.includes("contract_replaced_from_followup_context") || result?.baseReasons?.includes("contract_from_followup_context") ).toBe(true); }); it("replaces noisy referential counterparty anchor with previous counterparty from context", () => { const result = runAddressDecomposeStage("а теперь документы по нему", { previous_intent: "bank_operations_by_counterparty", previous_filters: { counterparty: "свк", period_from: "2020-11-01", period_to: "2020-11-30" }, previous_anchor_type: "counterparty", previous_anchor_value: "свк" }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("list_documents_by_counterparty"); expect(result?.filters.extracted_filters.counterparty).toBe("свк"); expect( result?.baseReasons?.includes("counterparty_replaced_from_followup_context") || result?.baseReasons?.includes("counterparty_from_followup_context") ).toBe(true); }); it("replaces 'кроме этого документа...' pseudo-anchor with previous counterparty from follow-up context", () => { const result = runAddressDecomposeStage("кроме этого документа есть еще чтото?", { previous_intent: "list_documents_by_counterparty", previous_filters: { counterparty: "ТСЖ \\Жуковка 51\\" }, previous_anchor_type: "counterparty", previous_anchor_value: "ТСЖ \\Жуковка 51\\" }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("list_documents_by_counterparty"); expect(result?.filters.extracted_filters.counterparty).toBe("ТСЖ \\Жуковка 51\\"); expect( result?.baseReasons?.includes("counterparty_replaced_from_followup_context") || result?.baseReasons?.includes("counterparty_from_followup_context") ).toBe(true); }); it("keeps entity carryover for customer value follow-up when counterparty is resolved from displayed list", () => { const result = runAddressDecomposeStage("сколько денег за 2020 принес калинин?", { previous_intent: "counterparty_activity_lifecycle", previous_filters: { period_from: "2020-01-01", period_to: "2020-12-31" }, previous_anchor_type: "counterparty", previous_anchor_value: "�?П Калинин Н.М.", resolved_counterparty_from_display: true }); expect(result).not.toBeNull(); expect(result?.mode.mode).toBe("address_query"); expect(result?.intent.intent).toBe("customer_revenue_and_payments"); expect(result?.filters.extracted_filters.counterparty).toBe("�?П Калинин Н.М."); expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01"); expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31"); expect( result?.baseReasons?.includes("counterparty_replaced_from_followup_context") || result?.baseReasons?.includes("counterparty_from_followup_context") ).toBe(true); }); it("promotes open-items intent from follow-up wording with inherited contract anchor", () => { const result = runAddressDecomposeStage("а теперь открытые позиции по нему", { previous_intent: "bank_operations_by_contract", previous_filters: { contract: "19/15", period_from: "2020-01-01", period_to: "2020-12-31" }, previous_anchor_type: "contract", previous_anchor_value: "19/15" }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("open_items_by_counterparty_or_contract"); expect(result?.filters.extracted_filters.contract).toBe("19/15"); expect(result?.baseReasons).toContain("open_items_from_followup_context"); }); it("derives as-of date from period for open-contract month query", () => { const result = runAddressDecomposeStage("какие есть открытые договора на март 2020", null); expect(result).not.toBeNull(); expect(result?.mode.mode).toBe("address_query"); expect(result?.intent.intent).toBe("open_contracts_confirmed_as_of_date"); expect(result?.filters.extracted_filters.period_from).toBe("2020-03-01"); expect(result?.filters.extracted_filters.period_to).toBe("2020-03-31"); expect(result?.filters.extracted_filters.as_of_date).toBe("2020-03-31"); expect(result?.baseReasons).toContain("as_of_date_derived_from_period_for_open_contracts"); }); it("keeps VAT debt follow-up in VAT intent even after open-contract context", () => { const result = runAddressDecomposeStage("\u0441\u043a\u043e\u043a\u0430 \u043d\u0434\u0441\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017", { previous_intent: "open_contracts_confirmed_as_of_date", previous_filters: { period_from: "2020-03-01", period_to: "2020-03-31" }, previous_anchor_type: "counterparty", previous_anchor_value: "ООО Ромашка" }); expect(result).not.toBeNull(); expect(result?.mode.mode).toBe("address_query"); expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date"); expect(result?.filters.extracted_filters.as_of_date).toBe("2017-09-30"); expect(result?.baseReasons).not.toContain("open_items_from_followup_context"); }); it("keeps balance family in follow-up when user gives compact account token", () => { const result = runAddressDecomposeStage("вернись на 2020-12-31 по 60", { previous_intent: "documents_forming_balance", previous_filters: { account: "62", as_of_date: "2020-05-31", period_from: "2020-05-01", period_to: "2020-05-31" }, previous_anchor_type: "account", previous_anchor_value: "62" }); expect(result).not.toBeNull(); expect(result?.intent.intent).toBe("account_balance_snapshot"); expect(result?.filters.extracted_filters.account).toBe("60"); expect(result?.filters.extracted_filters.as_of_date).toBe("2020-12-31"); expect(result?.baseReasons).toContain("intent_adjusted_to_balance_followup_context"); }); it("does not downgrade inherited follow-up anchor to missing_anchor when period has no rows", async () => { const service = new AddressQueryService(); const seed = await service.tryHandle("покажи документы по свк за 2020"); expect(seed?.handled).toBe(true); const followup = await service.tryHandle("а теперь только за май 2020", { followupContext: { previous_intent: (seed?.debug.detected_intent as any) ?? "list_documents_by_counterparty", previous_filters: seed?.debug.extracted_filters, previous_anchor_type: (seed?.debug.anchor_type as any) ?? "counterparty", previous_anchor_value: seed?.debug.anchor_value_resolved ?? seed?.debug.anchor_value_raw ?? null } }); expect(followup?.handled).toBe(true); if (followup?.reply_type === "partial_coverage") { expect(followup?.debug.limited_reason_category).not.toBe("missing_anchor"); } }); it("keeps VAT explain follow-up in address lane and inherits previous period window", () => { const result = runAddressDecomposeStage("почему прогноз к уплате 0?", { previous_intent: "vat_payable_forecast", previous_filters: { period_from: "2020-03-01", period_to: "2020-03-31" }, previous_anchor_type: "unknown", previous_anchor_value: null }); expect(result).not.toBeNull(); expect(result?.mode.mode).toBe("address_query"); expect(result?.intent.intent).toBe("vat_payable_forecast"); expect(result?.filters.extracted_filters.period_from).toBe("2020-03-01"); expect(result?.filters.extracted_filters.period_to).toBe("2020-03-31"); expect( result?.baseReasons?.includes("address_mode_from_followup_context") || result?.baseReasons?.includes("intent_from_followup_context") ).toBe(true); }); it("promotes short 'а ндс?' follow-up to confirmed VAT intent with inherited as-of date", () => { const result = runAddressDecomposeStage("\u0430 \u043d\u0434\u0441?", { previous_intent: "payables_confirmed_as_of_date", previous_filters: { period_from: "2017-09-01", period_to: "2017-09-30", as_of_date: "2017-09-30" }, previous_anchor_type: "unknown", previous_anchor_value: null }); expect(result).not.toBeNull(); expect(result?.mode.mode).toBe("address_query"); expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date"); expect(result?.filters.extracted_filters.as_of_date).toBe("2017-09-30"); expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01"); expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30"); expect(result?.baseReasons).toContain("intent_adjusted_to_vat_followup_context"); expect(result?.baseReasons).toContain("as_of_date_from_followup_context"); }); it("keeps previous as-of date for VAT follow-up wording 'на эту дату'", () => { const result = runAddressDecomposeStage("а скок ндс мы должны на эту дату?", { previous_intent: "receivables_confirmed_as_of_date", previous_filters: { period_from: "2020-03-01", period_to: "2020-03-31", as_of_date: "2020-03-31" }, previous_anchor_type: "unknown", previous_anchor_value: null }); expect(result).not.toBeNull(); expect(result?.mode.mode).toBe("address_query"); expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date"); expect(result?.filters.extracted_filters.as_of_date).toBe("2020-03-31"); expect(result?.filters.extracted_filters.period_from).toBe("2020-03-01"); expect(result?.filters.extracted_filters.period_to).toBe("2020-03-31"); expect(result?.baseReasons).toContain("as_of_date_from_followup_context"); expect(result?.baseReasons).toContain("period_from_followup_context"); }); it("keeps explicit current-date VAT follow-up and does not inherit stale as-of date", () => { const result = runAddressDecomposeStage("а на текущую дату", { previous_intent: "vat_payable_confirmed_as_of_date", previous_filters: { period_from: "2016-03-01", period_to: "2016-03-31", as_of_date: "2016-03-31" }, previous_anchor_type: "unknown", previous_anchor_value: null }); const todayIso = new Date().toISOString().slice(0, 10); expect(result).not.toBeNull(); expect(result?.mode.mode).toBe("address_query"); expect(result?.intent.intent).toBe("vat_payable_confirmed_as_of_date"); expect(result?.filters.extracted_filters.as_of_date).toBe(todayIso); expect(result?.filters.extracted_filters.as_of_date).not.toBe("2016-03-31"); expect(result?.filters.extracted_filters.period_from).toBeUndefined(); expect(result?.filters.extracted_filters.period_to).toBeUndefined(); }); }); describe("address recipe catalog counterparty filtering", () => { it("selects period coverage profile recipe and keeps aggregate markers", () => { const selected = selectAddressRecipe("period_coverage_profile", {}); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, {}); expect(plan.recipe.recipe_id).toBe("address_period_coverage_profile_v1"); expect(plan.limit).toBeGreaterThanOrEqual(600); expect(plan.query).toContain("MIN_DATE"); expect(plan.query).toContain("YEAR_DOCS"); expect(plan.query).toContain("MONTH_OPS"); expect(plan.query).not.toContain("ТЕКУЩАЯДАТА()"); }); it("selects document+section profile recipe and keeps aggregate markers", () => { const selected = selectAddressRecipe("document_type_and_account_section_profile", {}); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, {}); expect(plan.recipe.recipe_id).toBe("address_document_type_and_account_section_profile_v1"); expect(plan.limit).toBeGreaterThanOrEqual(800); expect(plan.query).toContain("DOC_TYPE_DOCS"); expect(plan.query).toContain("SECTION_DT_OPS"); expect(plan.query).toContain("SECTION_KT_OPS"); expect(plan.query).toContain("СГРУППИРОВАТЬ ПО\n Движения.СчетДт"); expect(plan.query).not.toContain("ЛЕВ(Движения.СчетДт.Код, 2)"); }); it("selects counterparty population recipe and keeps aggregate markers", () => { const selected = selectAddressRecipe("counterparty_population_and_roles", {}); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, {}); expect(plan.recipe.recipe_id).toBe("address_counterparty_population_roles_v1"); expect(plan.query).toContain("CP_TOTAL"); expect(plan.query).toContain("CP_CUSTOMER_ACTIVE"); expect(plan.query).toContain("CP_SUPPLIER_ACTIVE"); expect(plan.query).toContain("CP_MIXED_ACTIVE"); expect(plan.query).toContain("CP_ACTIVE_UNION"); }); it("selects contract usage overview recipe and keeps aggregate markers", () => { const selected = selectAddressRecipe("contract_usage_overview", {}); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, {}); expect(plan.recipe.recipe_id).toBe("address_contract_usage_overview_v1"); expect(plan.query).toContain("CT_TOTAL"); expect(plan.query).toContain("CT_USED"); expect(plan.query).toContain("ДоговорКонтрагента"); }); it("keeps recipe-default limits for inventory exact intents", () => { const onHand = extractAddressFilters("Какие товары сейчас лежат на складе?", "inventory_on_hand_as_of_date"); const provenance = extractAddressFilters( "От какого поставщика куплен товар Диван трехместный из текущего остатка на складе Основной склад?", "inventory_purchase_provenance_for_item" ); const purchaseDocs = extractAddressFilters( "По каким документам был куплен товар Диван трехместный для остатка на складе Основной склад?", "inventory_purchase_documents_for_item" ); const overlap = extractAddressFilters( "Какие товары от поставщика Гамма-мебель, ООО сейчас еще лежат на складе Основной склад?", "inventory_supplier_stock_overlap_as_of_date" ); const saleTrace = extractAddressFilters("Кому был продан товар Шкаф картотечный 1000*400*2100?", "inventory_sale_trace_for_item"); const chain = extractAddressFilters( "Через какие документы прошел путь товара Шкаф картотечный 1000*400*2100: закупка -> склад -> продажа?", "inventory_purchase_to_sale_chain" ); const aging = extractAddressFilters( "Есть ли среди текущих остатков на складе Основной склад позиции, закупленные очень давно?", "inventory_aging_by_purchase_date" ); expect(onHand.extracted_filters.limit).toBeUndefined(); expect(provenance.extracted_filters.limit).toBeUndefined(); expect(purchaseDocs.extracted_filters.limit).toBeUndefined(); expect(overlap.extracted_filters.limit).toBeUndefined(); expect(saleTrace.extracted_filters.limit).toBeUndefined(); expect(chain.extracted_filters.limit).toBeUndefined(); expect(aging.extracted_filters.limit).toBeUndefined(); }); it("selects customer value recipe and keeps top-20 default", () => { const selected = selectAddressRecipe("customer_revenue_and_payments", {}); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, {}); expect(plan.recipe.recipe_id).toBe("address_customer_revenue_and_payments_v1"); expect(plan.limit).toBe(20); expect(plan.query).toContain("ПоступлениеНаРасчетныйСчет"); expect(plan.query).toContain("БанкПоступление.ДоговорКонтрагента"); }); it("selects supplier payouts recipe and keeps top-20 default", () => { const selected = selectAddressRecipe("supplier_payouts_profile", {}); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, {}); expect(plan.recipe.recipe_id).toBe("address_supplier_payouts_profile_v1"); expect(plan.limit).toBe(20); expect(plan.query).toContain("СписаниеСРасчетногоСчета"); expect(plan.query).toContain("БанкСписание.ДоговорКонтрагента"); }); it("selects contract value recipe and keeps top-20 default", () => { const selected = selectAddressRecipe("contract_usage_and_value", {}); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, {}); expect(plan.recipe.recipe_id).toBe("address_contract_usage_and_value_v1"); expect(plan.limit).toBe(20); expect(plan.query).toContain("CT_VALUE_IN"); expect(plan.query).toContain("CT_VALUE_OUT"); expect(plan.query).toContain("ДоговорКонтрагента"); }); it("selects contracts-by-counterparty recipe from contract catalog", () => { const selected = selectAddressRecipe("list_contracts_by_counterparty", { counterparty: "Жуковка 51" }); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, { counterparty: "Жуковка 51" }); expect(plan.recipe.recipe_id).toBe("address_contracts_by_counterparty_v1"); expect(plan.query).toContain("Справочник.ДоговорыКонтрагентов"); expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Договоры.Владелец)"); }); it("selects counterparty lifecycle recipe and keeps activity marker", () => { const selected = selectAddressRecipe("counterparty_activity_lifecycle", {}); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, {}); expect(plan.recipe.recipe_id).toBe("address_counterparty_activity_lifecycle_v1"); expect(plan.query).toContain("CP_CUSTOMER_ACTIVITY"); expect(plan.query).toContain("ПоступлениеНаРасчетныйСчет"); }); it("boosts limit for all-time counterparty queries", () => { const filters = extractAddressFilters( "Покажи документы по контрагенту тестовый за все время", "list_documents_by_counterparty" ).extracted_filters; const selected = selectAddressRecipe("list_documents_by_counterparty", filters); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, filters); expect(plan.limit).toBe(1000); }); it("supports ascending order plan for historical counterparty lookup", () => { const selected = selectAddressRecipe("list_documents_by_counterparty", { counterparty: "Жуковка 51", sort: "period_asc" }); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, { counterparty: "Жуковка 51", sort: "period_asc" }); expect(plan.query).toContain("УПОРЯДОЧИТЬ ПО"); expect(plan.query).toContain("Период ВОЗР"); }); it("boosts limit for english all-time counterparty queries", () => { const filters = extractAddressFilters( "show documents by counterparty test_cp for all time", "list_documents_by_counterparty" ).extracted_filters; const selected = selectAddressRecipe("list_documents_by_counterparty", filters); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, filters); expect(plan.limit).toBe(1000); }); it("cuts english all-time tail from counterparty anchor", () => { const result = extractAddressFilters( "show documents by counterparty test_cp for all time", "list_documents_by_counterparty" ); expect(result.extracted_filters.counterparty).toBe("test_cp"); expect(result.extracted_filters.period_from).toBeUndefined(); expect(result.extracted_filters.period_to).toBeUndefined(); expect(result.warnings).not.toContain("period_defaulted_last_90_days"); }); it("boosts limit for account snapshot queries with explicit account", () => { const filters = extractAddressFilters( "Какой остаток по счету 60 на дату 2020-07-31", "account_balance_snapshot" ).extracted_filters; const selected = selectAddressRecipe("account_balance_snapshot", filters); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, filters); expect(plan.limit).toBe(200); }); it("allows extended limit for open-items by contract intent", () => { const selected = selectAddressRecipe("open_items_by_counterparty_or_contract", { contract: "19/15", limit: 1000 }); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, { contract: "19/15", limit: 1000 }); expect(plan.limit).toBe(1000); }); it("uses bank-doc profile with contract projection for open-items anchor matching", () => { const selected = selectAddressRecipe("open_items_by_counterparty_or_contract", { counterparty: "СВК", as_of_date: "2020-12-31" }); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, { counterparty: "СВК", as_of_date: "2020-12-31" }); expect(plan.query).toContain("Документ.СписаниеСРасчетногоСчета"); expect(plan.query).toContain("Документ.ПоступлениеНаРасчетныйСчет"); expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор"); }); it("allows extended limit for confirmed open-contracts intent", () => { const selected = selectAddressRecipe("open_contracts_confirmed_as_of_date", { as_of_date: "2020-12-31", limit: 1000 }); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, { as_of_date: "2020-12-31", limit: 1000 }); expect(plan.limit).toBe(1000); }); it("builds exact balance query for confirmed open-contracts snapshot", () => { const selected = selectAddressRecipe("open_contracts_confirmed_as_of_date", { as_of_date: "2020-12-31" }); expect(selected.selected_recipe?.recipe_id).toBe("address_open_contracts_confirmed_as_of_date_v1"); const plan = buildAddressRecipePlan(selected.selected_recipe!, { as_of_date: "2020-12-31" }); expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.Остатки("); expect(plan.query).toContain("СуммаРазвернутыйОстатокДт"); expect(plan.query).toContain("СуммаРазвернутыйОстатокКт"); expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Остатки.Счет.Код, \"\"), 1, 2) = \"60\""); }); it("injects account condition into movements query for account snapshot", () => { const filters = extractAddressFilters( "Какой остаток по счету 60 на дату 2020-07-31", "account_balance_snapshot" ).extracted_filters; const selected = selectAddressRecipe("account_balance_snapshot", filters); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, filters); expect(plan.query).toContain("Движения.СчетДт.Код"); expect(plan.query).toContain("ПОДОБНО \"60%\""); }); it("injects subaccount condition variants into movements query for documents_forming_balance", () => { const filters = extractAddressFilters( "Какие документы формируют остаток по счету 60.01 на дату 2020-07-31", "documents_forming_balance" ).extracted_filters; const selected = selectAddressRecipe("documents_forming_balance", filters); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, filters); expect(plan.query).toContain("ПОДОБНО \"60.01%\""); expect(plan.query).toContain("ПОДОБНО \"60.1%\""); }); it("builds VAT forecast query with safe account-prefix checks instead of presentation-like clauses", () => { const filters = extractAddressFilters( "мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года", "vat_payable_forecast" ).extracted_filters; const selected = selectAddressRecipe("vat_payable_forecast", filters); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, filters); expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, \"\"), 1, 5) = \"68.02\""); expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 5) = \"68.02\""); expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, \"\"), 1, 4) = \"68.2\""); expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 4) = \"68.2\""); expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 2) = \"19\""); expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, \"\"), 1, 2) = \"19\""); expect(plan.query).not.toContain("ПРЕДСТАВЛЕНИЕ(Движения.СчетКт) ПОДОБНО"); expect(plan.query).not.toContain("ПРЕДСТАВЛЕНИЕ(Движения.СчетДт) ПОДОБНО"); }); it("builds confirmed VAT tax-period query from sales and purchase VAT books", () => { const filters = extractAddressFilters( "сколько ндс надо заплатить в налоговую за декабрь 2019", "vat_liability_confirmed_for_tax_period" ).extracted_filters; const selected = selectAddressRecipe("vat_liability_confirmed_for_tax_period", filters); expect(selected.selected_recipe).toBeTruthy(); const plan = buildAddressRecipePlan(selected.selected_recipe!, filters); expect(plan.query).toContain("РегистрНакопления.НДСЗаписиКнигиПродаж"); expect(plan.query).toContain("РегистрНакопления.НДСЗаписиКнигиПокупок"); expect(plan.query).toContain("VAT_BOOK_SALES"); expect(plan.query).toContain("VAT_BOOK_PURCHASES"); }); it("keeps inventory-on-hand phrasing in address lane", () => { const result = detectAddressQuestionMode("Какие товары сейчас лежат на складе"); expect(result.mode).toBe("address_query"); }); it("detects exact inventory-on-hand intent", () => { const result = resolveAddressIntent("Какие товары сейчас лежат на складе"); expect(result.intent).toBe("inventory_on_hand_as_of_date"); expect(result.confidence).toBe("high"); }); it("detects colloquial warehouse snapshot wording as inventory-on-hand intent", () => { const result = resolveAddressIntent("что у нас на складе на март 2017"); expect(result.intent).toBe("inventory_on_hand_as_of_date"); expect(result.confidence).toBe("high"); }); it("routes account 41 composition wording into inventory snapshot intent", () => { const result = resolveAddressIntent("Из каких товаров состоит остаток по 41 счету"); expect(result.intent).toBe("inventory_on_hand_as_of_date"); }); it("routes account 41 date snapshot wording into inventory snapshot intent", () => { const result = resolveAddressIntent("Какие товары числятся на 41 счете на дату 2020-03-31"); expect(result.intent).toBe("inventory_on_hand_as_of_date"); }); it("routes supplier stock overlap wording into overlap intent", () => { const result = resolveAddressIntent("Какие товары от поставщика Гамма-мебель, ООО сейчас еще лежат на складе Основной склад"); expect(result.intent).toBe("inventory_supplier_stock_overlap_as_of_date"); }); it("routes supplier to buyer chain wording into purchase-to-sale chain intent", () => { const result = resolveAddressIntent( "Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы" ); expect(result.intent).toBe("inventory_purchase_to_sale_chain"); }); it("routes supplier-to-buyer inventory chains as exact trace intents", () => { const result = resolveAddressIntent( "Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы" ); expect(result.intent).toBe("inventory_purchase_to_sale_chain"); expect(result.confidence).toBe("medium"); }); it("routes old purchase residue questions to aging-by-purchase-date", () => { const result = resolveAddressIntent( "Относится ли товар Шкаф картотечный 1000*400*2100 в остатке на дату 2020-03-31 к старым закупкам" ); expect(result.intent).toBe("inventory_aging_by_purchase_date"); expect(result.confidence).toBe("high"); }); it("derives as_of_date for inventory-on-hand from explicit month window", () => { const filters = extractAddressFilters( "Какие товары лежат на складе на март 2020", "inventory_on_hand_as_of_date" ).extracted_filters; expect(filters.period_from).toBe("2020-03-01"); expect(filters.period_to).toBe("2020-03-31"); expect(filters.as_of_date).toBe("2020-03-31"); }); it("builds exact balance query for inventory-on-hand snapshot", () => { const selected = selectAddressRecipe("inventory_on_hand_as_of_date", { as_of_date: "2020-03-31" }); expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_on_hand_as_of_date_v1"); const plan = buildAddressRecipePlan(selected.selected_recipe!, { as_of_date: "2020-03-31" }); expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.Остатки("); expect(plan.query).toContain("КоличествоРазвернутыйОстатокДт"); expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Остатки.Счет.Код, \"\"), 1, 5) = \"41.01\""); }); it("renders confirmed inventory-on-hand snapshot from normalized rows", () => { const reply = composeFactualReply( "inventory_on_hand_as_of_date", [ { period: "2020-03-31T23:59:59Z", registrator: "Остатки на дату", account_dt: "41.01", account_kt: "", amount: 712500, analytics: ["Шкаф картотечный", "Основной склад", "ООО Ромашка"], item: "Шкаф картотечный", warehouse: "Основной склад", organization: "ООО Ромашка", quantity: 15 } ], { asOfDate: "2020-03-31", useRubCurrency: true } ); expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.text.split("\n")[0]).toContain("На 31.03.2020 на складе подтверждено"); expect(reply.text).toContain("Контур: остатки по счету 41.01"); expect(reply.text).not.toContain("Блок 1"); expect(reply.text).toContain("Шкаф картотечный"); expect(reply.text).toContain("Основной склад"); expect(reply.semantics?.result_mode).toBe("confirmed_balance"); expect(reply.semantics?.balance_confirmed).toBe(true); }); it("routes inventory provenance questions to a dedicated intent", () => { const result = resolveAddressIntent("От какого поставщика куплен товар Шкаф картоотечный?"); expect(result.intent).toBe("inventory_purchase_provenance_for_item"); }); it("keeps direct item supplier questions in provenance intent even with current-stock tail", () => { const result = resolveAddressIntent( "От какого поставщика куплен товар Диван трехместный из текущего остатка на складе Основной склад?" ); expect(result.intent).toBe("inventory_purchase_provenance_for_item"); }); it("keeps inventory supplier overlap questions out of on-hand routing", () => { const result = resolveAddressIntent("Какие товары от поставщика Альфа сейчас лежат на складе?"); expect(result.intent).toBe("inventory_supplier_stock_overlap_as_of_date"); }); it("routes inventory purchase document questions to a dedicated intent", () => { const result = resolveAddressIntent("По каким документам был куплен товар Шкаф картоотечный?"); expect(result.intent).toBe("inventory_purchase_documents_for_item"); }); it("routes inventory sale chain questions to a dedicated intent", () => { const result = resolveAddressIntent( "Через какие документы прошел путь товара Шкаф картоотечный: закупка -> склад -> продажа?" ); expect(result.intent).toBe("inventory_purchase_to_sale_chain"); }); it("routes conversational buyer wording to inventory sale trace intent", () => { const result = resolveAddressIntent("Кому в итоге мы продали товар Шкаф картоотечный?"); expect(result.intent).toBe("inventory_sale_trace_for_item"); }); it("keeps inventory provenance wording out of inventory-on-hand routing", () => { const result = resolveAddressIntent("От кого куплен товар Шкаф картоотечный и когда был куплен?"); expect(result.intent).toBe("inventory_purchase_provenance_for_item"); }); it("keeps aging wording out of open-items and bank routing", () => { const result = resolveAddressIntent("Какие товары были куплены очень давно и до сих пор лежат на складе?"); expect(result.intent).toBe("inventory_aging_by_purchase_date"); }); it("keeps very old purchase wording out of on-hand routing", () => { const result = resolveAddressIntent("Относится ли товар к закупленным задолго до даты остатка на складе?"); expect(result.intent).toBe("inventory_aging_by_purchase_date"); }); it("routes old stock wording with residue anchor to aging intent", () => { const result = resolveAddressIntent("Это остаток по очень давно купленному товару?"); expect(result.intent).toBe("inventory_aging_by_purchase_date"); }); it("routes purchase-document trace wording to dedicated inventory intent", () => { const result = resolveAddressIntent("Какими документами был куплен товар Шкаф картоотечный?"); expect(result.intent).toBe("inventory_purchase_documents_for_item"); }); it("keeps very old stock wording in dedicated aging intent", () => { const result = resolveAddressIntent("Есть ли остатки товара, которые закупались очень давно?"); expect(result.intent).toBe("inventory_aging_by_purchase_date"); }); it("routes residue wording with explicit cut-off date into aging intent", () => { const result = resolveAddressIntent( "Есть ли среди текущих остатков на складе Основной склад позиции, закупленные задолго до 2020-03-31?" ); expect(result.intent).toBe("inventory_aging_by_purchase_date"); }); it("keeps unresolved stock provenance wording out of open-items routing", () => { const result = resolveAddressIntent("Какие товары сейчас висят в остатке без понятной привязки к поставщику?"); expect(result.intent).toBe("inventory_supplier_stock_overlap_as_of_date"); }); it("routes documentary supplier-to-buyer chain wording into inventory chain intent", () => { const result = resolveAddressIntent( "Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы?" ); expect(result.intent).toBe("inventory_purchase_to_sale_chain"); }); it("routes explicit supplier-item-buyer chain wording into inventory chain intent", () => { const result = resolveAddressIntent("supplier -> item -> buyer"); expect(result.intent).toBe("inventory_purchase_to_sale_chain"); }); it("routes documented supplier-item-buyer chain wording into inventory chain intent", () => { const result = resolveAddressIntent("Документально подтвержденная цепочка: поставщик -> товар -> покупатель"); expect(result.intent).toBe("inventory_purchase_to_sale_chain"); }); });