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

3148 lines
141 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import { describe, expect, it } from "vitest";
import { detectAddressQuestionMode } from "../src/services/addressQueryClassifier";
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
import { classifyAddressQueryShape } from "../src/services/addressQueryShapeClassifier";
import { extractAddressFilters } from "../src/services/addressFilterExtractor";
import { AddressQueryService } from "../src/services/addressQueryService";
import { buildAddressRecipePlan, selectAddressRecipe } from "../src/services/addressRecipeCatalog";
import { runAddressDecomposeStage } from "../src/services/address_runtime/decomposeStage";
import { composeFactualReply } from "../src/services/address_runtime/composeStage";
describe("address query shape classifier", () => {
it("classifies explain question as deep-shape", () => {
const result = classifyAddressQueryShape("Why VAT chain does not match?");
expect(result.shape).toBe("EXPLAIN_OR_REASON");
expect(result.confidence).toBe("high");
});
it("classifies aggregate lookup question", () => {
const result = classifyAddressQueryShape("who owes us today?");
expect(result.shape).toBe("AGGREGATE_LOOKUP");
});
it("classifies compound factual question", () => {
const result = classifyAddressQueryShape("who owes us and who we owe today?");
expect(result.shape).toBe("COMPOUND_FACTUAL_QUERY");
});
it("keeps company lookup phrasing in address lane", () => {
const result = detectAddressQuestionMode("какие компании есть в базе");
expect(result.mode).toBe("address_query");
});
it("keeps loose by-anchor follow-up phrasing in address lane", () => {
const result = detectAddressQuestionMode("за любой период есть что-то по свк?");
expect(result.mode).toBe("address_query");
});
it("keeps slang transaction phrasing in address lane", () => {
const result = detectAddressQuestionMode("транзакции по свк за 2020");
expect(result.mode).toBe("address_query");
});
it("keeps short balance slang with compact account token in address lane", () => {
const result = detectAddressQuestionMode("скока по 60.02 на конец 2020-12");
expect(result.mode).toBe("address_query");
});
it("keeps management period profile question in address lane", () => {
const result = detectAddressQuestionMode("За какие годы в базе есть данные?");
expect(result.mode).toBe("address_query");
});
it("keeps management document/section profile question in address lane", () => {
const result = detectAddressQuestionMode("Какие разделы учета наиболее заполнены и какие почти не используются?");
expect(result.mode).toBe("address_query");
});
it("keeps management counterparty population question in address lane", () => {
const result = detectAddressQuestionMode("Сколько всего уникальных контрагентов в базе?");
expect(result.mode).toBe("address_query");
});
it("keeps slang supplier count question in address lane", () => {
const result = detectAddressQuestionMode("скока поставщиков в базе");
expect(result.mode).toBe("address_query");
});
it("keeps slang client count question in address lane", () => {
const result = detectAddressQuestionMode("скок клиентов");
expect(result.mode).toBe("address_query");
});
it("keeps customer activity lifecycle question in address lane", () => {
const result = detectAddressQuestionMode("Какие заказчики работали с нами в 2020 году?");
expect(result.mode).toBe("address_query");
});
it("keeps customer list all-time question in address lane", () => {
const result = detectAddressQuestionMode("выведи список заказчиков за все время");
expect(result.mode).toBe("address_query");
});
it("keeps customer list short-year question in address lane", () => {
const result = detectAddressQuestionMode("покажи список заказчиков за 20год");
expect(result.mode).toBe("address_query");
});
it("keeps noisy management phrase about years alive in address lane", () => {
const result = detectAddressQuestionMode("за какие года база ваще живая?");
expect(result.mode).toBe("address_query");
});
it("keeps noisy month-peak phrase in address lane", () => {
const result = detectAddressQuestionMode("а теперь месяц-пик по операциям");
expect(result.mode).toBe("address_query");
});
it("keeps management contract usage overview question in address lane", () => {
const result = detectAddressQuestionMode("Сколько всего договоров заведено и сколько из них реально использовались?");
expect(result.mode).toBe("address_query");
});
it("keeps customer value ranking question in address lane", () => {
const result = detectAddressQuestionMode("какие клиенты самые доходные, выдай топ-20");
expect(result.mode).toBe("address_query");
});
it("keeps highest inflow slang question in address lane", () => {
const result = detectAddressQuestionMode("какие приходы самые высокие за все время");
expect(result.mode).toBe("address_query");
});
it("keeps typo customer highest-check question in address lane", () => {
const result = detectAddressQuestionMode("с каких кликентов самый высокий чек");
expect(result.mode).toBe("address_query");
});
it("keeps supplier payout ranking question in address lane", () => {
const result = detectAddressQuestionMode("кому мы больше всего сгрузили денег, топ-20 поставщиков");
expect(result.mode).toBe("address_query");
});
it("keeps contract turnover ranking question in address lane", () => {
const result = detectAddressQuestionMode("договоры по обороту ранкни и дай топ-20");
expect(result.mode).toBe("address_query");
});
it("keeps top contract wording with 'контракт' in address lane", () => {
const result = detectAddressQuestionMode("по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?");
expect(result.mode).toBe("address_query");
});
});
describe("address compose stage utf8 headers", () => {
it("renders readable russian header for contract document list", () => {
const reply = composeFactualReply("list_documents_by_contract", [
{
period: "2020-10-15T13:34:53Z",
registrator: "Списание с расчетного счета 00000000246",
account_dt: "66.02",
account_kt: "51",
amount: 30819.47,
analytics: []
}
]);
expect(reply.text).toContain("Собран список документов по договору (live address lane).");
});
it("renders readable russian header for contract bank operations", () => {
const reply = composeFactualReply("bank_operations_by_contract", [
{
period: "2020-10-15T13:34:53Z",
registrator: "Списание с расчетного счета 00000000246",
account_dt: "66.02",
account_kt: "51",
amount: 30819.47,
analytics: []
}
]);
expect(reply.text).toContain("Собран список банковских операций по договору (live address lane).");
});
it("renders readable russian header for contracts-by-counterparty list", () => {
const reply = composeFactualReply("list_contracts_by_counterparty", [
{
period: "2000-01-01T00:00:00Z",
registrator: "Договор №19/15",
account_dt: null,
account_kt: null,
amount: 0,
analytics: ["Жуковка 51"]
}
]);
expect(reply.text).toContain("Собран список договоров по контрагенту (catalog address lane).");
expect(reply.text).toContain("Уникальных договоров: 1.");
expect(reply.text).toContain("Договор №19/15");
});
it("renders period coverage summary for management profile intent", () => {
const reply = composeFactualReply("period_coverage_profile", [
{
period: "2014-05-27T12:00:00Z",
registrator: "MIN_DATE",
account_dt: null,
account_kt: null,
amount: 0,
analytics: []
},
{
period: "2030-08-03T12:00:00Z",
registrator: "MAX_DATE",
account_dt: null,
account_kt: null,
amount: 0,
analytics: []
},
{
period: "2019-01-01T00:00:00Z",
registrator: "YEAR_DOCS",
account_dt: null,
account_kt: null,
amount: 1004,
analytics: []
},
{
period: "2015-02-01T00:00:00Z",
registrator: "MONTH_OPS",
account_dt: null,
account_kt: null,
amount: 1249,
analytics: []
}
]);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Профиль периодов базы собран");
expect(reply.text).toContain("Самый активный год по документам: 2019 (1004).");
expect(reply.text).toContain("Самый активный месяц по операциям: 2015-02 (1249).");
});
it("renders document type + account section profile summary", () => {
const reply = composeFactualReply("document_type_and_account_section_profile", [
{
period: "2000-01-01T00:00:00Z",
registrator: "DOC_TYPE_DOCS",
account_dt: "Списание с расчетного счета",
account_kt: "",
amount: 2352,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "DOC_TYPE_DOCS",
account_dt: "Поступление товаров и услуг",
account_kt: "",
amount: 486,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "SECTION_DT_OPS",
account_dt: "90",
account_kt: "DT",
amount: 1800,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "SECTION_KT_OPS",
account_dt: "90",
account_kt: "KT",
amount: 1173,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "SECTION_DT_OPS",
account_dt: "58",
account_kt: "DT",
amount: 1,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "SECTION_KT_OPS",
account_dt: "58",
account_kt: "KT",
amount: 1,
analytics: []
}
]);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Профиль типов документов и разделов учета собран");
expect(reply.text).toContain("Списание с расчетного счета: 2352");
expect(reply.text).toContain("90 (Продажи): 2973");
expect(reply.text).toContain("58 (Финансовые вложения): 2");
});
it("returns focused answer for active year question (without month block)", () => {
const reply = composeFactualReply(
"period_coverage_profile",
[
{
period: "2014-05-27T12:00:00Z",
registrator: "MIN_DATE",
account_dt: null,
account_kt: null,
amount: 0,
analytics: []
},
{
period: "2026-03-31T00:00:00Z",
registrator: "MAX_DATE",
account_dt: null,
account_kt: null,
amount: 0,
analytics: []
},
{
period: "2019-01-01T00:00:00Z",
registrator: "YEAR_DOCS",
account_dt: null,
account_kt: null,
amount: 1004,
analytics: []
},
{
period: "2026-01-01T00:00:00Z",
registrator: "YEAR_DOCS",
account_dt: null,
account_kt: null,
amount: 3,
analytics: []
},
{
period: "2015-02-01T00:00:00Z",
registrator: "MONTH_OPS",
account_dt: null,
account_kt: null,
amount: 1249,
analytics: []
}
],
{ userMessage: "Какой год самый активный по количеству документов?" }
);
expect(reply.text).toContain("Самый активный год по документам: 2019 (1004).");
expect(reply.text).not.toContain("Самый активный месяц по операциям");
expect(reply.text).not.toContain("Покрытие по датам");
});
it("returns focused answer for active month question (without year block)", () => {
const reply = composeFactualReply(
"period_coverage_profile",
[
{
period: "2019-01-01T00:00:00Z",
registrator: "YEAR_DOCS",
account_dt: null,
account_kt: null,
amount: 1004,
analytics: []
},
{
period: "2015-02-01T00:00:00Z",
registrator: "MONTH_OPS",
account_dt: null,
account_kt: null,
amount: 1249,
analytics: []
}
],
{ userMessage: "Какой месяц самый активный по количеству операций?" }
);
expect(reply.text).toContain("Самый активный месяц по операциям: 2015-02 (1249).");
expect(reply.text).not.toContain("Самый активный год по документам");
});
it("returns focused answer for passive year question (and ignores low-activity tail year)", () => {
const reply = composeFactualReply(
"period_coverage_profile",
[
{
period: "2014-05-27T12:00:00Z",
registrator: "MIN_DATE",
account_dt: null,
account_kt: null,
amount: 0,
analytics: []
},
{
period: "2026-03-31T00:00:00Z",
registrator: "MAX_DATE",
account_dt: null,
account_kt: null,
amount: 0,
analytics: []
},
{
period: "2019-01-01T00:00:00Z",
registrator: "YEAR_DOCS",
account_dt: null,
account_kt: null,
amount: 1004,
analytics: []
},
{
period: "2020-01-01T00:00:00Z",
registrator: "YEAR_DOCS",
account_dt: null,
account_kt: null,
amount: 112,
analytics: []
},
{
period: "2026-01-01T00:00:00Z",
registrator: "YEAR_DOCS",
account_dt: null,
account_kt: null,
amount: 3,
analytics: []
}
],
{ userMessage: "Какой год самый пассивный по количеству документов?" }
);
expect(reply.text).toContain("Самый пассивный год по документам: 2020 (112).");
expect(reply.text).not.toContain("Самый активный год по документам");
expect(reply.text).not.toContain("Покрытие по датам");
});
it("returns focused answer for passive month question (without year block)", () => {
const reply = composeFactualReply(
"period_coverage_profile",
[
{
period: "2019-01-01T00:00:00Z",
registrator: "YEAR_DOCS",
account_dt: null,
account_kt: null,
amount: 1004,
analytics: []
},
{
period: "2020-01-01T00:00:00Z",
registrator: "YEAR_DOCS",
account_dt: null,
account_kt: null,
amount: 112,
analytics: []
},
{
period: "2026-01-01T00:00:00Z",
registrator: "YEAR_DOCS",
account_dt: null,
account_kt: null,
amount: 3,
analytics: []
},
{
period: "2019-10-01T00:00:00Z",
registrator: "MONTH_OPS",
account_dt: null,
account_kt: null,
amount: 1400,
analytics: []
},
{
period: "2020-05-01T00:00:00Z",
registrator: "MONTH_OPS",
account_dt: null,
account_kt: null,
amount: 44,
analytics: []
},
{
period: "2026-01-01T00:00:00Z",
registrator: "MONTH_OPS",
account_dt: null,
account_kt: null,
amount: 2,
analytics: []
}
],
{ userMessage: "Какой месяц самый пассивный по количеству операций?" }
);
expect(reply.text).toContain("Самый пассивный месяц по операциям: 2020-05 (44).");
expect(reply.text).not.toContain("Самый активный месяц по операциям");
expect(reply.text).not.toContain("Самый активный год по документам");
});
it("shows operational range and low-activity tail for coverage question", () => {
const reply = composeFactualReply(
"period_coverage_profile",
[
{
period: "2014-05-27T12:00:00Z",
registrator: "MIN_DATE",
account_dt: null,
account_kt: null,
amount: 0,
analytics: []
},
{
period: "2026-03-31T00:00:00Z",
registrator: "MAX_DATE",
account_dt: null,
account_kt: null,
amount: 0,
analytics: []
},
{
period: "2019-01-01T00:00:00Z",
registrator: "YEAR_DOCS",
account_dt: null,
account_kt: null,
amount: 1004,
analytics: []
},
{
period: "2026-01-01T00:00:00Z",
registrator: "YEAR_DOCS",
account_dt: null,
account_kt: null,
amount: 3,
analytics: []
}
],
{ userMessage: "За какие годы в базе есть данные?" }
);
expect(reply.text).toContain("Операционный период с выраженной активностью: 2019..2019.");
expect(reply.text).toContain("Низкоактивный хвост (единичные записи): 2026.");
});
it("returns focused document-type answer without account sections", () => {
const reply = composeFactualReply(
"document_type_and_account_section_profile",
[
{
period: "2000-01-01T00:00:00Z",
registrator: "DOC_TYPE_DOCS",
account_dt: "Списание с расчетного счета",
account_kt: "",
amount: 2352,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "SECTION_DT_OPS",
account_dt: "90",
account_kt: "DT",
amount: 1800,
analytics: []
}
],
{ userMessage: "Какие типы документов используются чаще всего в базе?" }
);
expect(reply.text).toContain("Топ типов документов");
expect(reply.text).not.toContain("Наиболее заполненные разделы учета");
});
it("returns focused account-sections answer without document types", () => {
const reply = composeFactualReply(
"document_type_and_account_section_profile",
[
{
period: "2000-01-01T00:00:00Z",
registrator: "DOC_TYPE_DOCS",
account_dt: "Списание с расчетного счета",
account_kt: "",
amount: 2352,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "SECTION_DT_OPS",
account_dt: "90",
account_kt: "DT",
amount: 1800,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "SECTION_KT_OPS",
account_dt: "58",
account_kt: "KT",
amount: 2,
analytics: []
}
],
{ userMessage: "Какие разделы учета наиболее заполнены и какие почти не используются?" }
);
expect(reply.text).toContain("Наиболее заполненные разделы учета");
expect(reply.text).not.toContain("Топ типов документов");
});
it("returns focused answer for rare document types question", () => {
const reply = composeFactualReply(
"document_type_and_account_section_profile",
[
{
period: "2000-01-01T00:00:00Z",
registrator: "DOC_TYPE_DOCS",
account_dt: "Списание с расчетного счета",
account_kt: "",
amount: 2352,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "DOC_TYPE_DOCS",
account_dt: "Поступление на расчетный счет",
account_kt: "",
amount: 124,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "SECTION_DT_OPS",
account_dt: "90",
account_kt: "DT",
amount: 1800,
analytics: []
}
],
{ userMessage: "Какие типы документов используются реже всего в базе?" }
);
expect(reply.text).toContain("Наименее используемые типы документов");
expect(reply.text).toContain("Поступление на расчетный счет: 124");
expect(reply.text).not.toContain("Топ типов документов");
expect(reply.text).not.toContain("Наиболее заполненные разделы учета");
});
it("returns focused answer for least-filled account sections question", () => {
const reply = composeFactualReply(
"document_type_and_account_section_profile",
[
{
period: "2000-01-01T00:00:00Z",
registrator: "SECTION_DT_OPS",
account_dt: "90",
account_kt: "DT",
amount: 1800,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "SECTION_KT_OPS",
account_dt: "90",
account_kt: "KT",
amount: 1173,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "SECTION_DT_OPS",
account_dt: "58",
account_kt: "DT",
amount: 1,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "SECTION_KT_OPS",
account_dt: "58",
account_kt: "KT",
amount: 1,
analytics: []
}
],
{ userMessage: "Какие разделы учета наименее заполнены?" }
);
expect(reply.text).toContain("Наименее заполненные разделы учета");
expect(reply.text).toContain("58 (Финансовые вложения): 2");
expect(reply.text).not.toContain("Наиболее заполненные разделы учета");
expect(reply.text).not.toContain("Топ типов документов");
});
it("returns focused answer for total counterparties question", () => {
const reply = composeFactualReply(
"counterparty_population_and_roles",
[
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_TOTAL",
account_dt: "",
account_kt: "",
amount: 412,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_CUSTOMER_ACTIVE",
account_dt: "",
account_kt: "",
amount: 145,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_SUPPLIER_ACTIVE",
account_dt: "",
account_kt: "",
amount: 94,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_MIXED_ACTIVE",
account_dt: "",
account_kt: "",
amount: 23,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_ACTIVE_UNION",
account_dt: "",
account_kt: "",
amount: 216,
analytics: []
}
],
{ userMessage: "Сколько всего уникальных контрагентов в базе?" }
);
expect(reply.text).toContain("Всего уникальных контрагентов в базе: 412.");
expect(reply.text).not.toContain("Роли контрагентов по активности");
});
it("returns focused answer for counterparty roles split question", () => {
const reply = composeFactualReply(
"counterparty_population_and_roles",
[
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_TOTAL",
account_dt: "",
account_kt: "",
amount: 412,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_CUSTOMER_ACTIVE",
account_dt: "",
account_kt: "",
amount: 145,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_SUPPLIER_ACTIVE",
account_dt: "",
account_kt: "",
amount: 94,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_MIXED_ACTIVE",
account_dt: "",
account_kt: "",
amount: 23,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_ACTIVE_UNION",
account_dt: "",
account_kt: "",
amount: 216,
analytics: []
}
],
{ userMessage: "Сколько у нас заказчиков, поставщиков и смешанных контрагентов?" }
);
expect(reply.text).toContain("Роли контрагентов по активности:");
expect(reply.text).toContain("Заказчики (только customer-роль): 122.");
expect(reply.text).toContain("Поставщики (только supplier-роль): 71.");
expect(reply.text).toContain("Смешанные (и покупатель, и поставщик): 23.");
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
});
it("returns focused answer for slang supplier count question", () => {
const reply = composeFactualReply(
"counterparty_population_and_roles",
[
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_TOTAL",
account_dt: "",
account_kt: "",
amount: 412,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_CUSTOMER_ACTIVE",
account_dt: "",
account_kt: "",
amount: 145,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_SUPPLIER_ACTIVE",
account_dt: "",
account_kt: "",
amount: 94,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_MIXED_ACTIVE",
account_dt: "",
account_kt: "",
amount: 23,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_ACTIVE_UNION",
account_dt: "",
account_kt: "",
amount: 216,
analytics: []
}
],
{ userMessage: "скока поставщиков в базе" }
);
expect(reply.text).toContain("Поставщиков (только supplier-роль): 71.");
expect(reply.text).not.toContain("Роли контрагентов по активности:");
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
});
it("returns focused answer for slang client count question", () => {
const reply = composeFactualReply(
"counterparty_population_and_roles",
[
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_TOTAL",
account_dt: "",
account_kt: "",
amount: 412,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_CUSTOMER_ACTIVE",
account_dt: "",
account_kt: "",
amount: 145,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_SUPPLIER_ACTIVE",
account_dt: "",
account_kt: "",
amount: 94,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_MIXED_ACTIVE",
account_dt: "",
account_kt: "",
amount: 23,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CP_ACTIVE_UNION",
account_dt: "",
account_kt: "",
amount: 216,
analytics: []
}
],
{ userMessage: "скок клиентов" }
);
expect(reply.text).toContain("Заказчиков (только customer-роль): 122.");
expect(reply.text).not.toContain("Роли контрагентов по активности:");
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
});
it("returns customer activity lifecycle list for year question", () => {
const reply = composeFactualReply(
"counterparty_activity_lifecycle",
[
{
period: "2020-12-16T16:20:52Z",
registrator: "CP_CUSTOMER_ACTIVITY",
account_dt: "",
account_kt: "",
amount: 15,
analytics: ["НОРТОН"]
},
{
period: "2020-11-19T12:00:04Z",
registrator: "CP_CUSTOMER_ACTIVITY",
account_dt: "",
account_kt: "",
amount: 11,
analytics: ["Группа"]
}
],
{ userMessage: "Какие заказчики работали с нами в 2020 году?" }
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Собран профиль активности заказчиков");
expect(reply.text).toContain("Активные заказчики в 2020 году: 2.");
expect(reply.text).toContain("НОРТОН");
expect(reply.text).toContain("Группа");
});
it("returns explicit 2020 year label for short-year lifecycle question", () => {
const reply = composeFactualReply(
"counterparty_activity_lifecycle",
[
{
period: "2020-12-16T16:20:52Z",
registrator: "CP_CUSTOMER_ACTIVITY",
account_dt: "",
account_kt: "",
amount: 15,
analytics: ["НОРТОН"]
}
],
{ userMessage: "покажи список заказчиков за 20год" }
);
expect(reply.text).toContain("Активные заказчики в 2020 году: 1.");
});
it("returns contract usage overview summary", () => {
const reply = composeFactualReply("contract_usage_overview", [
{
period: "2000-01-01T00:00:00Z",
registrator: "CT_TOTAL",
account_dt: "",
account_kt: "",
amount: 520,
analytics: []
},
{
period: "2000-01-01T00:00:00Z",
registrator: "CT_USED",
account_dt: "",
account_kt: "",
amount: 148,
analytics: []
}
]);
expect(reply.text).toContain("Профиль договорной базы собран");
expect(reply.text).toContain("Всего договоров в базе: 520.");
expect(reply.text).toContain("Использованных договоров (есть factual связь с операциями): 148.");
expect(reply.text).toContain("Неиспользуемых договоров: 372.");
});
it("renders customer value top list with explicit top-2 limit", () => {
const reply = composeFactualReply(
"customer_revenue_and_payments",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "Поступление 1",
account_dt: "",
account_kt: "",
amount: 500,
analytics: ["Клиент А", "Договор А-1"]
},
{
period: "2020-03-02T00:00:00Z",
registrator: "Поступление 2",
account_dt: "",
account_kt: "",
amount: 700,
analytics: ["Клиент Б", "Договор Б-1"]
},
{
period: "2020-03-03T00:00:00Z",
registrator: "Поступление 3",
account_dt: "",
account_kt: "",
amount: 300,
analytics: ["Клиент А", "Договор А-1"]
}
],
{ userMessage: "покажи топ-2 заказчиков по сумме поступлений" }
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Топ-2 заказчиков по сумме поступлений:");
expect(reply.text).toContain("1. Клиент А | сумма: 800");
expect(reply.text).toContain("2. Клиент Б | сумма: 700");
});
it("renders top incoming deals for highest inflow wording", () => {
const reply = composeFactualReply(
"customer_revenue_and_payments",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "Поступление 1",
account_dt: "",
account_kt: "",
amount: 500,
analytics: ["Клиент А", "Договор А-1"]
},
{
period: "2020-03-02T00:00:00Z",
registrator: "Поступление 2",
account_dt: "",
account_kt: "",
amount: 700,
analytics: ["Клиент Б", "Договор Б-1"]
}
],
{ userMessage: "какие приходы самые высокие за все время" }
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("самых крупных разовых сделок по поступлениям");
expect(reply.text).toContain("Поступление 2");
});
it("renders max-single ranking for highest-check typo wording", () => {
const reply = composeFactualReply(
"customer_revenue_and_payments",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "Поступление 1",
account_dt: "",
account_kt: "",
amount: 500,
analytics: ["Клиент А", "Договор А-1"]
},
{
period: "2020-03-02T00:00:00Z",
registrator: "Поступление 2",
account_dt: "",
account_kt: "",
amount: 1200,
analytics: ["Клиент Б", "Договор Б-1"]
},
{
period: "2020-03-03T00:00:00Z",
registrator: "Поступление 3",
account_dt: "",
account_kt: "",
amount: 300,
analytics: ["Клиент А", "Договор А-1"]
}
],
{ userMessage: "с каких кликентов самый высокий чек" }
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("по максимальной сумме одной входящей операции");
expect(reply.text).toContain("1. Клиент Б | max single: 1200");
});
it("renders supplier payout list by operations count", () => {
const reply = composeFactualReply(
"supplier_payouts_profile",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "Списание 1",
account_dt: "",
account_kt: "",
amount: 100,
analytics: ["Поставщик А", "Договор А-1"]
},
{
period: "2020-03-02T00:00:00Z",
registrator: "Списание 2",
account_dt: "",
account_kt: "",
amount: 120,
analytics: ["Поставщик А", "Договор А-2"]
},
{
period: "2020-03-03T00:00:00Z",
registrator: "Списание 3",
account_dt: "",
account_kt: "",
amount: 500,
analytics: ["Поставщик Б", "Договор Б-1"]
}
],
{ userMessage: "топ-20 поставщиков по количеству исходящих платежных операций" }
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Топ-2 поставщиков по количеству исходящих платежных операций:");
expect(reply.text).toContain("1. Поставщик А | операций: 2");
});
it("renders contract value list for minimal active budgets", () => {
const reply = composeFactualReply(
"contract_usage_and_value",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "CT_VALUE_IN",
account_dt: "",
account_kt: "",
amount: 900,
analytics: ["Клиент А", "Договор 01/20"]
},
{
period: "2020-03-02T00:00:00Z",
registrator: "CT_VALUE_OUT",
account_dt: "",
account_kt: "",
amount: 100,
analytics: ["Поставщик Б", "Договор 02/20"]
},
{
period: "2020-03-03T00:00:00Z",
registrator: "CT_VALUE_IN",
account_dt: "",
account_kt: "",
amount: 150,
analytics: ["Клиент В", "Договор 03/20"]
}
],
{ userMessage: "покажи топ-20 договоров с минимальным бюджетом среди активных договоров" }
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("активных договоров с минимальным бюджетом");
expect(reply.text).toContain("1. Договор 02/20 | оборот: 100");
});
it("adds deterministic why-zero explanation for VAT forecast follow-up wording", () => {
const reply = composeFactualReply(
"vat_payable_forecast",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "VAT_68_CREDIT",
account_dt: "68",
account_kt: "",
amount: 9126,
analytics: []
},
{
period: "2020-03-01T00:00:00Z",
registrator: "VAT_68_DEBIT",
account_dt: "68",
account_kt: "",
amount: 115342,
analytics: []
},
{
period: "2020-03-01T00:00:00Z",
registrator: "VAT_19_DEBIT",
account_dt: "19",
account_kt: "",
amount: 1602384,
analytics: []
},
{
period: "2020-03-01T00:00:00Z",
registrator: "VAT_19_CREDIT",
account_dt: "19",
account_kt: "",
amount: 0,
analytics: []
}
],
{ userMessage: "почему прогноз к уплате 0?" }
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Почему прогноз к уплате 0");
expect(reply.text).toContain("max(0, 68 Кт - 68 Дт)");
expect(reply.text).toContain("За период 68 Кт = 9126.00, 68 Дт = 115342.00, разница = -106216.00.");
expect(reply.text).toContain("Разница неположительная");
expect(reply.text).toContain("оперативный прогноз по оборотам НДС-субсчетов 68.02*/19*");
});
it("adds VAT declaration and payment deadlines for as-of-date forecast window", () => {
const reply = composeFactualReply(
"vat_payable_forecast",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "VAT_68_CREDIT",
account_dt: "68",
account_kt: "",
amount: 300,
analytics: []
},
{
period: "2020-03-01T00:00:00Z",
registrator: "VAT_68_DEBIT",
account_dt: "68",
account_kt: "",
amount: 0,
analytics: []
}
],
{
userMessage: "сколько НДС нужно заплатить по состоянию на 15 марта 2020 года",
periodFrom: "2020-01-01",
periodTo: "2020-03-15"
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Период расчета (срез обязательств): 01.01.2020..15.03.2020.");
expect(reply.text).toContain("Налоговый период: 1 кв. 2020.");
expect(reply.text).toContain("Срок сдачи декларации: до 25.04.2020.");
expect(reply.text).toContain("Сроки уплаты: 28.04.2020, 28.05.2020, 28.06.2020.");
expect(reply.text).toContain("Ориентир по долям к уплате: 100.00 / 100.00 / 100.00.");
});
it("builds VAT deadlines correctly for Q4 with next-year rollover", () => {
const reply = composeFactualReply(
"vat_payable_forecast",
[
{
period: "2020-12-31T00:00:00Z",
registrator: "VAT_68_CREDIT",
account_dt: "68",
account_kt: "",
amount: 90,
analytics: []
}
],
{
userMessage: "прогноз НДС на 31 декабря 2020",
periodFrom: "2020-10-01",
periodTo: "2020-12-31"
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Налоговый период: 4 кв. 2020.");
expect(reply.text).toContain("Срок сдачи декларации: до 25.01.2021.");
expect(reply.text).toContain("Сроки уплаты: 28.01.2021, 28.02.2021, 28.03.2021.");
expect(reply.text).toContain("Ориентир по долям к уплате: 30.00 / 30.00 / 30.00.");
});
it("explains zero VAT as no-movements case when VAT turnovers are absent in window", () => {
const reply = composeFactualReply(
"vat_payable_forecast",
[
{
period: "2019-04-01T00:00:00Z",
registrator: "VAT_68_CREDIT",
account_dt: "68",
account_kt: "",
amount: 0,
analytics: []
},
{
period: "2019-04-01T00:00:00Z",
registrator: "VAT_68_DEBIT",
account_dt: "68",
account_kt: "",
amount: 0,
analytics: []
},
{
period: "2019-04-01T00:00:00Z",
registrator: "VAT_19_DEBIT",
account_dt: "19",
account_kt: "",
amount: 0,
analytics: []
},
{
period: "2019-04-01T00:00:00Z",
registrator: "VAT_19_CREDIT",
account_dt: "19",
account_kt: "",
amount: 0,
analytics: []
}
],
{
userMessage: "какой прогноз оплаты ндс на 12-05-2019",
periodFrom: "2019-04-01",
periodTo: "2019-05-12"
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Прогноз НДС к уплате: 0.00.");
expect(reply.text).toContain("не найдено движений по НДС-субсчетам 68.02*/19*");
expect(reply.text).toContain("Чеклист проверки в 1С (почему к уплате 0):");
expect(reply.text).toContain("Проверьте наличие движений в РегистрБухгалтерии.Хозрасчетный");
});
it("explains zero VAT as offset case when VAT turnovers exist but net is near zero", () => {
const reply = composeFactualReply(
"vat_payable_forecast",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "VAT_68_CREDIT",
account_dt: "68",
account_kt: "",
amount: 1000,
analytics: []
},
{
period: "2020-03-01T00:00:00Z",
registrator: "VAT_68_DEBIT",
account_dt: "68",
account_kt: "",
amount: 1000,
analytics: []
}
],
{
userMessage: "какой прогноз оплаты ндс на 12-05-2020",
periodFrom: "2020-04-01",
periodTo: "2020-05-12"
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Прогноз НДС к уплате: 0.00.");
expect(reply.text).toContain("обороты по 68* взаимно перекрылись");
expect(reply.text).toContain("Чеклист проверки в 1С (почему к уплате 0):");
});
});
describe("address intent resolver expansion (M2.3a)", () => {
it("resolves documents by counterparty intent", () => {
const result = resolveAddressIntent("show documents by counterparty Alfa from 2020-07-01 to 2020-07-31");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves bank operations by counterparty intent", () => {
const result = resolveAddressIntent("show bank operations by counterparty Alfa");
expect(result.intent).toBe("bank_operations_by_counterparty");
});
it("resolves documents forming balance intent", () => {
const result = resolveAddressIntent("which documents form balance for account 62 as of 2020-07-31");
expect(result.intent).toBe("documents_forming_balance");
});
it("resolves documents forming balance for russian participle phrasing", () => {
const result = resolveAddressIntent("Показать документы, формирующие остаток по счету 60.01 на дату 2020-07-31");
expect(result.intent).toBe("documents_forming_balance");
});
it("resolves documents forming balance for slang phrase with compact account token", () => {
const result = resolveAddressIntent("раскрой остаток 60.01 по документам на конец июля 2020");
expect(result.intent).toBe("documents_forming_balance");
});
it("resolves documents forming balance for 'доки под остатком' slang phrase", () => {
const result = resolveAddressIntent("доки под остатком 60.01 на 2020-07-31");
expect(result.intent).toBe("documents_forming_balance");
});
it("resolves documents by company phrase as counterparty intent", () => {
const result = resolveAddressIntent("Какие документы доступны по компании СВК за 2021 год?");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves transliterated docy slang as documents by counterparty intent", () => {
const result = resolveAddressIntent("svk poka docy za 2020");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves bank operations by supplier phrase", () => {
const result = resolveAddressIntent("Покажи платежи по поставщику Альфа за июль 2020");
expect(result.intent).toBe("bank_operations_by_counterparty");
});
it("resolves documents by contract intent", () => {
const result = resolveAddressIntent("Покажи документы по договору 19/15 за 2020");
expect(result.intent).toBe("list_documents_by_contract");
});
it("resolves bank operations by contract intent", () => {
const result = resolveAddressIntent("Покажи банковские операции по договору 19/15");
expect(result.intent).toBe("bank_operations_by_contract");
});
it("resolves shorthand bank-by-contract slang intent", () => {
const result = resolveAddressIntent("покажи банк опер по дог 19/15 пж");
expect(result.intent).toBe("bank_operations_by_contract");
});
it("resolves debt-by-contract query to open items intent", () => {
const result = resolveAddressIntent("Есть ли долг по договору 19/15 на 2020-07-31");
expect(result.intent).toBe("open_items_by_counterparty_or_contract");
});
it("resolves unclosed contracts list query without specific anchor", () => {
const result = resolveAddressIntent("Покажи незакрытые договоры на 2020-12-31");
expect(result.intent).toBe("list_open_contracts");
});
it("resolves bank operations by contract for normalized phrase with linked contract wording", () => {
const result = resolveAddressIntent(
"Показать банковские операции (счета 51, 60, 62) связанные с договором 19/15."
);
expect(result.intent).toBe("bank_operations_by_contract");
});
it("keeps bank_operations_by_counterparty even when account hints are present", () => {
const result = resolveAddressIntent("Показать банковские операции (счета 51, 62) для контрагента СВК за 2020 год");
expect(result.intent).toBe("bank_operations_by_counterparty");
});
it("resolves documents by client phrase", () => {
const result = resolveAddressIntent("Выведи документы по клиенту Бета за 2020-07");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves short slang docs phrase with loose by-anchor", () => {
const result = resolveAddressIntent("какие доки есть по свк за 2021");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves typo slang docs phrase with implicit anchor", () => {
const result = resolveAddressIntent("свк доки за 20год покеж");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves noisy docs phrase with slang tail", () => {
const result = resolveAddressIntent("свк 20 год - покажи доки плс");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves slang transactions phrase by counterparty", () => {
const result = resolveAddressIntent("транзакции по свк за 2020");
expect(result.intent).toBe("bank_operations_by_counterparty");
});
it("resolves short balance slang with compact account token", () => {
const result = resolveAddressIntent("скока по 60.02 на конец 2020-12");
expect(result.intent).toBe("account_balance_snapshot");
});
it("resolves colloquial 'что на счете' phrasing as account balance snapshot", () => {
const result = resolveAddressIntent("что на счете 60 на 2020.05");
expect(result.intent).toBe("account_balance_snapshot");
});
it("resolves mixed ru/en balance phrasing with account token", () => {
const result = resolveAddressIntent("баланс account 60.01 as of 2020-07-31");
expect(result.intent).toBe("account_balance_snapshot");
});
it("resolves 'по докам' slang as documents forming balance", () => {
const result = resolveAddressIntent("раскидай остаток 62.01 по докам на 2020-12-31");
expect(result.intent).toBe("documents_forming_balance");
});
it("resolves english compact docs-forming phrasing", () => {
const result = resolveAddressIntent("docs forming balance 60.01 as of 2020-07-31");
expect(result.intent).toBe("documents_forming_balance");
});
it("resolves loose by-anchor follow-up as documents by counterparty fallback", () => {
const result = resolveAddressIntent("за любой период есть что-то по свк?");
expect(result.intent).toBe("list_documents_by_counterparty");
});
it("resolves period coverage profile for years-in-database question", () => {
const result = resolveAddressIntent("За какие годы в базе есть данные?");
expect(result.intent).toBe("period_coverage_profile");
});
it("resolves period coverage profile for top active year by documents question", () => {
const result = resolveAddressIntent("Какой год самый активный по количеству документов?");
expect(result.intent).toBe("period_coverage_profile");
});
it("resolves period coverage profile for top active month by operations question", () => {
const result = resolveAddressIntent("Какой месяц самый активный по количеству операций?");
expect(result.intent).toBe("period_coverage_profile");
});
it("resolves period coverage profile for passive year by documents question", () => {
const result = resolveAddressIntent("Какой год самый пассивный по количеству документов?");
expect(result.intent).toBe("period_coverage_profile");
});
it("resolves period coverage profile for passive month by operations question", () => {
const result = resolveAddressIntent("Какой месяц самый пассивный по количеству операций?");
expect(result.intent).toBe("period_coverage_profile");
});
it("resolves period coverage profile for noisy active-year-by-docs phrase", () => {
const result = resolveAddressIntent("какой год тут самый движовый по докам");
expect(result.intent).toBe("period_coverage_profile");
});
it("resolves period coverage profile for month-peak follow-up phrase", () => {
const result = resolveAddressIntent("а теперь месяц-пик по операциям");
expect(result.intent).toBe("period_coverage_profile");
});
it("resolves document+section profile for document type usage question", () => {
const result = resolveAddressIntent("Какие типы документов используются чаще всего в базе?");
expect(result.intent).toBe("document_type_and_account_section_profile");
});
it("resolves document+section profile for account section fill question", () => {
const result = resolveAddressIntent("Какие разделы учета наиболее заполнены и какие почти не используются?");
expect(result.intent).toBe("document_type_and_account_section_profile");
});
it("resolves document+section profile for rare document types question", () => {
const result = resolveAddressIntent("Какие типы документов используются реже всего в базе?");
expect(result.intent).toBe("document_type_and_account_section_profile");
});
it("resolves document+section profile for least-filled account sections question", () => {
const result = resolveAddressIntent("Какие разделы учета наименее заполнены?");
expect(result.intent).toBe("document_type_and_account_section_profile");
});
it("resolves document+section profile for noisy docs usage phrase", () => {
const result = resolveAddressIntent("каких доков у нас больше всего крутится?");
expect(result.intent).toBe("document_type_and_account_section_profile");
});
it("resolves document+section profile for summary by doc types and share phrase", () => {
const result = resolveAddressIntent("Сформируй сводку по типам документов и их доле в общем объеме.");
expect(result.intent).toBe("document_type_and_account_section_profile");
});
it("resolves counterparty population intent for total unique counterparties question", () => {
const result = resolveAddressIntent("Сколько всего уникальных контрагентов в базе?");
expect(result.intent).toBe("counterparty_population_and_roles");
});
it("resolves counterparty population intent for roles split question", () => {
const result = resolveAddressIntent("Сколько у нас заказчиков, поставщиков и смешанных контрагентов?");
expect(result.intent).toBe("counterparty_population_and_roles");
});
it("resolves counterparty population intent for slang supplier count question", () => {
const result = resolveAddressIntent("скока поставщиков");
expect(result.intent).toBe("counterparty_population_and_roles");
});
it("resolves counterparty population intent for slang supplier count in base question", () => {
const result = resolveAddressIntent("скока поставщиков в базе");
expect(result.intent).toBe("counterparty_population_and_roles");
});
it("resolves counterparty population intent for slang client count question", () => {
const result = resolveAddressIntent("скок клиентов");
expect(result.intent).toBe("counterparty_population_and_roles");
});
it("resolves counterparty population intent for slang client count in base question", () => {
const result = resolveAddressIntent("скок клиентов в базе");
expect(result.intent).toBe("counterparty_population_and_roles");
});
it("resolves counterparty lifecycle intent for active customers in year question", () => {
const result = resolveAddressIntent("Какие заказчики работали с нами в 2020 году?");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("resolves counterparty lifecycle intent for active customers all-time question", () => {
const result = resolveAddressIntent("Какие клиенты работали с нами за все время?");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("resolves counterparty lifecycle intent for customer list all-time question", () => {
const result = resolveAddressIntent("выведи список заказчиков за все время");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("resolves counterparty lifecycle intent for customer list short-year question", () => {
const result = resolveAddressIntent("покажи список заказчиков за 20год");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("resolves counterparty lifecycle intent for roster wording without explicit period", () => {
const result = resolveAddressIntent("кто у нас заказчики вообще");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("resolves counterparty lifecycle intent for one-time counterparties wording", () => {
const result = resolveAddressIntent("Какие контрагенты работали с нами только один раз за все время?");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("resolves counterparty lifecycle intent for longest-running counterparties wording", () => {
const result = resolveAddressIntent("Какие контрагенты работают с нами дольше всех?");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("resolves supplier lifecycle segmentation wording into lifecycle intent", () => {
const result = resolveAddressIntent("Раздели поставщиков на регулярных и эпизодических по активности.");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("resolves stale suppliers wording into lifecycle intent", () => {
const result = resolveAddressIntent("Какие поставщики давно не использовались?");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps supplier lifecycle segmentation with operations wording in lifecycle intent", () => {
const result = resolveAddressIntent("Раздели поставщиков на регулярных и эпизодических по частоте операций.");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps stale supplier operations wording in lifecycle intent", () => {
const result = resolveAddressIntent("Какие поставщики давно не использовались в операционной активности?");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps slang all-customers-all-time wording in lifecycle intent", () => {
const result = resolveAddressIntent("выведи всех заков за все время");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps churn wording with year in lifecycle intent", () => {
const result = resolveAddressIntent("кто был активен в 2020 и потом отвалился");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps one-time-plus-churn wording in lifecycle intent", () => {
const result = resolveAddressIntent("кто с нами был ровно один раз и пропал");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps oldest-collaboration slang wording in lifecycle intent", () => {
const result = resolveAddressIntent("самые старые по сотрудничеству кто");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps regular-vs-one-off supplier slang in lifecycle intent (not population)", () => {
const result = resolveAddressIntent("разбей поставщиков на регуляр и разовые");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("resolves contract usage overview intent", () => {
const result = resolveAddressIntent("Сколько всего договоров заведено и сколько из них реально использовались?");
expect(result.intent).toBe("contract_usage_overview");
});
it("resolves stale contracts wording into contract usage overview intent", () => {
const result = resolveAddressIntent("Какие договоры давно не использовались?");
expect(result.intent).toBe("contract_usage_overview");
});
it("resolves customer revenue/payout ranking intent", () => {
const result = resolveAddressIntent("какие клиенты самые доходные, выдай топ-20");
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves customer revenue intent from highest inflow slang wording", () => {
const result = resolveAddressIntent("какие приходы самые высокие за все время");
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves customer revenue intent from small deals by budget slang wording", () => {
const result = resolveAddressIntent("покажи топ-20 самых маленьких сделок по бюджету");
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves customer revenue intent from typo highest-check wording", () => {
const result = resolveAddressIntent("с каких кликентов самый высокий чек");
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves top counterparty slang wording into customer revenue intent", () => {
const result = resolveAddressIntent("какой самый жирный контрагент у нее? кто больше платит денег");
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves supplier payouts profile intent from slang wording", () => {
const result = resolveAddressIntent("кому мы больше всего сгрузили денег, топ-20 поставщиков");
expect(result.intent).toBe("supplier_payouts_profile");
});
it("resolves contract usage and value intent", () => {
const result = resolveAddressIntent("договоры по обороту ранкни и дай топ-20");
expect(result.intent).toBe("contract_usage_and_value");
});
it("resolves top contract wording with 'контракт' into contract usage and value intent", () => {
const result = resolveAddressIntent("по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?");
expect(result.intent).toBe("contract_usage_and_value");
});
it("resolves revenue-total slang wording into customer revenue intent", () => {
const result = resolveAddressIntent("скока денег альтернатива заработала за 22 год");
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves overall-turnover wording into customer revenue intent", () => {
const result = resolveAddressIntent("какие общие обороты за все время");
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("resolves VAT payment forecast wording into dedicated VAT forecast intent", () => {
const result = resolveAddressIntent("какой прогноз оплаты ндс за 12 мая 2020");
expect(result.intent).toBe("vat_payable_forecast");
expect(result.reasons).toContain("forecast_tax_signal_detected");
});
it("resolves colloquial VAT payable estimate wording without explicit 'прогноз'", () => {
const result = resolveAddressIntent("мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года");
expect(result.intent).toBe("vat_payable_forecast");
expect(result.reasons).toContain("forecast_tax_signal_detected");
});
it("resolves multi-contract counterparties wording into contract usage and value intent", () => {
const result = resolveAddressIntent("Покажи контрагентов с несколькими договорами и какие из договоров активны.");
expect(result.intent).toBe("contract_usage_and_value");
});
it("resolves contracts-by-counterparty intent from list wording", () => {
const result = resolveAddressIntent("покажи договора все по жуковке 51");
expect(result.intent).toBe("list_contracts_by_counterparty");
});
it("prefers documents-by-contract intent for explicit document follow-up wording", () => {
const result = resolveAddressIntent("покажи документы по этому же договору");
expect(result.intent).toBe("list_documents_by_contract");
});
});
describe("address filter extraction for balance drilldown", () => {
it("does not force default limit=20 for management aggregate intents", () => {
const periodProfile = extractAddressFilters("За какие годы в базе есть данные?", "period_coverage_profile");
const docSectionProfile = extractAddressFilters(
"Какие типы документов используются чаще всего в базе?",
"document_type_and_account_section_profile"
);
const counterpartyProfile = extractAddressFilters(
"Сколько всего уникальных контрагентов в базе?",
"counterparty_population_and_roles"
);
const counterpartyLifecycle = extractAddressFilters(
"Какие заказчики работали с нами в 2020 году?",
"counterparty_activity_lifecycle"
);
const contractOverview = extractAddressFilters(
"Сколько всего договоров заведено и сколько из них реально использовались?",
"contract_usage_overview"
);
const customerValue = extractAddressFilters(
"какие клиенты самые доходные, выдай топ-20",
"customer_revenue_and_payments"
);
const supplierValue = extractAddressFilters(
"кому мы больше всего сгрузили денег, топ-20 поставщиков",
"supplier_payouts_profile"
);
const contractValue = extractAddressFilters(
"договоры по обороту ранкни и дай топ-20",
"contract_usage_and_value"
);
const vatForecast = extractAddressFilters("какой прогноз оплаты ндс за 12 мая 2020", "vat_payable_forecast");
expect(periodProfile.extracted_filters.limit).toBeUndefined();
expect(docSectionProfile.extracted_filters.limit).toBeUndefined();
expect(counterpartyProfile.extracted_filters.limit).toBeUndefined();
expect(counterpartyLifecycle.extracted_filters.limit).toBeUndefined();
expect(contractOverview.extracted_filters.limit).toBeUndefined();
expect(customerValue.extracted_filters.limit).toBe(20);
expect(supplierValue.extracted_filters.limit).toBe(20);
expect(contractValue.extracted_filters.limit).toBe(20);
expect(vatForecast.extracted_filters.limit).toBeUndefined();
expect(periodProfile.extracted_filters.period_to).toBeDefined();
expect(docSectionProfile.extracted_filters.period_to).toBeDefined();
expect(counterpartyProfile.extracted_filters.period_to).toBeDefined();
expect(counterpartyLifecycle.extracted_filters.period_to).toBeDefined();
expect(contractOverview.extracted_filters.period_to).toBeDefined();
expect(customerValue.extracted_filters.period_to).toBeDefined();
expect(supplierValue.extracted_filters.period_to).toBeDefined();
expect(contractValue.extracted_filters.period_to).toBeDefined();
expect(vatForecast.extracted_filters.period_from).toBe("2020-04-01");
expect(vatForecast.extracted_filters.period_to).toBe("2020-05-12");
expect(periodProfile.warnings).toContain("period_to_defaulted_today_for_management_profile");
expect(docSectionProfile.warnings).toContain("period_to_defaulted_today_for_management_profile");
expect(counterpartyProfile.warnings).toContain("period_to_defaulted_today_for_management_profile");
expect(counterpartyLifecycle.warnings).not.toContain("period_to_defaulted_today_for_management_profile");
expect(contractOverview.warnings).toContain("period_to_defaulted_today_for_management_profile");
expect(customerValue.warnings).toContain("period_to_defaulted_today_for_management_profile");
expect(supplierValue.warnings).toContain("period_to_defaulted_today_for_management_profile");
expect(contractValue.warnings).toContain("period_to_defaulted_today_for_management_profile");
expect(vatForecast.warnings).toContain("period_derived_from_month_phrase");
expect(vatForecast.warnings).toContain("period_from_derived_from_quarter_for_vat_forecast");
expect(vatForecast.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast");
expect(vatForecast.warnings).not.toContain("period_to_defaulted_today_for_management_profile");
});
it("extracts short-year period for lifecycle customer list question", () => {
const lifecycleShortYear = extractAddressFilters(
"покажи список заказчиков за 20год",
"counterparty_activity_lifecycle"
);
expect(lifecycleShortYear.extracted_filters.period_from).toBe("2020-01-01");
expect(lifecycleShortYear.extracted_filters.period_to).toBe("2020-12-31");
});
it("drops noisy counterparty anchor in ranking question for customer revenue profile", () => {
const extracted = extractAddressFilters(
"какой самый жирный контрагент у нее? кто больше платит денег",
"customer_revenue_and_payments"
);
expect(extracted.extracted_filters.counterparty).toBeUndefined();
expect(extracted.warnings).toContain("counterparty_anchor_dropped_low_quality");
});
it("derives VAT forecast quarter-to-date window when plain date phrase is present", () => {
const extracted = extractAddressFilters(
"мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года",
"vat_payable_forecast"
);
expect(extracted.extracted_filters.period_from).toBe("2020-01-01");
expect(extracted.extracted_filters.period_to).toBe("2020-03-15");
expect(extracted.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast");
});
it("derives VAT forecast quarter-to-date window for explicit day+month+year phrase", () => {
const extracted = extractAddressFilters(
"сколько НДС нужно заплатить за 5 марта 2017 года",
"vat_payable_forecast"
);
expect(extracted.extracted_filters.period_from).toBe("2017-01-01");
expect(extracted.extracted_filters.period_to).toBe("2017-03-05");
expect(extracted.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast");
});
it("derives VAT forecast quarter-to-date window when strict as-of cue is present", () => {
const extracted = extractAddressFilters(
"сколько НДС нужно заплатить по состоянию на 15 марта 2020 года",
"vat_payable_forecast"
);
expect(extracted.extracted_filters.period_from).toBe("2020-01-01");
expect(extracted.extracted_filters.period_to).toBe("2020-03-15");
expect(extracted.warnings).toContain("period_to_derived_from_as_of_date_for_vat_forecast");
});
it("defaults as_of_date for documents_forming_balance when date is omitted", () => {
const result = extractAddressFilters("which documents form balance for account 62", "documents_forming_balance");
expect(result.extracted_filters.account).toBe("62");
expect(result.extracted_filters.as_of_date).toBeDefined();
expect(result.missing_required_filters).toEqual([]);
});
it("cuts period tail from counterparty anchor", () => {
const result = extractAddressFilters(
"Покажи документы по контрагенту test_cp с 2020-07-01 по 2020-07-31",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("test_cp");
expect(result.extracted_filters.period_from).toBe("2020-07-01");
expect(result.extracted_filters.period_to).toBe("2020-07-31");
});
it("cuts all-time tail from counterparty anchor and skips 90-day default window", () => {
const result = extractAddressFilters(
"Покажи документы по контрагенту тестовый за все время",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("тестовый");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
it("keeps all-time period by default for counterparty docs query without explicit window", () => {
const result = extractAddressFilters(
"Покажи документы по контрагенту тестовый",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("тестовый");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
it("extracts counterparty from company phrase and derives year period", () => {
const result = extractAddressFilters(
"Какие документы доступны по компании СВК за 2021 год?",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
expect(result.extracted_filters.period_from).toBe("2021-01-01");
expect(result.extracted_filters.period_to).toBe("2021-12-31");
expect(result.warnings).toContain("period_derived_from_year_phrase");
});
it("extracts counterparty from supplier phrase and derives month period", () => {
const result = extractAddressFilters(
"Покажи документы по поставщику Альфа за июль 2020",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("Альфа");
expect(result.extracted_filters.period_from).toBe("2020-07-01");
expect(result.extracted_filters.period_to).toBe("2020-07-31");
expect(result.warnings).toContain("period_derived_from_month_phrase");
});
it("cuts period-end tail from counterparty anchor and keeps as_of for open-items query", () => {
const result = extractAddressFilters(
"Покажи хвосты по контрагенту СВК на конец периода 2020-12-31",
"open_items_by_counterparty_or_contract"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
expect(result.extracted_filters.as_of_date).toBe("2020-12-31");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).toContain("period_window_cleared_for_as_of_intent");
});
it("cuts report-date tail from counterparty anchor and keeps clean as_of filter", () => {
const result = extractAddressFilters(
"Показать незакрытые записи для контрагента 'СВК' на дату отчетности 2020-12-31",
"open_items_by_counterparty_or_contract"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
expect(result.extracted_filters.as_of_date).toBe("2020-12-31");
expect(String(result.extracted_filters.counterparty ?? "").toLowerCase()).not.toContain("отчетности");
});
it("derives month period for balance snapshot from 'на май 2020'", () => {
const result = extractAddressFilters("Какой остаток по счету 60 на май 2020", "account_balance_snapshot");
expect(result.extracted_filters.account).toBe("60");
expect(result.extracted_filters.period_from).toBe("2020-05-01");
expect(result.extracted_filters.period_to).toBe("2020-05-31");
expect(result.extracted_filters.as_of_date).toBe("2020-05-31");
expect(result.warnings).toContain("period_derived_from_month_phrase");
expect(result.warnings).toContain("as_of_date_derived_from_period_to");
});
it("derives month period for balance snapshot from 'на 2020.05'", () => {
const result = extractAddressFilters("Какой остаток по счету 60 на 2020.05", "account_balance_snapshot");
expect(result.extracted_filters.account).toBe("60");
expect(result.extracted_filters.period_from).toBe("2020-05-01");
expect(result.extracted_filters.period_to).toBe("2020-05-31");
expect(result.extracted_filters.as_of_date).toBe("2020-05-31");
expect(result.warnings).toContain("period_derived_from_month_phrase");
expect(result.warnings).toContain("as_of_date_derived_from_period_to");
});
it("derives month period for balance snapshot from 'на 2020 май'", () => {
const result = extractAddressFilters("Какой остаток по счету 60 на 2020 май", "account_balance_snapshot");
expect(result.extracted_filters.account).toBe("60");
expect(result.extracted_filters.period_from).toBe("2020-05-01");
expect(result.extracted_filters.period_to).toBe("2020-05-31");
expect(result.extracted_filters.as_of_date).toBe("2020-05-31");
expect(result.warnings).toContain("period_derived_from_month_phrase");
expect(result.warnings).toContain("as_of_date_derived_from_period_to");
});
it("extracts dotted account by heuristic for docs-forming phrasing without 'счет' keyword", () => {
const result = extractAddressFilters(
"раскрой остаток 60.01 по документам на конец июля 2020",
"documents_forming_balance"
);
expect(result.extracted_filters.account).toBe("60.01");
expect(result.extracted_filters.as_of_date).toBe("2020-07-31");
expect(result.warnings).toContain("account_anchor_derived_from_heuristic_token");
});
it("extracts dotted account by heuristic for short balance slang", () => {
const result = extractAddressFilters("скока по 60.02 на конец 2020-12", "account_balance_snapshot");
expect(result.extracted_filters.account).toBe("60.02");
expect(result.extracted_filters.as_of_date).toBe("2020-12-31");
expect(result.warnings).toContain("account_anchor_derived_from_heuristic_token");
});
it("does not derive counterparty from follow-up filler token in bank phrase", () => {
const result = extractAddressFilters("а теперь банковские операции", "bank_operations_by_counterparty");
expect(result.extracted_filters.counterparty).toBeUndefined();
});
it("keeps compact account for docs-forming follow-up and avoids fake counterparty anchor", () => {
const result = extractAddressFilters("раскрой 62.01 документами на ту же дату", "documents_forming_balance");
expect(result.extracted_filters.account).toBe("62.01");
expect(result.extracted_filters.counterparty).toBeUndefined();
});
it("drops accidental account for non-account intent without explicit account cue", () => {
const result = extractAddressFilters("покажи банк операции по свк за 2020", "bank_operations_by_counterparty");
expect(result.extracted_filters.account).toBeUndefined();
});
it("extracts leading counterparty token for short bank phrase", () => {
const result = extractAddressFilters("свк списания/поступления за 2020", "bank_operations_by_counterparty");
expect(result.extracted_filters.counterparty).toBe("свк");
expect(
result.warnings.includes("counterparty_anchor_derived_from_leading_token") ||
result.warnings.includes("counterparty_anchor_derived_from_free_text_heuristic")
).toBe(true);
});
it("treats 'за весь период' as all-time hint and does not force 90-day default", () => {
const result = extractAddressFilters(
"Покажи банковские операции по клиенту Бета за весь период",
"bank_operations_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("Бета");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
it("extracts loose by-anchor and year period for short slang docs phrase", () => {
const result = extractAddressFilters(
"какие доки есть по свк за 2021",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.period_from).toBe("2021-01-01");
expect(result.extracted_filters.period_to).toBe("2021-12-31");
expect(result.warnings).toContain("counterparty_anchor_derived_from_loose_by_phrase");
expect(result.warnings).toContain("period_derived_from_year_phrase");
});
it("extracts implicit counterparty and short-year period for typo slang docs phrase", () => {
const result = extractAddressFilters(
"свк доки за 20год покеж",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
expect(result.warnings).toContain("counterparty_anchor_derived_from_implicit_phrase");
expect(result.warnings).toContain("period_derived_from_year_phrase");
});
it("does not use filler token 'есть' as counterparty when explicit shorthand anchor exists", () => {
const result = extractAddressFilters("какие у свк есть доки за 2020?", "list_documents_by_counterparty");
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.counterparty).not.toBe("есть");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
});
it("extracts free-text counterparty and relaxed short-year period from noisy phrase", () => {
const result = extractAddressFilters(
"свк 20 год - покажи доки плс",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
expect(result.warnings).toContain("counterparty_anchor_derived_from_free_text_heuristic");
expect(result.warnings).toContain("period_derived_from_year_phrase");
expect(result.extracted_filters.counterparty).not.toBe("плс");
});
it("extracts short ordinal year period from noisy docs phrase", () => {
const result = extractAddressFilters(
"бля епт покажи доки по свк за 20-й",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
expect(result.warnings).toContain("period_derived_from_year_phrase");
});
it("extracts short bare year period from follow-up phrase", () => {
const result = extractAddressFilters("теперь за 21", "counterparty_activity_lifecycle");
expect(result.extracted_filters.period_from).toBe("2021-01-01");
expect(result.extracted_filters.period_to).toBe("2021-12-31");
expect(result.warnings).toContain("period_derived_from_year_phrase");
});
it("does not use action verb as counterparty when phrase is 'Показать документы <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_free_text_heuristic" ||
warning === "counterparty_anchor_derived_from_implicit_phrase"
)
).toBe(true);
expect(result.warnings).toContain("period_derived_from_year_phrase");
});
it("does not treat transliterated filler verb as counterparty in docy phrase", () => {
const result = extractAddressFilters(
"svk poka docy za 2020",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("svk");
expect(result.extracted_filters.counterparty).not.toBe("poka");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
});
it("repairs mojibake phrase before extracting counterparty filters", () => {
const result = extractAddressFilters(
"Показать документы СВК за 2020 год.",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
});
it("extracts explicit year range period from phrase", () => {
const result = extractAddressFilters(
"Какие документы по СВК за 2000 - 2025 год?",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
expect(result.extracted_filters.period_from).toBe("2000-01-01");
expect(result.extracted_filters.period_to).toBe("2025-12-31");
expect(result.warnings).toContain("period_derived_from_year_range_phrase");
});
it("extracts contract and year period for contract document list", () => {
const result = extractAddressFilters(
"Покажи документы по договору 19/15 за 2020 год",
"list_documents_by_contract"
);
expect(result.extracted_filters.contract).toBe("19/15");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
expect(result.warnings).toContain("period_derived_from_year_phrase");
});
it("extracts contracts-by-counterparty anchor with numeric suffix from loose 'по ...' phrase", () => {
const result = extractAddressFilters(
"покажи договора все по жуковке 51",
"list_contracts_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("жуковке 51");
expect(result.extracted_filters.contract).toBeUndefined();
expect(result.missing_required_filters).toEqual([]);
expect(result.warnings).toContain("counterparty_anchor_derived_from_loose_by_phrase");
});
it("cuts trailing as-of date from contract anchor", () => {
const result = extractAddressFilters(
"Покажи документы по договору 1-ПМ/2020 на дату 31.07.2020",
"list_documents_by_contract"
);
expect(result.extracted_filters.contract).toBe("1-ПМ/2020");
expect(result.extracted_filters.period_from).toBe("2020-07-01");
expect(result.extracted_filters.period_to).toBe("2020-07-31");
});
it("does not force 90-day default window for by-contract query without explicit period", () => {
const result = extractAddressFilters(
"Покажи документы по договору 19/15",
"list_documents_by_contract"
);
expect(result.extracted_filters.contract).toBe("19/15");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
it("extracts heuristic contract token for noisy contract phrase", () => {
const result = extractAddressFilters(
"доки 19/15 за 2020",
"list_documents_by_contract"
);
expect(result.extracted_filters.contract).toBe("19/15");
expect(result.warnings).toContain("contract_anchor_derived_from_heuristic_token");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
});
it("trims english year tail from contract anchor", () => {
const result = extractAddressFilters(
"docs by contract 19/15 year 2020",
"list_documents_by_contract"
);
expect(result.extracted_filters.contract).toBe("19/15");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
});
it("trims trailing separated year from contract anchor", () => {
const result = extractAddressFilters(
"docs by contract 19/15 2020",
"list_documents_by_contract"
);
expect(result.extracted_filters.contract).toBe("19/15");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2020-12-31");
});
it("trims explanatory tail after contract token", () => {
const result = extractAddressFilters(
"документы по договору 19/15 выведите связанные документы",
"list_documents_by_contract"
);
expect(result.extracted_filters.contract).toBe("19/15");
});
it("extracts multiline year range period from phrase", () => {
const result = extractAddressFilters(
"Какие документы по СВК за 2000 - 2025\n год?",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("СВК");
expect(result.extracted_filters.period_from).toBe("2000-01-01");
expect(result.extracted_filters.period_to).toBe("2025-12-31");
expect(result.warnings).toContain("period_derived_from_year_range_phrase");
expect(result.warnings).not.toContain("period_derived_from_year_phrase");
});
it("extracts russian year range period from 'с ... по ...' phrase", () => {
const result = extractAddressFilters(
"какие есть доки по свк с 2020 по 2025 год",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.period_from).toBe("2020-01-01");
expect(result.extracted_filters.period_to).toBe("2025-12-31");
expect(result.warnings).toContain("period_derived_from_year_range_phrase");
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
it("treats 'за любой период' as all-time hint and keeps loose by-anchor", () => {
const result = extractAddressFilters(
"за любой период есть что-то по свк?",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("свк");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).toContain("counterparty_anchor_derived_from_loose_by_phrase");
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
});
describe("address query limited taxonomy and stage diagnostics", () => {
it("routes period coverage profile question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("За какие годы в базе есть данные?");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_mode).toBe("address_query");
expect(result?.debug.detected_intent).toBe("period_coverage_profile");
expect(result?.debug.selected_recipe).toBe("address_period_coverage_profile_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type);
});
it("does not rewrite active-month management question into bank-ops counterparty lane", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Какой месяц самый активный по количеству операций?");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("period_coverage_profile");
expect(result?.debug.selected_recipe).toBe("address_period_coverage_profile_v1");
expect(result?.debug.extracted_filters.counterparty).toBeUndefined();
expect(["FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type);
});
it("routes document+section profile question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Какие типы документов используются чаще всего в базе?");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_mode).toBe("address_query");
expect(result?.debug.detected_intent).toBe("document_type_and_account_section_profile");
expect(result?.debug.selected_recipe).toBe("address_document_type_and_account_section_profile_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type);
});
it("routes counterparty population question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Сколько всего уникальных контрагентов в базе?");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("counterparty_population_and_roles");
expect(result?.debug.selected_recipe).toBe("address_counterparty_population_roles_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type);
});
it("routes contract usage overview question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Сколько всего договоров заведено и сколько из них реально использовались?");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("contract_usage_overview");
expect(result?.debug.selected_recipe).toBe("address_contract_usage_overview_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_SUMMARY", "LIMITED_WITH_REASON"]).toContain(result?.response_type);
});
it("routes customer value question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("какие клиенты самые доходные, выдай топ-20");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes highest inflow slang wording into customer value aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("какие приходы самые высокие за все время");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes typo highest-check wording into customer value aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("с каких кликентов самый высокий чек");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes top counterparty slang wording into customer value aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("какой самый жирный контрагент у нее? кто больше платит денег");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
expect(result?.debug.extracted_filters.counterparty).toBeUndefined();
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes supplier payout question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("кому мы больше всего сгрузили денег, топ-20 поставщиков");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("supplier_payouts_profile");
expect(result?.debug.selected_recipe).toBe("address_supplier_payouts_profile_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes contract value question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("договоры по обороту ранкни и дай топ-20");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("contract_usage_and_value");
expect(result?.debug.selected_recipe).toBe("address_contract_usage_and_value_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes top contract wording with 'контракт' into contract value aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("по альтернативе вопрос. какой самый жирный контракт за всю историю у нее?");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("contract_usage_and_value");
expect(result?.debug.selected_recipe).toBe("address_contract_usage_and_value_v1");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes revenue-total slang wording into customer value aggregate recipe (no account-missing fallback)", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("скока денег альтернатива заработала за 22 год");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
expect(result?.debug.missing_required_filters).not.toContain("account");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes overall-turnover wording into customer value aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("какие общие обороты за все время");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes VAT payment forecast wording into dedicated VAT forecast recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("какой прогноз оплаты ндс за 12 мая 2020");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("vat_payable_forecast");
expect(result?.debug.selected_recipe).toBe("address_vat_payable_forecast_v1");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(result?.debug.extracted_filters.counterparty).toBeUndefined();
});
it("routes colloquial VAT payable estimate wording into VAT forecast recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("vat_payable_forecast");
expect(result?.debug.selected_recipe).toBe("address_vat_payable_forecast_v1");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
});
it("routes customer lifecycle question into dedicated aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Какие заказчики работали с нами в 2020 году?");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle");
expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes roster-style customer wording into lifecycle aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("кто у нас заказчики вообще");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle");
expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes one-time counterparties wording into lifecycle aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Какие контрагенты работали с нами только один раз за все время?");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("counterparty_activity_lifecycle");
expect(result?.debug.selected_recipe).toBe("address_counterparty_activity_lifecycle_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes stale contracts wording into contract usage overview recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Какие договоры давно не использовались?");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("contract_usage_overview");
expect(result?.debug.selected_recipe).toBe("address_contract_usage_overview_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes multi-contract counterparties wording into contract usage and value recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Покажи контрагентов с несколькими договорами и какие из договоров активны.");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("contract_usage_and_value");
expect(result?.debug.selected_recipe).toBe("address_contract_usage_and_value_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("returns missing_anchor for open items without concrete counterparty/contract anchor", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("show open items by contract");
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.debug.limited_reason_category).toBe("missing_anchor");
expect(result?.debug.mcp_call_status).toBe("skipped");
});
it("does not return fallback factual rows for unmatched open-items contract anchor", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Покажи открытые позиции по договору 9999/NOPE");
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.reply_type).toBe("partial_coverage");
expect(result?.debug.detected_intent).toBe("open_items_by_counterparty_or_contract");
expect(result?.debug.rows_matched).toBe(0);
expect(["empty_match", "missing_anchor"]).toContain(result?.debug.limited_reason_category);
expect(String(result?.assistant_reply ?? "")).not.toContain("Собраны открытые позиции");
});
it("does not return broad fallback document list when counterparty anchor is not matched", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("покажи документы все по жуковке 51");
expect(result?.handled).toBe(true);
expect(String(result?.assistant_reply ?? "")).not.toContain("Точный якорь не подтвердился");
expect(String(result?.assistant_reply ?? "")).not.toContain("якорь не подтвердился");
if (result?.reply_type === "partial_coverage") {
expect(result?.debug.rows_matched).toBe(0);
if (String(result?.debug.match_failure_reason ?? "").includes("counterparty_anchor_not_matched")) {
expect(String(result?.assistant_reply ?? "")).toContain("уточните точное имя контрагента");
}
}
});
it("does not keep report-date phrase inside open-items counterparty anchor", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("покажи хвосты по контрагенту СВК на 2020-12-31");
expect(result?.handled).toBe(true);
expect(String(result?.debug.extracted_filters.counterparty ?? "").toLowerCase()).toContain("свк");
expect(String(result?.debug.extracted_filters.counterparty ?? "").toLowerCase()).not.toContain("дата отчетности");
expect(String(result?.debug.anchor_value_raw ?? "").toLowerCase()).not.toContain("дата отчетности");
if (result?.reply_type === "partial_coverage") {
expect(result?.debug.limited_reason_category).not.toBe("missing_anchor");
}
});
it("routes contract document list intent into address recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("show documents by contract 19/15");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_documents_by_contract");
expect(result?.debug.selected_recipe).toBe("address_documents_by_contract_v1");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
});
it("routes contracts-by-counterparty intent into dedicated catalog recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("покажи договора все по жуковке 51");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_contracts_by_counterparty");
expect(result?.debug.selected_recipe).toBe("address_contracts_by_counterparty_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON"]).toContain(result?.response_type);
});
it("routes bank operations by contract intent into address recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Покажи банковские операции по договору 19/15");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("bank_operations_by_contract");
expect(result?.debug.selected_recipe).toBe("address_bank_operations_by_contract_v1");
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
});
it("includes resolver and row-stage diagnostics", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("which documents form balance for account 62 as of 2020-07-31");
expect(result?.handled).toBe(true);
expect(["LIMITED_WITH_REASON", "FACTUAL_LIST"]).toContain(result?.response_type);
expect(result?.debug.anchor_type).toBe("account");
expect(result?.debug.rows_fetched).toBeTypeOf("number");
expect(result?.debug.raw_rows_received).toBeTypeOf("number");
expect(result?.debug.rows_after_account_scope).toBeTypeOf("number");
expect(result?.debug.rows_materialized).toBeTypeOf("number");
expect(result?.debug.rows_after_recipe_filter).toBeTypeOf("number");
expect(result?.debug.rows_matched).toBeTypeOf("number");
expect(["strict", "preferred"]).toContain(result?.debug.account_scope_mode);
expect(result?.debug.account_scope_fallback_applied).toBeTypeOf("boolean");
expect(result?.debug.mcp_call_status_legacy).toBeDefined();
expect(result?.debug.match_failure_stage).toBeDefined();
expect([
"error",
"no_raw_rows",
"raw_rows_received_but_not_materialized",
"materialized_but_not_anchor_matched",
"materialized_but_filtered_out_by_recipe",
"materialized_but_not_matched",
"matched_non_empty"
]).toContain(result?.debug.mcp_call_status);
expect(result?.debug.raw_row_keys_sample).toBeDefined();
expect(result?.debug.materialization_drop_reason).toBeDefined();
expect(result?.debug.account_scope_fields_checked).toBeDefined();
expect(result?.debug.account_scope_match_strategy).toBe("account_code_regex_plus_alias_map_v1");
expect(result?.debug.account_scope_drop_reason).toBeDefined();
});
it("keeps short slang docs request in address lane (no deep fallback)", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("какие доки есть по свк за 2021");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_mode).toBe("address_query");
expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty");
expect(result?.debug.extracted_filters.counterparty).toBe("свк");
expect(result?.debug.extracted_filters.period_from).toBe("2021-01-01");
expect(result?.debug.extracted_filters.period_to).toBe("2021-12-31");
});
it("keeps typo slang docs request in address lane and extracts implicit anchor", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("свк доки за 20год покеж");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_mode).toBe("address_query");
expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty");
expect(result?.debug.extracted_filters.counterparty).toBe("свк");
expect(result?.debug.extracted_filters.period_from).toBe("2020-01-01");
expect(result?.debug.extracted_filters.period_to).toBe("2020-12-31");
});
it("keeps noisy docs request in address lane and ignores slang tail token", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("свк 20 год - покажи доки плс");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_mode).toBe("address_query");
expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty");
expect(result?.debug.extracted_filters.counterparty).toBe("свк");
expect(result?.debug.extracted_filters.counterparty).not.toBe("плс");
expect(result?.debug.extracted_filters.period_from).toBe("2020-01-01");
expect(result?.debug.extracted_filters.period_to).toBe("2020-12-31");
});
it("auto-broadens out-of-window period and returns available factual rows", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("Какие документы по СВК за 2000 год?");
expect(result?.handled).toBe(true);
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON"]).toContain(result?.response_type);
if (result?.response_type === "FACTUAL_LIST") {
expect(result?.debug.limited_reason_category).toBeNull();
}
});
});
describe("address decompose stage follow-up carryover", () => {
it("keeps slang all-customers-all-time wording in address lane via resolved intent fallback", () => {
const result = runAddressDecomposeStage("выведи всех заков за все время", null);
expect(result).not.toBeNull();
expect(result?.mode.mode).toBe("address_query");
expect(result?.intent.intent).toBe("counterparty_activity_lifecycle");
});
it("keeps churn wording with year in address lane via resolved intent fallback", () => {
const result = runAddressDecomposeStage("кто был активен в 2020 и потом отвалился", null);
expect(result).not.toBeNull();
expect(result?.mode.mode).toBe("address_query");
expect(result?.intent.intent).toBe("counterparty_activity_lifecycle");
expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31");
});
it("uses short bare year in follow-up period switch", () => {
const result = runAddressDecomposeStage("теперь за 21", {
previous_intent: "counterparty_activity_lifecycle",
previous_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "unknown",
previous_anchor_value: null
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("counterparty_activity_lifecycle");
expect(result?.filters.extracted_filters.period_from).toBe("2021-01-01");
expect(result?.filters.extracted_filters.period_to).toBe("2021-12-31");
});
it("keeps lifecycle follow-up phrasing with referential pointer and inherits period", () => {
const result = runAddressDecomposeStage("А кто из них новые?", {
previous_intent: "counterparty_activity_lifecycle",
previous_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "unknown",
previous_anchor_value: null
});
expect(result).not.toBeNull();
expect(result?.mode.mode).toBe("address_query");
expect(result?.intent.intent).toBe("counterparty_activity_lifecycle");
expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31");
expect(result?.baseReasons).toContain("address_followup_context_applied");
});
it("keeps short period follow-up in address lane and preserves previous counterparty anchor", () => {
const result = runAddressDecomposeStage("а теперь только за май 2020", {
previous_intent: "list_documents_by_counterparty",
previous_filters: {
counterparty: "свк",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Группа СВК"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("list_documents_by_counterparty");
expect(result?.filters.extracted_filters.counterparty).toBe("свк");
expect(result?.filters.extracted_filters.period_from).toBe("2020-05-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-05-31");
expect(result?.baseReasons).toContain("address_followup_context_applied");
});
it("inherits organization scope from follow-up context when organization is omitted in user text", () => {
const result = runAddressDecomposeStage("покажи документы по свк за 2020", {
previous_intent: "list_documents_by_counterparty",
previous_filters: {
organization: "ООО Альтернатива Плюс",
counterparty: "свк",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "свк"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("list_documents_by_counterparty");
expect(result?.filters.extracted_filters.organization).toBe("ООО Альтернатива Плюс");
expect(result?.baseReasons).toContain("organization_from_followup_context");
});
it("inherits as_of_date from previous period for same-date balance follow-up", () => {
const result = runAddressDecomposeStage("а по счету 60.01 на ту же дату", {
previous_intent: "list_documents_by_counterparty",
previous_filters: {
counterparty: "свк",
period_from: "2020-05-01",
period_to: "2020-05-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Группа СВК"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("account_balance_snapshot");
expect(result?.filters.extracted_filters.account).toBe("60.01");
expect(result?.filters.extracted_filters.as_of_date).toBe("2020-05-31");
expect(result?.baseReasons).toContain("as_of_date_from_followup_context");
expect(result?.baseReasons).toContain("address_followup_context_applied");
});
it("keeps contract scope when follow-up asks for bank operations without explicit anchor", () => {
const result = runAddressDecomposeStage("а теперь банковские операции", {
previous_intent: "list_documents_by_contract",
previous_filters: {
contract: "19/15",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "contract",
previous_anchor_value: "19/15"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("bank_operations_by_contract");
expect(result?.filters.extracted_filters.contract).toBe("19/15");
expect(result?.baseReasons).toContain("intent_adjusted_to_contract_followup_context");
});
it("replaces noisy follow-up contract anchor with previous contract from context", () => {
const result = runAddressDecomposeStage("а документы по этому же договору за тот же период", {
previous_intent: "bank_operations_by_contract",
previous_filters: {
contract: "19/15",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "contract",
previous_anchor_value: "19/15"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("list_documents_by_contract");
expect(result?.filters.extracted_filters.contract).toBe("19/15");
expect(
result?.baseReasons?.includes("contract_replaced_from_followup_context") ||
result?.baseReasons?.includes("contract_from_followup_context")
).toBe(true);
});
it("replaces noisy referential counterparty anchor with previous counterparty from context", () => {
const result = runAddressDecomposeStage("а теперь документы по нему", {
previous_intent: "bank_operations_by_counterparty",
previous_filters: {
counterparty: "свк",
period_from: "2020-11-01",
period_to: "2020-11-30"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "свк"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("list_documents_by_counterparty");
expect(result?.filters.extracted_filters.counterparty).toBe("свк");
expect(
result?.baseReasons?.includes("counterparty_replaced_from_followup_context") ||
result?.baseReasons?.includes("counterparty_from_followup_context")
).toBe(true);
});
it("replaces 'кроме этого документа...' pseudo-anchor with previous counterparty from follow-up context", () => {
const result = runAddressDecomposeStage("кроме этого документа есть еще чтото?", {
previous_intent: "list_documents_by_counterparty",
previous_filters: {
counterparty: "ТСЖ \\Жуковка 51\\"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "ТСЖ \\Жуковка 51\\"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("list_documents_by_counterparty");
expect(result?.filters.extracted_filters.counterparty).toBe("ТСЖ \\Жуковка 51\\");
expect(
result?.baseReasons?.includes("counterparty_replaced_from_followup_context") ||
result?.baseReasons?.includes("counterparty_from_followup_context")
).toBe(true);
});
it("promotes open-items intent from follow-up wording with inherited contract anchor", () => {
const result = runAddressDecomposeStage("а теперь открытые позиции по нему", {
previous_intent: "bank_operations_by_contract",
previous_filters: {
contract: "19/15",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "contract",
previous_anchor_value: "19/15"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("open_items_by_counterparty_or_contract");
expect(result?.filters.extracted_filters.contract).toBe("19/15");
expect(result?.baseReasons).toContain("open_items_from_followup_context");
});
it("keeps balance family in follow-up when user gives compact account token", () => {
const result = runAddressDecomposeStage("вернись на 2020-12-31 по 60", {
previous_intent: "documents_forming_balance",
previous_filters: {
account: "62",
as_of_date: "2020-05-31",
period_from: "2020-05-01",
period_to: "2020-05-31"
},
previous_anchor_type: "account",
previous_anchor_value: "62"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("account_balance_snapshot");
expect(result?.filters.extracted_filters.account).toBe("60");
expect(result?.filters.extracted_filters.as_of_date).toBe("2020-12-31");
expect(result?.baseReasons).toContain("intent_adjusted_to_balance_followup_context");
});
it("does not downgrade inherited follow-up anchor to missing_anchor when period has no rows", async () => {
const service = new AddressQueryService();
const seed = await service.tryHandle("покажи документы по свк за 2020");
expect(seed?.handled).toBe(true);
const followup = await service.tryHandle("а теперь только за май 2020", {
followupContext: {
previous_intent: (seed?.debug.detected_intent as any) ?? "list_documents_by_counterparty",
previous_filters: seed?.debug.extracted_filters,
previous_anchor_type: (seed?.debug.anchor_type as any) ?? "counterparty",
previous_anchor_value: seed?.debug.anchor_value_resolved ?? seed?.debug.anchor_value_raw ?? null
}
});
expect(followup?.handled).toBe(true);
if (followup?.reply_type === "partial_coverage") {
expect(followup?.debug.limited_reason_category).not.toBe("missing_anchor");
}
});
it("keeps VAT explain follow-up in address lane and inherits previous period window", () => {
const result = runAddressDecomposeStage("почему прогноз к уплате 0?", {
previous_intent: "vat_payable_forecast",
previous_filters: {
period_from: "2020-03-01",
period_to: "2020-03-31"
},
previous_anchor_type: "unknown",
previous_anchor_value: null
});
expect(result).not.toBeNull();
expect(result?.mode.mode).toBe("address_query");
expect(result?.intent.intent).toBe("vat_payable_forecast");
expect(result?.filters.extracted_filters.period_from).toBe("2020-03-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-03-31");
expect(
result?.baseReasons?.includes("address_mode_from_followup_context") ||
result?.baseReasons?.includes("intent_from_followup_context")
).toBe(true);
});
});
describe("address recipe catalog counterparty filtering", () => {
it("selects period coverage profile recipe and keeps aggregate markers", () => {
const selected = selectAddressRecipe("period_coverage_profile", {});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_period_coverage_profile_v1");
expect(plan.limit).toBeGreaterThanOrEqual(600);
expect(plan.query).toContain("MIN_DATE");
expect(plan.query).toContain("YEAR_DOCS");
expect(plan.query).toContain("MONTH_OPS");
expect(plan.query).not.toContain("ТЕКУЩАЯДАТА()");
});
it("selects document+section profile recipe and keeps aggregate markers", () => {
const selected = selectAddressRecipe("document_type_and_account_section_profile", {});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_document_type_and_account_section_profile_v1");
expect(plan.limit).toBeGreaterThanOrEqual(800);
expect(plan.query).toContain("DOC_TYPE_DOCS");
expect(plan.query).toContain("SECTION_DT_OPS");
expect(plan.query).toContain("SECTION_KT_OPS");
expect(plan.query).toContain("СГРУППИРОВАТЬ ПО\n Движения.СчетДт");
expect(plan.query).not.toContain("ЛЕВ(Движения.СчетДт.Код, 2)");
});
it("selects counterparty population recipe and keeps aggregate markers", () => {
const selected = selectAddressRecipe("counterparty_population_and_roles", {});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_counterparty_population_roles_v1");
expect(plan.query).toContain("CP_TOTAL");
expect(plan.query).toContain("CP_CUSTOMER_ACTIVE");
expect(plan.query).toContain("CP_SUPPLIER_ACTIVE");
expect(plan.query).toContain("CP_MIXED_ACTIVE");
expect(plan.query).toContain("CP_ACTIVE_UNION");
});
it("selects contract usage overview recipe and keeps aggregate markers", () => {
const selected = selectAddressRecipe("contract_usage_overview", {});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_contract_usage_overview_v1");
expect(plan.query).toContain("CT_TOTAL");
expect(plan.query).toContain("CT_USED");
expect(plan.query).toContain("ДоговорКонтрагента");
});
it("selects customer value recipe and keeps top-20 default", () => {
const selected = selectAddressRecipe("customer_revenue_and_payments", {});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_customer_revenue_and_payments_v1");
expect(plan.limit).toBe(20);
expect(plan.query).toContain("ПоступлениеНаРасчетныйСчет");
expect(plan.query).toContain("БанкПоступление.ДоговорКонтрагента");
});
it("selects supplier payouts recipe and keeps top-20 default", () => {
const selected = selectAddressRecipe("supplier_payouts_profile", {});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_supplier_payouts_profile_v1");
expect(plan.limit).toBe(20);
expect(plan.query).toContain("СписаниеСРасчетногоСчета");
expect(plan.query).toContain("БанкСписание.ДоговорКонтрагента");
});
it("selects contract value recipe and keeps top-20 default", () => {
const selected = selectAddressRecipe("contract_usage_and_value", {});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_contract_usage_and_value_v1");
expect(plan.limit).toBe(20);
expect(plan.query).toContain("CT_VALUE_IN");
expect(plan.query).toContain("CT_VALUE_OUT");
expect(plan.query).toContain("ДоговорКонтрагента");
});
it("selects contracts-by-counterparty recipe from contract catalog", () => {
const selected = selectAddressRecipe("list_contracts_by_counterparty", {
counterparty: "Жуковка 51"
});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
counterparty: "Жуковка 51"
});
expect(plan.recipe.recipe_id).toBe("address_contracts_by_counterparty_v1");
expect(plan.query).toContain("Справочник.ДоговорыКонтрагентов");
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Договоры.Владелец)");
});
it("selects counterparty lifecycle recipe and keeps activity marker", () => {
const selected = selectAddressRecipe("counterparty_activity_lifecycle", {});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_counterparty_activity_lifecycle_v1");
expect(plan.query).toContain("CP_CUSTOMER_ACTIVITY");
expect(plan.query).toContain("ПоступлениеНаРасчетныйСчет");
});
it("boosts limit for all-time counterparty queries", () => {
const filters = extractAddressFilters(
"Покажи документы по контрагенту тестовый за все время",
"list_documents_by_counterparty"
).extracted_filters;
const selected = selectAddressRecipe("list_documents_by_counterparty", filters);
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.limit).toBe(1000);
});
it("supports ascending order plan for historical counterparty lookup", () => {
const selected = selectAddressRecipe("list_documents_by_counterparty", {
counterparty: "Жуковка 51",
sort: "period_asc"
});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
counterparty: "Жуковка 51",
sort: "period_asc"
});
expect(plan.query).toContain("УПОРЯДОЧИТЬ ПО");
expect(plan.query).toContain("Период ВОЗР");
});
it("boosts limit for english all-time counterparty queries", () => {
const filters = extractAddressFilters(
"show documents by counterparty test_cp for all time",
"list_documents_by_counterparty"
).extracted_filters;
const selected = selectAddressRecipe("list_documents_by_counterparty", filters);
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.limit).toBe(1000);
});
it("cuts english all-time tail from counterparty anchor", () => {
const result = extractAddressFilters(
"show documents by counterparty test_cp for all time",
"list_documents_by_counterparty"
);
expect(result.extracted_filters.counterparty).toBe("test_cp");
expect(result.extracted_filters.period_from).toBeUndefined();
expect(result.extracted_filters.period_to).toBeUndefined();
expect(result.warnings).not.toContain("period_defaulted_last_90_days");
});
it("boosts limit for account snapshot queries with explicit account", () => {
const filters = extractAddressFilters(
"Какой остаток по счету 60 на дату 2020-07-31",
"account_balance_snapshot"
).extracted_filters;
const selected = selectAddressRecipe("account_balance_snapshot", filters);
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.limit).toBe(200);
});
it("allows extended limit for open-items by contract intent", () => {
const selected = selectAddressRecipe("open_items_by_counterparty_or_contract", {
contract: "19/15",
limit: 1000
});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
contract: "19/15",
limit: 1000
});
expect(plan.limit).toBe(1000);
});
it("uses bank-doc profile with contract projection for open-items anchor matching", () => {
const selected = selectAddressRecipe("open_items_by_counterparty_or_contract", {
counterparty: "СВК",
as_of_date: "2020-12-31"
});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
counterparty: "СВК",
as_of_date: "2020-12-31"
});
expect(plan.query).toContain("Документ.СписаниеСРасчетногоСчета");
expect(plan.query).toContain("Документ.ПоступлениеНаРасчетныйСчет");
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор");
});
it("allows extended limit for open-contracts intent", () => {
const selected = selectAddressRecipe("list_open_contracts", {
as_of_date: "2020-12-31",
limit: 1000
});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
as_of_date: "2020-12-31",
limit: 1000
});
expect(plan.limit).toBe(1000);
});
it("injects account condition into movements query for account snapshot", () => {
const filters = extractAddressFilters(
"Какой остаток по счету 60 на дату 2020-07-31",
"account_balance_snapshot"
).extracted_filters;
const selected = selectAddressRecipe("account_balance_snapshot", filters);
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.query).toContain("Движения.СчетДт.Код");
expect(plan.query).toContain("ПОДОБНО \"60%\"");
});
it("injects subaccount condition variants into movements query for documents_forming_balance", () => {
const filters = extractAddressFilters(
"Какие документы формируют остаток по счету 60.01 на дату 2020-07-31",
"documents_forming_balance"
).extracted_filters;
const selected = selectAddressRecipe("documents_forming_balance", filters);
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.query).toContain("ПОДОБНО \"60.01%\"");
expect(plan.query).toContain("ПОДОБНО \"60.1%\"");
});
it("builds VAT forecast query with safe account-prefix checks instead of presentation-like clauses", () => {
const filters = extractAddressFilters(
"мож прикинусь плиз скока ндс надо заплатить на 15 марта 2020 года",
"vat_payable_forecast"
).extracted_filters;
const selected = selectAddressRecipe("vat_payable_forecast", filters);
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, \"\"), 1, 5) = \"68.02\"");
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 5) = \"68.02\"");
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, \"\"), 1, 4) = \"68.2\"");
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 4) = \"68.2\"");
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетДт.Код, \"\"), 1, 2) = \"19\"");
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Движения.СчетКт.Код, \"\"), 1, 2) = \"19\"");
expect(plan.query).not.toContain("ПРЕДСТАВЛЕНИЕ(Движения.СчетКт) ПОДОБНО");
expect(plan.query).not.toContain("ПРЕДСТАВЛЕНИЕ(Движения.СчетДт) ПОДОБНО");
});
});