5723 lines
272 KiB
TypeScript
5723 lines
272 KiB
TypeScript
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("Заказчики с ролью покупателя: 122.");
|
||
expect(reply.text).toContain("Поставщики с ролью поставщика: 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("Поставщиков с ролью поставщика: 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("Заказчиков с ролью покупателя: 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. Клиент Б | максимальная разовая сумма: 1.200,00 ₽");
|
||
});
|
||
|
||
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-источников в 1С");
|
||
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",
|
||
organizationHint: "ООО Альтернатива Плюс",
|
||
useRubCurrency: true
|
||
}
|
||
);
|
||
|
||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||
expect(reply.text).toContain("Коротко: подтвержденный НДС к уплате за налоговый период по организации ООО Альтернатива Плюс");
|
||
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. Проверка VAT-источников в 1С");
|
||
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("Дополнительная проверка 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("routes organization-level top-year revenue wording to exact value-flow ranking", () => {
|
||
const result = resolveAddressIntent(
|
||
"\u043a\u0430\u043a\u0438\u0435 \u0441\u0430\u043c\u044b\u0435 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0435 \u0433\u043e\u0434\u0430 \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u044b"
|
||
);
|
||
expect(result.intent).toBe("customer_revenue_and_payments");
|
||
expect(result.reasons).toContain("unicode_customer_revenue_ranking_bridge_signal_detected");
|
||
});
|
||
|
||
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("Коротко: на 31.05.2020 мы должны");
|
||
expect(reply).toContain("Крупнейшие позиции к оплате");
|
||
expect(reply).toContain("Основа: подтвержденный остаток по счетам 60/76");
|
||
expect(reply.length).toBeLessThan(1800);
|
||
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("routes organization-level yearly revenue ranking wording into 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("customer_revenue_and_payments");
|
||
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
|
||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
|
||
});
|
||
|
||
it("routes typo highest-check wording into customer value aggregate recipe", async () => {
|
||
const service = new AddressQueryService();
|
||
const result = await service.tryHandle("с каких кликентов самый высокий чек");
|
||
expect(result?.handled).toBe(true);
|
||
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
|
||
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
|
||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
|
||
});
|
||
|
||
it("routes top counterparty slang wording into customer value aggregate recipe", async () => {
|
||
const service = new AddressQueryService();
|
||
const result = await service.tryHandle("какой самый жирный контрагент у нее? кто больше платит денег");
|
||
expect(result?.handled).toBe(true);
|
||
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
|
||
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
|
||
expect(result?.debug.extracted_filters.counterparty).toBeUndefined();
|
||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
|
||
});
|
||
|
||
it("routes supplier payout question into dedicated aggregate recipe", async () => {
|
||
const service = new AddressQueryService();
|
||
const result = await service.tryHandle("кому мы больше всего сгрузили денег, топ-20 поставщиков");
|
||
expect(result?.handled).toBe(true);
|
||
expect(result?.debug.detected_intent).toBe("supplier_payouts_profile");
|
||
expect(result?.debug.selected_recipe).toBe("address_supplier_payouts_profile_v1");
|
||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
|
||
});
|
||
|
||
it("routes contract value question into dedicated aggregate recipe", async () => {
|
||
const service = new AddressQueryService();
|
||
const result = await service.tryHandle("договоры по обороту ранкни и дай топ-20");
|
||
expect(result?.handled).toBe(true);
|
||
expect(result?.debug.detected_intent).toBe("contract_usage_and_value");
|
||
expect(result?.debug.selected_recipe).toBe("address_contract_usage_and_value_v1");
|
||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
|
||
});
|
||
|
||
it("routes top contract wording with 'контракт' into contract value aggregate recipe", async () => {
|
||
const service = new AddressQueryService();
|
||
const result = await service.tryHandle("по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?");
|
||
expect(result?.handled).toBe(true);
|
||
expect(result?.debug.detected_intent).toBe("contract_usage_and_value");
|
||
expect(result?.debug.selected_recipe).toBe("address_contract_usage_and_value_v1");
|
||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||
expect(result?.debug.mcp_call_status).not.toBe("skipped");
|
||
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
|
||
});
|
||
|
||
it("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 expanded top-200 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(200);
|
||
expect(plan.query).toContain("ПоступлениеНаРасчетныйСчет");
|
||
expect(plan.query).toContain("БанкПоступление.ДоговорКонтрагента");
|
||
});
|
||
|
||
it("injects counterparty condition into customer value recipe", () => {
|
||
const selected = selectAddressRecipe("customer_revenue_and_payments", {
|
||
counterparty: "Группа СВК",
|
||
period_from: "2020-01-01",
|
||
period_to: "2020-12-31"
|
||
});
|
||
expect(selected.selected_recipe).toBeTruthy();
|
||
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||
counterparty: "Группа СВК",
|
||
period_from: "2020-01-01",
|
||
period_to: "2020-12-31"
|
||
});
|
||
|
||
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 expanded top-200 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(200);
|
||
expect(plan.query).toContain("СписаниеСРасчетногоСчета");
|
||
expect(plan.query).toContain("БанкСписание.ДоговорКонтрагента");
|
||
});
|
||
|
||
it("injects counterparty condition into supplier payout recipe", () => {
|
||
const selected = selectAddressRecipe("supplier_payouts_profile", {
|
||
counterparty: "Группа СВК",
|
||
period_from: "2020-01-01",
|
||
period_to: "2020-12-31"
|
||
});
|
||
expect(selected.selected_recipe).toBeTruthy();
|
||
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||
counterparty: "Группа СВК",
|
||
period_from: "2020-01-01",
|
||
period_to: "2020-12-31"
|
||
});
|
||
|
||
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("injects counterparty condition into lifecycle and bank document recipes", () => {
|
||
const lifecycle = selectAddressRecipe("counterparty_activity_lifecycle", {
|
||
counterparty: "Группа СВК"
|
||
});
|
||
const bankDocs = selectAddressRecipe("bank_operations_by_counterparty", {
|
||
counterparty: "Группа СВК"
|
||
});
|
||
expect(lifecycle.selected_recipe).toBeTruthy();
|
||
expect(bankDocs.selected_recipe).toBeTruthy();
|
||
const lifecyclePlan = buildAddressRecipePlan(lifecycle.selected_recipe!, {
|
||
counterparty: "Группа СВК"
|
||
});
|
||
const bankDocsPlan = buildAddressRecipePlan(bankDocs.selected_recipe!, {
|
||
counterparty: "Группа СВК"
|
||
});
|
||
|
||
expect(lifecyclePlan.query).toContain('БанкПоступление.Контрагент.Наименование ПОДОБНО "%СВК%"');
|
||
expect(bankDocsPlan.query).toContain('БанкПоступление.Контрагент.Наименование ПОДОБНО "%СВК%"');
|
||
expect(bankDocsPlan.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("builds debt due-date aging query without carrying noisy organization suffix", () => {
|
||
const selected = selectAddressRecipe("debt_due_date_aging_for_organization", {
|
||
as_of_date: "2020-12-31",
|
||
organization: "ООО Альтернатива Плюс на конец 2020 можно точно понять"
|
||
});
|
||
expect(selected.selected_recipe?.recipe_id).toBe("address_debt_due_date_aging_for_organization_v1");
|
||
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||
as_of_date: "2020-12-31",
|
||
organization: "ООО Альтернатива Плюс на конец 2020 можно точно понять"
|
||
});
|
||
|
||
expect(plan.query).toContain("УстановленСрокОплаты");
|
||
expect(plan.query).toContain('Наименование ПОДОБНО "%Альтернатива%"');
|
||
expect(plan.query).toContain('Наименование ПОДОБНО "%Плюс%"');
|
||
expect(plan.query).not.toContain('Наименование ПОДОБНО "%конец%"');
|
||
expect(plan.query).not.toContain('Наименование ПОДОБНО "%2020%"');
|
||
expect(plan.query).not.toContain('Наименование ПОДОБНО "%можно%"');
|
||
expect(plan.query).not.toContain('Наименование ПОДОБНО "%понять%"');
|
||
});
|
||
|
||
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");
|
||
});
|
||
|
||
});
|