NODEDC_1C/llm_normalizer/backend/tests/addressReplyBuildersRegress...

447 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 { describe, expect, it } from "vitest";
import { composeCounterpartyAnalyticsReply } from "../src/services/address_runtime/counterpartyAnalyticsReplyBuilders";
import { composeFactualReply } from "../src/services/address_runtime/composeStage";
import { composeInventoryReply } from "../src/services/address_runtime/inventoryReplyBuilders";
describe("address reply builders regressions", () => {
it("starts top customer aggregate reply with a direct business answer", () => {
const result = composeCounterpartyAnalyticsReply(
"customer_revenue_and_payments",
[
{
counterparty: "Чапурнов",
amount: 250000,
period: "2020-03-31",
registrator: "Поступление 1"
} as any,
{
counterparty: "Малый клиент",
amount: 100000,
period: "2020-04-30",
registrator: "Поступление 2"
} as any
],
{
userMessage: "кто у нас самый доходный клиент за все время?"
},
{
formatPercent: () => null,
formatDateRu: (value: string) => value,
formatMoneyRub: (value: number) => `${value}`,
extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null),
detectCounterpartyProfileFocus: () => "full_profile",
detectCounterpartyLifecycleFocus: () => "active_customers_all_time",
hasCounterpartyLifecycleLongevityQuestion: () => false,
hasCounterpartyActivityAgeQuestion: () => false,
detectRankingLimit: () => 5,
detectValueRankingFocus: () => "top_by_total",
detectContractValueFocus: () => "top_by_turnover",
detectMinOpsForAvgCheck: () => 1,
extractRequestedYearFromQuestion: () => null,
extractCounterpartyName: (row: any) => row.counterparty ?? "Чапурнов",
extractContractName: () => null,
counterpartyLookupMatches: () => false,
toUtcDayTimestamp: () => null,
formatAgeYearsMonthsDays: () => "0 дней",
normalizeQuestionText: (value: string | null | undefined) => String(value ?? "")
}
);
expect(result?.text.split("\n")[0]).toContain("Самый доходный клиент");
expect(result?.text.split("\n")[0]).toContain("Чапурнов");
});
it("keeps canonical single-best customer wording compact instead of returning the default top list", () => {
const result = composeCounterpartyAnalyticsReply(
"customer_revenue_and_payments",
[
{
counterparty: "Крупный клиент",
amount: 250000,
period: "2020-03-31",
registrator: "Поступление 1"
} as any,
{
counterparty: "Малый клиент",
amount: 100000,
period: "2020-04-30",
registrator: "Поступление 2"
} as any
],
{
userMessage: "определить самого доходного клиента за весь период"
},
{
formatPercent: () => null,
formatDateRu: (value: string) => value,
formatMoneyRub: (value: number) => `${value} руб.`,
extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null),
detectCounterpartyProfileFocus: () => "full_profile",
detectCounterpartyLifecycleFocus: () => "active_customers_all_time",
hasCounterpartyLifecycleLongevityQuestion: () => false,
hasCounterpartyActivityAgeQuestion: () => false,
detectRankingLimit: () => 20,
detectValueRankingFocus: () => "top_by_total",
detectContractValueFocus: () => "top_by_turnover",
detectMinOpsForAvgCheck: () => 1,
extractRequestedYearFromQuestion: () => null,
extractCounterpartyName: (row: any) => row.counterparty,
extractContractName: () => null,
counterpartyLookupMatches: () => false,
toUtcDayTimestamp: () => null,
formatAgeYearsMonthsDays: () => "0 дней",
normalizeQuestionText: (value: string | null | undefined) => String(value ?? "").toLowerCase()
}
);
expect(result?.text.split("\n")[0]).toContain("Крупный клиент");
expect(result?.text).not.toContain("2. Малый клиент");
});
it("keeps direct receivables snapshot answers compact", () => {
const result = composeFactualReply(
"receivables_confirmed_as_of_date",
[
{
period: "2020-05-10",
registrator: "Поступление 1",
account_dt: "62.01",
account_kt: "90.01",
amount: 100000,
analytics: ["Клиент А"],
counterparty: "Клиент А"
},
{
period: "2020-05-11",
registrator: "Поступление 2",
account_dt: "62.01",
account_kt: "90.01",
amount: 50000,
analytics: ["Клиент Б"],
counterparty: "Клиент Б"
}
],
{
userMessage: "кто нам должен денег на май 2020",
asOfDate: "2020-05-31"
}
);
expect(result.text.split("\n")[0]).toContain("Коротко:");
expect(result.text.split("\n")[0]).toContain("Клиент А");
expect(result.text.length).toBeLessThan(1200);
expect(result.text).toContain("Крупнейшие позиции к получению");
});
it("does not overclaim a comparative top customer ranking when only one candidate is present", () => {
const result = composeCounterpartyAnalyticsReply(
"customer_revenue_and_payments",
[
{
amount: 250000,
period: "2021-03-31",
registrator: "Поступление 1"
} as any
],
{
userMessage: "кто больше всего принес денег в 2021"
},
{
formatPercent: () => null,
formatDateRu: (value: string) => value,
formatMoneyRub: (value: number) => `${value}`,
extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null),
detectCounterpartyProfileFocus: () => "full_profile",
detectCounterpartyLifecycleFocus: () => "active_customers_all_time",
hasCounterpartyLifecycleLongevityQuestion: () => false,
hasCounterpartyActivityAgeQuestion: () => false,
detectRankingLimit: () => 5,
detectValueRankingFocus: () => "top_by_total",
detectContractValueFocus: () => "top_by_turnover",
detectMinOpsForAvgCheck: () => 1,
extractRequestedYearFromQuestion: () => null,
extractCounterpartyName: () => "Группа СВК",
extractContractName: () => null,
counterpartyLookupMatches: () => false,
toUtcDayTimestamp: () => null,
formatAgeYearsMonthsDays: () => "0 дней",
normalizeQuestionText: (value: string | null | undefined) => String(value ?? "")
}
);
expect(result?.text.split("\n")[0]).toContain("найден один клиент");
expect(result?.text.split("\n")[0]).toContain("не полноценный сравнительный рейтинг");
expect(result?.text).not.toContain("Самый доходный клиент");
});
it("starts top year aggregate reply with a direct business answer", () => {
const result = composeCounterpartyAnalyticsReply(
"customer_revenue_and_payments",
[
{
amount: 320000,
period: "2021-05-12",
registrator: "Поступление 2"
} as any
],
{
userMessage: "какой у нас самый доходный год"
},
{
formatPercent: () => null,
formatDateRu: (value: string) => value,
formatMoneyRub: (value: number) => `${value}`,
extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null),
detectCounterpartyProfileFocus: () => "full_profile",
detectCounterpartyLifecycleFocus: () => "active_customers_all_time",
hasCounterpartyLifecycleLongevityQuestion: () => false,
hasCounterpartyActivityAgeQuestion: () => false,
detectRankingLimit: () => 5,
detectValueRankingFocus: () => "top_years_by_total",
detectContractValueFocus: () => "top_by_turnover",
detectMinOpsForAvgCheck: () => 1,
extractRequestedYearFromQuestion: () => null,
extractCounterpartyName: () => "Чапурнов",
extractContractName: () => null,
counterpartyLookupMatches: () => false,
toUtcDayTimestamp: () => null,
formatAgeYearsMonthsDays: () => "0 дней",
normalizeQuestionText: (value: string | null | undefined) => String(value ?? "")
}
);
expect(result?.text.split("\n")[0]).toContain("Самый доходный год");
expect(result?.text.split("\n")[0]).toContain("2021");
});
it("keeps very old stock answer free of explicit as-of date in the first line", () => {
const result = composeInventoryReply(
"inventory_aging_by_purchase_date",
[
{
amount: 1000,
period: "2015-02-05",
registrator: "Поступление 3"
} as any
],
{
userMessage: "Есть ли остатки товара, которые закупались очень давно",
asOfDate: "2026-04-18"
},
{
resolvePayablesAsOfDate: () => "2026-04-18",
buildInventoryOnHandAggregate: () => [],
uniqueStrings: (values: string[]) => values,
formatDateRu: (value: string) => value,
formatNumberWithDots: (value: number) => String(value),
formatMoneyRub: (value: number) => `${value}`,
isInventoryPurchaseMovement: () => true,
summarizeInventoryTraceRows: () => ({
item: "Рабочая станция",
warehouses: ["Основной склад"],
organizations: ["ООО Альтернатива Плюс"],
counterparties: ["Чапурнов"],
documents: ["Поступление 3"],
firstPeriod: "2015-02-05",
lastPeriod: "2015-02-05",
totalAmount: 1000
}),
formatInventoryTraceRows: () => [],
hasInventoryPurchaseDateActionFocus: () => false,
inventoryTraceDateLabel: (value: string | null) => value ?? "дата не указана",
extractInventoryCounterpartyCandidates: () => ["Чапурнов"],
buildInventoryAgingByItemAggregate: () => [
{
item: "Рабочая станция",
warehouse: "Основной склад",
organization: "ООО Альтернатива Плюс",
firstPurchasePeriod: "2015-02-05",
lastPurchasePeriod: "2015-02-05",
operations: 1,
documentCount: 1,
counterparties: ["Чапурнов"],
ageDays: 4089
}
],
formatInventoryAgingRows: () => ["1. Рабочая станция | первая закупка: 2015-02-05"],
isInventorySaleMovement: () => false
}
);
const firstLine = result?.text.split("\n")[0] ?? "";
expect(firstLine).toContain("К самым старым закупкам");
expect(firstLine).not.toContain("2026-04-18");
});
it("excludes mirrored 76 settlements from clean payables", () => {
const result = composeFactualReply(
"payables_confirmed_as_of_date",
[
{
period: "2026-05-12",
registrator: "",
account_dt: "51",
account_kt: "76.09",
amount: 3677454.14,
analytics: ["Комитет государственных услуг г. Москвы", "ООО \\Альтернатива Плюс\\", "Финансовое обеспечение заявки"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО \\Альтернатива Плюс\\"
},
{
period: "2026-05-12",
registrator: "Списание с расчетного счета 1",
account_dt: "76.09",
account_kt: "51",
amount: 1000000,
analytics: ["Комитет государственных услуг г. Москвы", "ООО \\Альтернатива Плюс\\", "Финансовое обеспечение заявки"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО \\Альтернатива Плюс\\"
},
{
period: "2026-05-12",
registrator: "Списание с расчетного счета 2",
account_dt: "76.09",
account_kt: "51",
amount: 2677454.14,
analytics: ["Комитет государственных услуг г. Москвы", "ООО \\Альтернатива Плюс\\", "Финансовое обеспечение заявки"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО \\Альтернатива Плюс\\"
},
{
period: "2026-05-12",
registrator: "Поступление товаров 1",
account_dt: "41.01",
account_kt: "60.01",
amount: 7271.2,
analytics: ["Авант мебель"],
counterparty: "Авант мебель",
organization: "ООО Альтернатива Плюс"
}
],
{
userMessage: "мы должны кому-то денег на сегодня?",
asOfDate: "2026-05-12"
}
);
const firstLine = result.text.split("\n")[0] ?? "";
expect(firstLine).toContain("7.271,20");
expect(firstLine).toContain("Авант мебель");
expect(firstLine).not.toContain("3.677.454,14");
expect(result.text).toContain("встречных остатков");
expect(result.text).toContain("Комитет государственных услуг г. Москвы");
expect(result.text).toContain("Финансовое обеспечение заявки");
expect(result.text).not.toContain("договор/аналитика: ООО \\Альтернатива Плюс\\");
});
it("excludes mirrored 76 settlements from clean receivables", () => {
const result = composeFactualReply(
"receivables_confirmed_as_of_date",
[
{
period: "2026-05-12",
registrator: "Реализация 1",
account_dt: "62.01",
account_kt: "90.01",
amount: 9612904.9,
analytics: ["Департамент капитального ремонта города Москвы."],
counterparty: "Департамент капитального ремонта города Москвы.",
organization: "ООО Альтернатива Плюс"
},
{
period: "2026-05-12",
registrator: "",
account_dt: "51",
account_kt: "76.09",
amount: 3677454.14,
analytics: ["Комитет государственных услуг г. Москвы", "Финансовое обеспечение заявки"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО Альтернатива Плюс"
},
{
period: "2026-05-12",
registrator: "Списание с расчетного счета 1",
account_dt: "76.09",
account_kt: "51",
amount: 3677454.14,
analytics: ["Комитет государственных услуг г. Москвы", "Финансовое обеспечение заявки"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО Альтернатива Плюс"
}
],
{
userMessage: "а нам кто должен на сегодня?",
asOfDate: "2026-05-12"
}
);
const firstLine = result.text.split("\n")[0] ?? "";
expect(firstLine).toContain("9.612.904,90");
expect(firstLine).toContain("Департамент капитального ремонта города Москвы.");
expect(firstLine).not.toContain("13.290.359,04");
expect(result.text).toContain("встречных остатков");
expect(result.text).toContain("Комитет государственных услуг г. Москвы");
});
it("keeps bank-named balance counterparties as counterparties, not settlement analytics", () => {
const result = composeFactualReply(
"receivables_confirmed_as_of_date",
[
{
period: "2017-05-31",
registrator: "Остатки на дату",
account_dt: "76.09",
account_kt: null,
amount: 39079.12,
analytics: ["Сбербанк-АСТ, ЗАО", "Финансовое обеспечение заявки"],
organization: "ООО \\Альтернатива Плюс\\"
}
],
{
userMessage: "кто нам должен денег на май 2017?",
asOfDate: "2017-05-31"
}
);
const firstLine = result.text.split("\n")[0] ?? "";
expect(firstLine).toContain("Сбербанк-АСТ, ЗАО");
expect(firstLine).not.toContain("Финансовое обеспечение заявки");
});
it("keeps different 76 contracts separated instead of netting by counterparty only", () => {
const result = composeFactualReply(
"payables_confirmed_as_of_date",
[
{
period: "2026-05-12",
registrator: "",
account_dt: "51",
account_kt: "76.09",
amount: 1000,
analytics: ["Комитет государственных услуг г. Москвы", "Договор А"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО Альтернатива Плюс"
},
{
period: "2026-05-12",
registrator: "Списание с расчетного счета 1",
account_dt: "76.09",
account_kt: "51",
amount: 1000,
analytics: ["Комитет государственных услуг г. Москвы", "Договор Б"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО Альтернатива Плюс"
}
],
{
userMessage: "мы должны кому-то денег на сегодня?",
asOfDate: "2026-05-12"
}
);
expect(result.text.split("\n")[0]).toContain("1.000,00");
expect(result.text).not.toContain("встречных остатков");
});
});