NODEDC_1C/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts

5649 lines
268 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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