ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - Спека exact-маршрута payables на дату: confirmed_balance без эвристического финала
This commit is contained in:
@@ -470,6 +470,142 @@ function extractCounterpartyName(row) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function liabilityCategoryLabel(category) {
|
||||
if (category === "supplier_or_contractor") {
|
||||
return "поставщики/подрядчики";
|
||||
}
|
||||
if (category === "bank_or_credit") {
|
||||
return "банки/кредиты";
|
||||
}
|
||||
if (category === "tax_or_state") {
|
||||
return "налоги/госорганы";
|
||||
}
|
||||
return "прочие";
|
||||
}
|
||||
function classifyPayablesLiabilityCategory(row, counterparty) {
|
||||
const scores = {
|
||||
supplier_or_contractor: 0,
|
||||
bank_or_credit: 0,
|
||||
tax_or_state: 0,
|
||||
other: 0
|
||||
};
|
||||
const reasons = new Set();
|
||||
const text = `${counterparty} ${row.registrator} ${row.analytics.join(" ")}`.toLowerCase();
|
||||
const accountPrefixes = [extractAccountSectionCode(row.account_dt), extractAccountSectionCode(row.account_kt)].filter((item) => Boolean(item));
|
||||
if (accountPrefixes.includes("60")) {
|
||||
scores.supplier_or_contractor += 3;
|
||||
reasons.add("участие счета 60");
|
||||
}
|
||||
if (accountPrefixes.includes("66") || accountPrefixes.includes("67")) {
|
||||
scores.bank_or_credit += 4;
|
||||
reasons.add("участие счета 66/67");
|
||||
}
|
||||
if (accountPrefixes.includes("68") || accountPrefixes.includes("69")) {
|
||||
scores.tax_or_state += 4;
|
||||
reasons.add("участие счета 68/69");
|
||||
}
|
||||
if (accountPrefixes.includes("76")) {
|
||||
scores.supplier_or_contractor += 1;
|
||||
reasons.add("участие счета 76");
|
||||
}
|
||||
if (/(?:банк|сбер|втб|альфа|газпромбанк|кредит|loan|overdraft)/iu.test(text)) {
|
||||
scores.bank_or_credit += 3;
|
||||
reasons.add("банк/кредит в аналитике");
|
||||
}
|
||||
if (/(?:уфк|ифнс|фнс|налог|пфр|фсс|сфр|казнач|бюджет|гос)/iu.test(text)) {
|
||||
scores.tax_or_state += 3;
|
||||
reasons.add("налог/госорган в аналитике");
|
||||
}
|
||||
if (/(?:\bип\b|ооо|ао|зао|пао|подряд|поставщик|supplier|vendor|contractor)/iu.test(text)) {
|
||||
scores.supplier_or_contractor += 2;
|
||||
reasons.add("коммерческий контрагент в аналитике");
|
||||
}
|
||||
return {
|
||||
scores,
|
||||
reasons: Array.from(reasons)
|
||||
};
|
||||
}
|
||||
function buildPayablesCounterpartyRiskAggregate(rows) {
|
||||
const byCounterparty = new Map();
|
||||
for (const row of rows) {
|
||||
const name = extractCounterpartyName(row);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const amountRaw = row.amount ?? 0;
|
||||
if (!Number.isFinite(amountRaw)) {
|
||||
continue;
|
||||
}
|
||||
const amount = Math.abs(amountRaw);
|
||||
const classified = classifyPayablesLiabilityCategory(row, name);
|
||||
const current = byCounterparty.get(name);
|
||||
if (!current) {
|
||||
byCounterparty.set(name, {
|
||||
base: {
|
||||
name,
|
||||
totalAmount: amount,
|
||||
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)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
current.base.totalAmount += amount;
|
||||
current.base.operations += 1;
|
||||
if ((row.period ?? "") < (current.base.firstPeriod ?? "")) {
|
||||
current.base.firstPeriod = row.period;
|
||||
}
|
||||
if ((row.period ?? "") > (current.base.lastPeriod ?? "")) {
|
||||
current.base.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);
|
||||
}
|
||||
}
|
||||
const scoreKeys = ["supplier_or_contractor", "bank_or_credit", "tax_or_state", "other"];
|
||||
const toCategory = (scores) => {
|
||||
let winner = "other";
|
||||
let best = Number.NEGATIVE_INFINITY;
|
||||
for (const key of scoreKeys) {
|
||||
const score = scores[key];
|
||||
if (score > best) {
|
||||
best = score;
|
||||
winner = key;
|
||||
}
|
||||
}
|
||||
if (best <= 0) {
|
||||
return "other";
|
||||
}
|
||||
return winner;
|
||||
};
|
||||
return Array.from(byCounterparty.values())
|
||||
.map((item) => ({
|
||||
...item.base,
|
||||
category: toCategory(item.categoryScores),
|
||||
categoryReasons: Array.from(item.reasons).slice(0, 2)
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
if (right.totalAmount !== left.totalAmount) {
|
||||
return right.totalAmount - left.totalAmount;
|
||||
}
|
||||
if (right.operations !== left.operations) {
|
||||
return right.operations - left.operations;
|
||||
}
|
||||
return left.name.localeCompare(right.name);
|
||||
});
|
||||
}
|
||||
function buildCounterpartyRiskAggregate(rows) {
|
||||
const byCounterparty = new Map();
|
||||
for (const row of rows) {
|
||||
@@ -1528,22 +1664,55 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
};
|
||||
}
|
||||
if (intent === "list_payables_counterparties") {
|
||||
const counterparties = buildCounterpartyRiskAggregate(rows);
|
||||
const counterparties = buildPayablesCounterpartyRiskAggregate(rows);
|
||||
const scopeLine = (() => {
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||
if (asOfDate) {
|
||||
return `Дата среза: ${formatDateRu(asOfDate)}.`;
|
||||
}
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
if (periodFrom || periodTo) {
|
||||
return `Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`;
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
const lines = [
|
||||
"Проверил поставщиков с признаками незакрытых хвостов по взаиморасчетам (контур 60/76).",
|
||||
"Коротко: собран shortlist кандидатов на ручную проверку по потенциально незакрытым обязательствам (контур 60/76).",
|
||||
"",
|
||||
"Что это значит:",
|
||||
"- Режим результата: эвристический скоринг по движениям.",
|
||||
"- Это не финальный подтвержденный остаток к оплате.",
|
||||
...(scopeLine ? ["", scopeLine] : []),
|
||||
"",
|
||||
`Строк в выборке: ${rows.length}.`,
|
||||
`Контрагентов с сигналом: ${counterparties.length}.`
|
||||
`Контрагентов-кандидатов: ${counterparties.length}.`
|
||||
];
|
||||
if (counterparties.length > 0) {
|
||||
lines.push("Приоритет ручной проверки (по сумме/частоте хвостов):");
|
||||
const categoryCounts = counterparties.reduce((acc, item) => {
|
||||
acc[item.category] += 1;
|
||||
return acc;
|
||||
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
|
||||
lines.push("");
|
||||
lines.push("Категории обязательств:");
|
||||
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`);
|
||||
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`);
|
||||
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`);
|
||||
lines.push(`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`);
|
||||
lines.push("");
|
||||
lines.push("Приоритет ручной проверки (по сумме/частоте сигналов):");
|
||||
lines.push(...counterparties
|
||||
.slice(0, 8)
|
||||
.map((item, index) => `${index + 1}. ${item.name} | сумма сигнала: ${formatMoney(item.totalAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}`));
|
||||
.map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | сумма сигнала: ${formatMoney(item.totalAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""} | статус: требует ручной проверки${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`));
|
||||
lines.push("");
|
||||
lines.push("Примеры исходных строк:");
|
||||
lines.push(...formatTopRows(rows, 4));
|
||||
}
|
||||
else {
|
||||
lines.push("Явных признаков системной задолженности по доступному срезу не найдено.");
|
||||
lines.push("");
|
||||
lines.push("Явных кандидатов на незакрытые обязательства по текущему срезу не найдено.");
|
||||
lines.push("");
|
||||
lines.push("Примеры исходных строк:");
|
||||
lines.push(...formatTopRows(rows, 6));
|
||||
}
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user