АРЧ АП11 - Архитектура: убрать технический мусор из inventory reply presentation

This commit is contained in:
2026-04-17 18:42:57 +03:00
parent 7f42d8ab50
commit 29721d16cd
6 changed files with 241 additions and 141 deletions
@@ -4,6 +4,11 @@ import {
buildFactualListReply,
buildFactualSummaryReply
} from "./replyContracts";
import {
appendInventoryBulletSection,
appendInventorySection,
formatInventorySnapshotPositionLine
} from "./inventoryReplyPresentation";
import type { ComposeReplyResult } from "./replyPackaging";
import type { ComposeStageRow } from "./composeStage";
@@ -90,31 +95,32 @@ export function composeInventoryReply(
const lines: string[] = [directAnswerLine];
if (positions.length > 0) {
lines.push("", "Позиции:");
lines.push(
...positions.slice(0, 20).map((item, index) => {
const warehouseLabel = item.warehouse ?? "склад не определен";
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
const periodLabel = item.lastPeriod ? ` | дата строки: ${item.lastPeriod}` : "";
const refsLabel = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${deps.formatNumberWithDots(item.quantity, 3)} | стоимость: ${deps.formatMoneyRub(item.amount)}${organizationLabel}${periodLabel}${refsLabel}`;
})
appendInventorySection(
lines,
"Позиции:",
positions.slice(0, 20).map((item, index) =>
formatInventorySnapshotPositionLine(item, index, {
formatDateRu: deps.formatDateRu,
formatNumberWithDots: deps.formatNumberWithDots,
formatMoneyRub: deps.formatMoneyRub
})
)
);
} else {
lines.push("", "Позиции:", "- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
appendInventorySection(lines, "Позиции:", [
"- На дату среза товары с ненулевым остатком не найдены."
]);
}
lines.push(
"",
"Подтверждение:",
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
"- Контур: остатки по счету 41.01 «Товары на складах».",
`- Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
`- Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
`- Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`
);
appendInventoryBulletSection(lines, "Сводка:", [
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
`Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`
]);
if (rows.length !== positions.length) {
lines.push(`- Строк в подтвержденной выборке: ${deps.formatNumberWithDots(rows.length)}.`);
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
}
return positions.length > 0
@@ -129,12 +135,13 @@ export function composeInventoryReply(
const itemLabel = summary.item ?? "товар не определен";
const directAnswerLine =
purchaseRows.length <= 0
? `По позиции ${itemLabel} подтвержденные документы закупки в доступном контуре не найдены.`
? `По позиции ${itemLabel} подтвержденные документы закупки в доступных данных не найдены.`
: `По позиции ${itemLabel} найдено ${deps.formatNumberWithDots(summary.documents.length)} подтвержденных документов закупки до ${deps.formatDateRu(asOfDate)}.`;
const lines: string[] = [directAnswerLine];
lines.push("", "Подтверждение:");
lines.push(`- Дата верхней границы: ${deps.formatDateRu(asOfDate)}.`);
lines.push(`- Операций поступления в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
appendInventoryBulletSection(lines, "Сводка:", [
`Дата верхней границы: ${deps.formatDateRu(asOfDate)}.`,
`Операций поступления в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
]);
if (summary.counterparties.length === 1) {
lines.push(`- Поставщик: ${summary.counterparties[0]}.`);
} else if (summary.counterparties.length > 1) {
@@ -144,7 +151,7 @@ export function composeInventoryReply(
if (purchaseRows.length > 0) {
lines.push(...deps.formatInventoryTraceRows(purchaseRows, 12));
} else {
lines.push("- По выбранному товару не найдено проводок поступления на 41.01 в доступном контуре.");
lines.push("- По выбранному товару не найдено проводок поступления в доступных данных.");
}
return purchaseRows.length > 0
? buildFactualListReply(lines, buildConfirmedBalanceSemantics("strong", true))
@@ -163,7 +170,7 @@ export function composeInventoryReply(
const lastPurchaseDate = deps.inventoryTraceDateLabel(summary.lastPeriod);
const directAnswerLine =
purchaseRows.length <= 0 || !summary.firstPeriod
? `По позиции ${itemLabel} подтвержденная дата закупки в доступном контуре не найдена.`
? `По позиции ${itemLabel} подтвержденная дата закупки в доступных данных не найдена.`
: summary.firstPeriod === summary.lastPeriod
? `Позиция ${itemLabel} куплена ${firstPurchaseDate}.`
: boundedAsOfLabel
@@ -240,7 +247,7 @@ export function composeInventoryReply(
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
}
if (summary.documents.length > 0) {
lines.push("", "Опорные документы:", ...deps.formatInventoryTraceRows(purchaseRows, 8));
appendInventorySection(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, 8));
}
return buildFactualSummaryReply(
lines,
@@ -262,34 +269,30 @@ export function composeInventoryReply(
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
: summary.counterparties.length > 1
? `По складскому остатку ${warehouseLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 6).join("; ")}.`
: `По складскому остатку ${warehouseLabel} поставщик в текущем exact-контуре не материализован.`;
const lines: string[] = [
directAnswerLine,
`Собран exact-срез supplier overlap для складского остатка до ${deps.formatDateRu(asOfDate)}.`,
"",
"Блок 1. Статус результата",
`- Контур: подтвержденные закупочные движения на 41.01, связанные со складом ${warehouseLabel}.`,
"- Важно: без партионности этот контур не доказывает конкретного владельца каждой партии, а показывает наблюдаемый закупочный след текущего остатка.",
"",
"Блок 2. Подтверждение",
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`- Первая найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`,
`- Последняя найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
`- Закупочных документов в выборке: ${deps.formatNumberWithDots(summary.documents.length)}.`,
`- Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
];
: `По складскому остатку ${warehouseLabel} поставщик в доступных данных не выделен.`;
const lines: string[] = [directAnswerLine];
appendInventoryBulletSection(lines, "Что проверили:", [
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`Первая найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`,
`Последняя найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
`Закупочных документов в выборке: ${deps.formatNumberWithDots(summary.documents.length)}.`,
`Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
]);
appendInventoryBulletSection(lines, "Ограничения:", [
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии."
]);
if (summary.counterparties.length > 0) {
lines.push(`- Найденные поставщики в наблюдаемом контуре: ${summary.counterparties.slice(0, 6).join("; ")}.`);
lines.push(`- Найденные поставщики: ${summary.counterparties.slice(0, 6).join("; ")}.`);
} else if (purchaseRows.length > 0) {
lines.push("- Закупочные движения найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
lines.push("- Закупочные движения найдены, но поставщик не выделен отдельным полем в доступных данных.");
} else {
lines.push("- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного складского среза.");
lines.push("- В доступных данных не найдено закупочных движений по выбранному складскому срезу.");
}
if (unresolvedRows.length > 0) {
lines.push(`- Операций без явно материализованного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`);
lines.push(`- Операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`);
}
if (purchaseRows.length > 0) {
lines.push("", "Блок 3. Опорные документы", ...deps.formatInventoryTraceRows(purchaseRows, 10));
appendInventorySection(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, 10));
}
return buildFactualSummaryReply(
lines,
@@ -315,23 +318,18 @@ export function composeInventoryReply(
agingItems.length > 0
? `К старым закупкам на ${deps.formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
: `По доступному закупочному следу на ${deps.formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
const lines: string[] = [
directAnswerLine,
`Собран exact-срез старых закупок для складского остатка на ${deps.formatDateRu(asOfDate)}.`,
"",
"Блок 1. Статус результата",
"- Контур: показан item-level список товарных позиций с самым ранним документально наблюдаемым закупочным следом на 41.01.",
"- Порядок: позиции отсортированы от самой старой первой закупки к более новым.",
"- Важно: без партионности этот контур не доказывает возраст конкретного лота, а показывает документально наблюдаемый возраст закупочного следа по товарной позиции.",
"",
"Блок 2. Сводка",
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`- Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
`- Самая поздняя найденная закупка в наблюдаемом следе: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
`- Позиции в aging-срезе: ${deps.formatNumberWithDots(agingItems.length)}.`,
`- Закупочных документов в наблюдаемом следе: ${deps.formatNumberWithDots(summary.documents.length)}.`,
`- Закупочных операций в наблюдаемом следе: ${deps.formatNumberWithDots(purchaseRows.length)}.`
];
const lines: string[] = [directAnswerLine];
appendInventoryBulletSection(lines, "Сводка:", [
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
]);
appendInventoryBulletSection(lines, "Ограничения:", [
"Без партионного учета этот ответ показывает возраст закупочного следа по товарной позиции, а не возраст конкретного лота."
]);
if (oldestPurchaseAgeDays !== null) {
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
}
@@ -339,9 +337,11 @@ export function composeInventoryReply(
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
}
if (agingItems.length > 0) {
lines.push("", "Блок 3. Позиции от самых старых закупок", ...deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
appendInventorySection(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
} else {
lines.push("", "Блок 3. Позиции от самых старых закупок", "- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного среза.");
appendInventorySection(lines, "Позиции от самых старых закупок:", [
"- В доступных данных не найдено закупочных движений для выбранного среза."
]);
}
return buildFactualSummaryReply(
lines,
@@ -362,7 +362,7 @@ export function composeInventoryReply(
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
: summary.counterparties.length > 1
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
: `По товару ${itemLabel} покупатель в текущем exact-контуре не материализован.`;
: `По товару ${itemLabel} покупатель в доступных данных не выделен.`;
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
lines.push(`- Первая найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
lines.push(`- Последняя найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`);
@@ -373,13 +373,13 @@ export function composeInventoryReply(
} else if (summary.counterparties.length > 1) {
lines.push(`- По доступным движениям найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`);
} else if (saleRows.length > 0) {
lines.push("- Документы выбытия найдены, но покупатель не материализован отдельным полем в текущем exact-контуре.");
lines.push("- Документы выбытия найдены, но покупатель не выделен отдельным полем в доступных данных.");
}
lines.push("", "Документы выбытия:");
if (saleRows.length > 0) {
lines.push(...deps.formatInventoryTraceRows(saleRows, 12, excludedCounterpartyTokens));
} else {
lines.push("- По выбранному товару не найдено проводок выбытия со счета 41.01 в доступном контуре.");
lines.push("- По выбранному товару не найдено проводок выбытия в доступных данных.");
}
return saleRows.length > 0
? buildFactualListReply(
@@ -403,11 +403,11 @@ export function composeInventoryReply(
lines.push(`- Закупочных движений на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
lines.push(`- Движений выбытия со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
if (purchaseRows.length > 0 && saleRows.length > 0) {
lines.push("- В текущем контуре найдены обе стороны цепочки: поступление и последующее выбытие.");
lines.push("- В доступных данных найдены обе стороны цепочки: поступление и последующее выбытие.");
} else if (purchaseRows.length > 0) {
lines.push("- Найдена только закупочная часть цепочки; выбытие в текущем exact-контуре не подтверждено.");
lines.push("- Найдена только закупочная часть цепочки; выбытие в доступных данных не подтверждено.");
} else if (saleRows.length > 0) {
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в текущем exact-контуре не подтверждена.");
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в доступных данных не подтверждена.");
} else {
lines.push("- Для выбранного товара не найдено движений по 41.01, из которых можно собрать цепочку.");
}
@@ -0,0 +1,43 @@
interface InventorySnapshotPositionLike {
item: string;
warehouse: string | null;
organization: string | null;
quantity: number;
amount: number;
lastPeriod: string | null;
}
interface InventoryPresentationDeps {
formatDateRu: (isoDate: string) => string;
formatNumberWithDots: (value: number, fractionDigits?: number) => string;
formatMoneyRub: (value: number) => string;
}
export function appendInventorySection(lines: string[], title: string, entries: string[]): void {
const visibleEntries = entries.filter((entry) => String(entry ?? "").trim().length > 0);
if (visibleEntries.length === 0) {
return;
}
lines.push("", title, ...visibleEntries);
}
export function appendInventoryBulletSection(lines: string[], title: string, bullets: string[]): void {
appendInventorySection(
lines,
title,
bullets
.filter((bullet) => String(bullet ?? "").trim().length > 0)
.map((bullet) => (bullet.startsWith("- ") ? bullet : `- ${bullet}`))
);
}
export function formatInventorySnapshotPositionLine(
item: InventorySnapshotPositionLike,
index: number,
deps: InventoryPresentationDeps
): string {
const warehouseLabel = item.warehouse ?? "склад не указан";
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
const lastSeenLabel = item.lastPeriod ? ` | последняя дата: ${deps.formatDateRu(item.lastPeriod)}` : "";
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${deps.formatNumberWithDots(item.quantity, 3)} | сумма: ${deps.formatMoneyRub(item.amount)}${organizationLabel}${lastSeenLabel}`;
}