ДОМЕНЫ - ВОПРОСЫ - кто должен нам

This commit is contained in:
2026-04-12 20:20:43 +03:00
parent ae9daa50e7
commit 040a55aaea
31 changed files with 1005 additions and 89 deletions
@@ -176,10 +176,7 @@ function formatMoneyRub(value: number): string {
}
function emphasizeNumericTokens(line: string): string {
if (!line) {
return line;
}
return line.replace(/(?<!\*)\d(?:[\d.,:/-]*\d)?(?!\*)/g, (token) => `**${token}**`);
return line;
}
function parseIsoDateToken(value: string | null | undefined): { year: number; month: number; day: number } | null {
@@ -700,6 +697,19 @@ function liabilityCategoryLabel(category: PayablesLiabilityCategory): string {
return "прочие";
}
function receivablesCategoryLabel(category: PayablesLiabilityCategory): string {
if (category === "supplier_or_contractor") {
return "покупатели/заказчики";
}
if (category === "bank_or_credit") {
return "банки/финансовые";
}
if (category === "tax_or_state") {
return "бюджет/госорганы";
}
return "прочие";
}
function classifyPayablesLiabilityCategory(row: ComposeStageRow, counterparty: string): {
scores: Record<PayablesLiabilityCategory, number>;
reasons: string[];
@@ -784,6 +794,11 @@ function hasPayablesSectionPrefix(account: string | null): boolean {
return section === "60" || section === "76";
}
function hasReceivablesSectionPrefix(account: string | null): boolean {
const section = extractAccountSectionCode(account);
return section === "62" || section === "76";
}
function resolvePayablesAsOfDate(options: ComposeFactualReplyOptions): string {
const explicit = normalizeIsoDateOnly(options.asOfDate);
if (explicit) {
@@ -998,6 +1013,126 @@ function buildPayablesConfirmedBalanceAggregate(
});
}
function buildReceivablesConfirmedBalanceAggregate(
rows: ComposeStageRow[],
asOfDate: string
): PayablesConfirmedBalanceAggregate[] {
const byCounterparty = new Map<
string,
{
outstandingAmount: number;
operations: number;
firstPeriod: string | null;
lastPeriod: string | null;
categoryScores: Record<PayablesLiabilityCategory, number>;
reasons: Set<string>;
contracts: Set<string>;
documents: Set<string>;
sourceRefs: Set<string>;
}
>();
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
for (const row of rows) {
const name = extractCounterpartyName(row);
if (!name) {
continue;
}
const rowTimestamp = toUtcDayTimestamp(row.period);
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
continue;
}
const amount = row.amount;
if (typeof amount !== "number" || !Number.isFinite(amount)) {
continue;
}
const absAmount = Math.abs(amount);
let delta = 0;
if (hasReceivablesSectionPrefix(row.account_dt)) {
delta += absAmount;
}
if (hasReceivablesSectionPrefix(row.account_kt)) {
delta -= absAmount;
}
if (Math.abs(delta) <= 0.0000001) {
continue;
}
const classified = classifyPayablesLiabilityCategory(row, name);
const contract = extractContractName(row);
const sourceRefs = extractPayablesSourceRefs(row, name, contract);
const current = byCounterparty.get(name);
if (!current) {
byCounterparty.set(name, {
outstandingAmount: delta,
operations: 1,
firstPeriod: row.period,
lastPeriod: row.period,
categoryScores: {
supplier_or_contractor: classified.scores.supplier_or_contractor,
bank_or_credit: classified.scores.bank_or_credit,
tax_or_state: classified.scores.tax_or_state,
other: classified.scores.other
},
reasons: new Set(classified.reasons),
contracts: new Set(contract ? [contract] : []),
documents: new Set(row.registrator ? [row.registrator] : []),
sourceRefs: new Set(sourceRefs)
});
continue;
}
current.outstandingAmount += delta;
current.operations += 1;
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
current.categoryScores.supplier_or_contractor += classified.scores.supplier_or_contractor;
current.categoryScores.bank_or_credit += classified.scores.bank_or_credit;
current.categoryScores.tax_or_state += classified.scores.tax_or_state;
current.categoryScores.other += classified.scores.other;
for (const reason of classified.reasons) {
current.reasons.add(reason);
}
if (contract) {
current.contracts.add(contract);
}
if (row.registrator) {
current.documents.add(row.registrator);
}
for (const ref of sourceRefs) {
current.sourceRefs.add(ref);
}
}
return Array.from(byCounterparty.entries())
.map(([name, item]) => ({
name,
outstandingAmount: item.outstandingAmount,
operations: item.operations,
firstPeriod: item.firstPeriod,
lastPeriod: item.lastPeriod,
category: resolvePayablesLiabilityCategory(item.categoryScores),
categoryReasons: Array.from(item.reasons).slice(0, 2),
contracts: Array.from(item.contracts).slice(0, 2),
documents: Array.from(item.documents).slice(0, 2),
sourceRefs: Array.from(item.sourceRefs).slice(0, 3)
}))
.filter((item) => item.outstandingAmount > 0.005)
.sort((left, right) => {
if (right.outstandingAmount !== left.outstandingAmount) {
return right.outstandingAmount - left.outstandingAmount;
}
if (right.operations !== left.operations) {
return right.operations - left.operations;
}
return left.name.localeCompare(right.name);
});
}
function buildCounterpartyRiskAggregate(rows: ComposeStageRow[]): CounterpartyRiskAggregate[] {
const byCounterparty = new Map<string, CounterpartyRiskAggregate>();
@@ -2462,7 +2597,7 @@ export function composeFactualReply(
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Режим результата: подтвержденный срез обязательств к оплате (exact route)."
"- Результат: подтвержденный срез обязательств к оплате."
];
lines.push("");
@@ -2492,11 +2627,14 @@ export function composeFactualReply(
lines.push("Блок 5. Подтвержденные позиции к оплате");
if (confirmedBalances.length > 0) {
lines.push(
...confirmedBalances.slice(0, 10).map(
(item, index) =>
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
)
...confirmedBalances.slice(0, 10).flatMap((item, index) => [
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
""
])
);
if (lines[lines.length - 1] === "") {
lines.pop();
}
} else {
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено.");
}
@@ -2512,6 +2650,86 @@ export function composeFactualReply(
};
}
if (intent === "receivables_confirmed_as_of_date") {
const receivablesAsOfDate = resolveReceivablesAsOfDate(options);
const confirmedBalances = buildReceivablesConfirmedBalanceAggregate(rows, receivablesAsOfDate);
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
const totalOutstandingAmount = confirmedBalances.reduce((sum, item) => sum + item.outstandingAmount, 0);
const periodScopeLine =
!asOfDate && (periodFrom || periodTo)
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
: null;
const carryoverLine =
asOfDate || periodFrom || periodTo
? "- В срез могут входить задолженности, возникшие до периода, если они оставались открытыми на дату среза."
: null;
const categoryCounts = confirmedBalances.reduce<Record<PayablesLiabilityCategory, number>>(
(acc, item) => {
acc[item.category] += 1;
return acc;
},
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
);
const lines: string[] = [
`Итого подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
"",
"Блок 1. Статус результата",
"- Результат: подтвержденный срез дебиторской задолженности."
];
lines.push("");
lines.push("Блок 2. Что учтено");
lines.push(`- Дата среза: ${formatDateRu(receivablesAsOfDate)}.`);
if (periodScopeLine) {
lines.push(periodScopeLine);
}
lines.push("- Контур: дебиторская задолженность по счетам 62/76.");
if (carryoverLine) {
lines.push(carryoverLine);
}
lines.push("");
lines.push("Блок 3. Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
lines.push("");
lines.push("Блок 4. Категории дебиторской задолженности");
lines.push(`- ${receivablesCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
lines.push(`- ${receivablesCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
lines.push(`- ${receivablesCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
lines.push(`- ${receivablesCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
lines.push("");
lines.push("Блок 5. Подтвержденные позиции к получению");
if (confirmedBalances.length > 0) {
lines.push(
...confirmedBalances.slice(0, 10).flatMap((item, index) => [
`${index + 1}. ${item.name} | категория: ${receivablesCategoryLabel(item.category)} | остаток к получению: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
""
])
);
if (lines[lines.length - 1] === "") {
lines.pop();
}
} else {
lines.push("- Подтвержденной открытой дебиторской задолженности на дату среза не найдено.");
}
return {
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: lines.map(emphasizeNumericTokens).join("\n"),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
balance_confirmed: true
}
};
}
if (intent === "list_payables_counterparties") {
const counterparties = buildPayablesCounterpartyRiskAggregate(rows);
const payablesAsOfDate = resolvePayablesAsOfDate(options);
@@ -442,7 +442,8 @@ function mergeFollowupFilters(
if (
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts" ||
intent === "payables_confirmed_as_of_date"
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date"
) {
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
const currentContract = toNonEmptyString(merged.contract);
@@ -537,6 +538,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
account_balance_snapshot: ["account", "as_of_date"],
documents_forming_balance: ["account", "as_of_date"],
payables_confirmed_as_of_date: ["as_of_date"],
receivables_confirmed_as_of_date: ["as_of_date"],
list_documents_by_counterparty: ["counterparty"],
bank_operations_by_counterparty: ["counterparty"],
list_contracts_by_counterparty: ["counterparty"],
@@ -192,7 +192,8 @@ function inferAggregationProfile(intent: AddressIntent, shape: AddressQueryShape
if (
intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "payables_confirmed_as_of_date"
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date"
) {
return "balance_snapshot";
}