NODEDC_1C/llm_normalizer/backend/tests/addressCounterpartyItemFlow...

466 lines
19 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 { afterEach, describe, expect, it, vi } from "vitest";
const { executeAddressMcpQueryMock } = vi.hoisted(() => ({
executeAddressMcpQueryMock: vi.fn()
}));
vi.mock("../src/services/addressMcpClient", async () => {
const actual = await vi.importActual<typeof import("../src/services/addressMcpClient")>(
"../src/services/addressMcpClient"
);
return {
...actual,
executeAddressMcpQuery: executeAddressMcpQueryMock
};
});
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
import { AddressQueryService } from "../src/services/addressQueryService";
import { composeFactualReply } from "../src/services/address_runtime/composeStage";
afterEach(() => {
executeAddressMcpQueryMock.mockReset();
vi.restoreAllMocks();
});
describe("counterparty shipment item flow and open-items routing", () => {
it("routes counterparty shipment item-flow wording to documents by counterparty", () => {
const result = resolveAddressIntent("что нам отгружал чепурнов? какой товар или услугу?");
expect(result.intent).toBe("list_documents_by_counterparty");
expect(result.reasons).toContain("counterparty_item_flow_signal_detected");
});
it("keeps plain Russian counterparty item-flow wording out of stale inventory context", async () => {
executeAddressMcpQueryMock
.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Counterparty: "Чепурнов П.Д.",
Registrator: "Чепурнов П.Д."
}
],
rows: [],
error: null
})
.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2022-01-20T12:00:03Z",
Registrator: "Поступление товаров и услуг 000000001 от 20.01.2022",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 890660,
Nomenclature: "Услуги по договору",
Counterparty: "Чепурнов П.Д.",
Contract: "Договор № 11/1 от 25.11.2020 г.",
Organization: 'ООО "Альтернатива Плюс"'
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("что нам отгружал чепурнов? какой товар или услугу?", {
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
target_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2026-04-24"
},
previous_anchor_type: "organization",
previous_anchor_value: 'ООО "Альтернатива Плюс"'
}
});
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty");
expect(result?.debug.selected_recipe).toBe("address_documents_by_counterparty_v1");
expect(String(result?.reply_text ?? "")).toContain("Чепурнов П.Д.");
expect(String(result?.reply_text ?? "")).toContain("Услуги по договору");
});
it("routes account 60 tails wording to open items intent", () => {
const result = resolveAddressIntent("хвосты покажи по счету 60 на август 2022");
expect(result.intent).toBe("open_items_by_counterparty_or_contract");
});
it("includes resolved full counterparty name in document reply", () => {
const reply = composeFactualReply(
"list_documents_by_counterparty",
[
{
period: "2022-08-15T00:00:00Z",
registrator: "Поступление товаров и услуг 000000123 от 15.08.2022",
account_dt: "41.01",
account_kt: "60.01",
amount: 12500,
analytics: ["Чепурнов П.Д.", "Основной договор"],
item: "Кабель силовой",
organization: 'ООО "Альтернатива Плюс"'
}
],
{
userMessage: "покажи все документы по чапурнову",
counterpartyHint: "Чепурнов П.Д."
}
);
expect(reply.text).toContain("Контрагент: Чепурнов П.Д.");
});
it("uses purchase document query for fuzzy counterparty item-flow wording", async () => {
executeAddressMcpQueryMock.mockImplementation(async (request?: { query?: string }) => {
const query = String(request?.query ?? "");
if (query.includes("Справочник.Контрагенты")) {
return {
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Counterparty: "Чепурнов П.Д.",
Registrator: "Чепурнов П.Д."
}
],
rows: [],
error: null
};
}
return {
fetched_rows: 2,
matched_rows: 2,
raw_rows: [
{
Period: "2020-03-10T00:00:00Z",
Registrator: "Поступление товаров и услуг 000000001 от 10.03.2020",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 12000,
Nomenclature: "Кабель силовой",
Counterparty: "Чепурнов П.Д.",
Contract: "Основной договор",
Quantity: 2,
Organization: 'ООО "Альтернатива Плюс"'
},
{
Period: "2020-03-14T00:00:00Z",
Registrator: "Поступление товаров и услуг 000000002 от 14.03.2020",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 5400,
Nomenclature: "Патч-корд",
Counterparty: "Чепурнов П.Д.",
Contract: "Основной договор",
Quantity: 10,
Organization: 'ООО "Альтернатива Плюс"'
}
],
rows: [],
error: null
};
});
const service = new AddressQueryService();
const result = await service.tryHandle(
"какие товары или услуги были отгружены нашей компании контрагентом чапурновым?"
);
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_LIST");
expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty");
expect(result?.debug.address_coverage_evidence_v1?.coverage_status).toBe("full");
expect(result?.debug.address_coverage_evidence_v1?.evidence_basis).toBe("matched_rows");
expect(result?.debug.address_truth_gate_v1?.truth_gate_status).toBe("full_confirmed");
expect(String(result?.reply_text ?? "")).toContain("Контрагент: Чепурнов П.Д.");
expect(String(result?.reply_text ?? "")).toContain("Позиции:");
expect(String(result?.reply_text ?? "")).toContain("Кабель силовой");
expect(String(result?.reply_text ?? "")).toContain("договор:");
expect(String(result?.reply_text ?? "")).toContain("дата:");
const query = String(executeAddressMcpQueryMock.mock.calls.at(-1)?.[0]?.query ?? "");
expect(query).toContain("Документ.ПоступлениеТоваровУслуг.Товары");
expect(query).toContain("Документ.ПоступлениеТоваровУслуг.Услуги");
expect(query).toContain("Товары.Ссылка.Контрагент.Наименование ПОДОБНО");
expect(query).not.toContain("контрагентом");
});
it("keeps resolved counterparty group name in user-facing document reply for svk wording", async () => {
executeAddressMcpQueryMock
.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Counterparty: "Группа СВК",
Registrator: "Группа СВК"
}
],
rows: [],
error: null
})
.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2021-11-10T12:00:07Z",
Registrator: "Поступление на расчетный счет 00000000013 от 10.11.2021 12:00:07",
AccountDt: "0",
AccountKt: "0",
Amount: 20000,
Counterparty: "Группа СВК",
Contract: "Договор № 1-ПМ/2020 от 05.06.2020",
Organization: 'ООО "Альтернатива Плюс"'
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("покажи документы по свк");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty");
expect(String(result?.reply_text ?? "")).toContain("Контрагент: Группа СВК");
expect(String(result?.reply_text ?? "")).not.toContain("Контрагент: Группа Найдено");
});
it("prefers requested full counterparty label when document rows only expose a generic group label", () => {
const reply = composeFactualReply(
"list_documents_by_counterparty",
[
{
period: "2021-11-10T12:00:07Z",
registrator: "Поступление на расчетный счет 00000000013 от 10.11.2021 12:00:07",
account_dt: "0",
account_kt: "0",
amount: 20000,
analytics: ["Группа", "Договор № 1-ПМ/2020 от 05.06.2020"],
organization: 'ООО "Альтернатива Плюс"'
}
],
{
userMessage: "а по свк",
counterpartyHint: "Группа СВК"
}
);
expect(reply.text).toContain("Контрагент: Группа СВК. Найдено документов: 1.");
expect(reply.text).not.toContain("Контрагент: Группа. Найдено документов");
});
it("keeps document follow-up answer compact for larger counterparty lists", () => {
const rows = Array.from({ length: 7 }, (_, index) => ({
period: `2021-11-${String(index + 1).padStart(2, "0")}T12:00:00Z`,
registrator: `Документ ${index + 1}`,
account_dt: "0",
account_kt: "0",
amount: 1000 + index,
analytics: ["Группа СВК", "Договор № 1-ПМ/2020"],
organization: "ООО Альтернатива Плюс"
}));
const reply = composeFactualReply("list_documents_by_counterparty", rows, {
counterpartyHint: "Группа СВК"
});
expect(reply.text).toContain("Контрагент: Группа СВК. Найдено документов: 7.");
expect(reply.text).toContain("Показаны первые 5 из 7 документов");
expect(reply.text).toContain("Документ 5");
expect(reply.text).not.toContain("Документ 6");
expect(reply.text.split("\n").filter((line) => line.startsWith("Контрагент:")).length).toBe(1);
});
it("keeps current resolved counterparty label over stale follow-up anchor during short retarget", async () => {
executeAddressMcpQueryMock
.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Counterparty: "Группа СВК",
Registrator: "Группа СВК"
}
],
rows: [],
error: null
})
.mockResolvedValueOnce({
fetched_rows: 2,
matched_rows: 2,
raw_rows: [
{
Period: "2021-11-10T12:00:07Z",
Registrator: "Поступление на расчетный счет 00000000013 от 10.11.2021 12:00:07",
AccountDt: "0",
AccountKt: "0",
Amount: 20000,
Counterparty: "Группа",
Contract: "Договор № 1-ПМ/2020 от 05.06.2020",
Organization: 'ООО "Альтернатива Плюс"'
},
{
Period: "2021-09-29T12:00:03Z",
Registrator: "Поступление на расчетный счет 00000000012 от 29.09.2021 12:00:03",
AccountDt: "0",
AccountKt: "0",
Amount: 50000,
Counterparty: "Группа",
Contract: "Договор № 1-ПМ/2020 от 05.06.2020",
Organization: 'ООО "Альтернатива Плюс"'
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("а по свк", {
followupContext: {
previous_intent: "list_documents_by_counterparty",
previous_filters: {
counterparty: "Чепурнов П.Д."
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Чепурнов П.Д."
}
});
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty");
expect(String(result?.reply_text ?? "")).toContain("Контрагент: Группа СВК");
expect(String(result?.reply_text ?? "")).not.toContain("Контрагент: Группа Найдено");
expect(String(result?.reply_text ?? "")).not.toContain("Контрагент: Чепурнов");
});
it("explains supplier payments and return when no supply rows are found", async () => {
executeAddressMcpQueryMock.mockImplementation(async (request?: { query?: string }) => {
const query = String(request?.query ?? "");
if (query.includes("Справочник.Контрагенты")) {
return {
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Counterparty: "Чепурнов П.Д.",
Registrator: "Чепурнов П.Д."
}
],
rows: [],
error: null
};
}
if (query.includes("Документ.ПоступлениеТоваровУслуг.Товары") || query.includes("Документ.ПоступлениеТоваровУслуг.Услуги")) {
return {
fetched_rows: 0,
matched_rows: 0,
raw_rows: [],
rows: [],
error: null
};
}
return {
fetched_rows: 3,
matched_rows: 3,
raw_rows: [
{
Period: "2021-06-11T12:00:01Z",
Registrator: "Списание с расчетного счета 00000000124 от 11.06.2021",
AccountDt: "60.02",
AccountKt: "51",
Amount: 119210,
SubcontoDt1: "Чепурнов П.Д.",
SubcontoDt2: "Договор № 11/1 от 25.11.2020",
SubcontoKt1: "ПАО СБЕРБАНК"
},
{
Period: "2021-05-18T12:00:00Z",
Registrator: "Списание с расчетного счета 00000000112 от 18.05.2021",
AccountDt: "60.02",
AccountKt: "51",
Amount: 180230,
SubcontoDt1: "Чепурнов П.Д.",
SubcontoDt2: "Договор № 11/1 от 25.11.2020",
SubcontoKt1: "ПАО СБЕРБАНК"
},
{
Period: "2022-01-20T12:00:03Z",
Registrator: "Поступление на расчетный счет 00000000001 от 20.01.2022",
AccountDt: "51",
AccountKt: "60.02",
Amount: 299440,
SubcontoKt1: "Чепурнов П.Д.",
SubcontoKt2: "Договор № 11/1 от 25.11.2020",
SubcontoDt1: "ПАО СБЕРБАНК"
}
],
rows: [],
error: null
};
});
const service = new AddressQueryService();
const result = await service.tryHandle(
"какие товары или услуги были отгружены нашей компании контрагентом чапурновым?"
);
expect(result?.handled).toBe(true);
expect(String(result?.reply_text ?? "")).toContain("Подтвержденных поставок товаров или услуг не найдено");
expect(String(result?.reply_text ?? "")).toContain("исходящих оплат поставщику");
expect(String(result?.reply_text ?? "")).toContain("возвратов от поставщика");
expect(String(result?.reply_text ?? "")).toContain("Договор:");
expect(result?.debug.address_coverage_evidence_v1?.coverage_status).toBe("blocked");
expect(result?.debug.address_coverage_evidence_v1?.evidence_basis).toBe("unknown");
expect(result?.debug.reasons).toContain("counterparty_item_flow_no_supply_but_bank_activity_explained");
expect(executeAddressMcpQueryMock.mock.calls.length).toBeGreaterThanOrEqual(2);
});
it("keeps account 60 tails in open-items route and mentions the account in reply", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2022-08-31T23:59:59Z",
Registrator: "Поступление на расчетный счет 000000001 от 31.08.2022",
AccountDt: "51",
AccountKt: "60.01",
Amount: 150000,
SubcontoKt1: "ООО Поставщик",
SubcontoKt2: "Договор поставки",
Organization: 'ООО "Альтернатива Плюс"'
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("хвосты покажи по счету 60 на август 2022");
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_LIST");
expect(result?.debug.detected_intent).toBe("open_items_by_counterparty_or_contract");
expect(String(result?.reply_text ?? "")).toContain("счету 60");
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("Коротко:");
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("точный открытый остаток");
expect(String(result?.reply_text ?? "")).toContain("не подтвержден");
expect(String(result?.reply_text ?? "")).toContain("предварительные сигналы");
expect(result?.debug.address_coverage_evidence_v1?.requested_result_mode).toBe("confirmed_balance");
expect(result?.debug.address_coverage_evidence_v1?.result_mode).toBe("heuristic_candidates");
expect(result?.debug.address_coverage_evidence_v1?.coverage_status).toBe("partial");
expect(result?.debug.address_coverage_evidence_v1?.balance_confirmed).toBe(false);
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто");
expect(query).toContain("60%");
});
});