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"); }); }); 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 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 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("За период 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("Срок сдачи декларации: до 25.04.2020."); expect(reply.text).toContain("Сроки уплаты: 28.04.2020, 28.05.2020, 28.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: "прогноз НДС на 31 декабря 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, 28.02.2021, 28.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):"); }); }); 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("list_open_contracts"); }); 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("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 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("resolves colloquial VAT payable estimate wording without explicit 'прогноз'", () => { 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 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"); }); }); 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("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 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("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.includes("counterparty_anchor_derived_from_leading_token") || result.warnings.includes("counterparty_anchor_derived_from_free_text_heuristic") ).toBe(true); }); 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 free-text 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_free_text_heuristic"); 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_free_text_heuristic" || warning === "counterparty_anchor_derived_from_implicit_phrase" ) ).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", () => { 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 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.extracted_filters.counterparty).toBeUndefined(); }); it("routes colloquial VAT payable estimate wording 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"); }); 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 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("returns missing_anchor for open items without concrete counterparty/contract anchor", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("show open items by contract"); expect(result?.handled).toBe(true); expect(result?.response_type).toBe("LIMITED_WITH_REASON"); expect(result?.debug.limited_reason_category).toBe("missing_anchor"); expect(result?.debug.mcp_call_status).toBe("skipped"); }); 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("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("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("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("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("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); }); }); 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("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 open-contracts intent", () => { const selected = selectAddressRecipe("list_open_contracts", { 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("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("ПРЕДСТАВЛЕНИЕ(Движения.СчетДт) ПОДОБНО"); }); });