436 lines
17 KiB
TypeScript
436 lines
17 KiB
TypeScript
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 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");
|
||
|
||
const query = String(executeAddressMcpQueryMock.mock.calls[0]?.[0]?.query ?? "");
|
||
expect(query).toContain("РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто");
|
||
expect(query).toContain("60%");
|
||
});
|
||
});
|