447 lines
19 KiB
TypeScript
447 lines
19 KiB
TypeScript
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("встречных остатков");
|
||
});
|
||
});
|