Добавить financial flow hints для банковских контрагентов
This commit is contained in:
@@ -346,7 +346,10 @@ const BANK_DOCS_QUERY_TEMPLATE = `
|
||||
"" КАК СчетКт,
|
||||
БанкСписание.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
|
||||
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
|
||||
БанкСписание.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.СписаниеСРасчетногоСчета КАК БанкСписание
|
||||
__WHERE_OUT__
|
||||
@@ -358,7 +361,10 @@ __WHERE_OUT__
|
||||
"" КАК СчетКт,
|
||||
БанкПоступление.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
|
||||
"" КАК НазначениеПлатежа,
|
||||
БанкПоступление.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
|
||||
__WHERE_IN__
|
||||
@@ -624,7 +630,9 @@ const CUSTOMER_REVENUE_PROFILE_QUERY_TEMPLATE = `
|
||||
"" КАК СчетКт,
|
||||
БанкПоступление.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
|
||||
БанкПоступление.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
|
||||
__WHERE_IN__
|
||||
@@ -639,7 +647,10 @@ const SUPPLIER_PAYOUT_PROFILE_QUERY_TEMPLATE = `
|
||||
"" КАК СчетКт,
|
||||
БанкСписание.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
|
||||
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
|
||||
БанкСписание.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.СписаниеСРасчетногоСчета КАК БанкСписание
|
||||
__WHERE_OUT__
|
||||
|
||||
+21
-1
@@ -595,6 +595,26 @@ function businessOverviewDebtDueDateAgingText(overview) {
|
||||
}
|
||||
return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`;
|
||||
}
|
||||
function financialFlowHintTextRuFromBucket(bucket) {
|
||||
const rows = bucket?.financial_flow_hint_rows ?? 0;
|
||||
const rowsText = rows > 0 ? ` (${rows} строк)` : "";
|
||||
if (bucket?.financial_flow_hint === "loan_or_credit") {
|
||||
return ` По полям банковского документа доминирует кредитный/заемный признак${rowsText}; это не обычный поставщик и не клиентская выручка без отдельной проверки назначения.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "bank_fee_or_service") {
|
||||
return ` По полям банковского документа доминирует признак банковской комиссии/услуг банка${rowsText}; это не обычный поставщик товаров/услуг без отдельной проверки договора.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "tax_or_budget") {
|
||||
return ` По полям банковского документа доминирует налоговый/бюджетный признак${rowsText}.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "payroll_or_social") {
|
||||
return ` По полям банковского документа доминирует зарплатный/социальный признак${rowsText}.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "supplier_payment") {
|
||||
return ` По полям банковского документа доминирует признак оплаты поставщику${rowsText}; если получатель по названию является банком, это все равно требует осторожной трактовки.`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function businessOverviewVendorProcurementQualityText(overview) {
|
||||
const quality = overview.vendor_procurement_quality;
|
||||
if (!quality) {
|
||||
@@ -617,7 +637,7 @@ function businessOverviewVendorProcurementQualityText(overview) {
|
||||
? ` Договорный профиль: используется ${quality.used_contracts} договоров.`
|
||||
: ` Договорный профиль: используется ${quality.used_contracts}/${quality.total_contracts} договоров${quality.used_contract_share_pct === null ? "" : ` (${quality.used_contract_share_pct}%)`}.`;
|
||||
if (quality.evidence_status === "financial_institution_leads_outgoing_cash") {
|
||||
return `Проверенный procurement-concentration route за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим маршрутом не доказаны.`;
|
||||
return `Проверенный procurement-concentration route за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${financialFlowHintTextRuFromBucket(top)}${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим маршрутом не доказаны.`;
|
||||
}
|
||||
if (quality.evidence_status === "reviewed_procurement_concentration") {
|
||||
return `Проверенный procurement-concentration route за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;
|
||||
|
||||
+120
-8
@@ -1611,6 +1611,80 @@ function rowCounterpartyValue(row) {
|
||||
}
|
||||
return rowAnalyticsTextValues(row).find(isLikelyCounterpartyToken) ?? null;
|
||||
}
|
||||
const FINANCIAL_FLOW_HINTS = [
|
||||
"loan_or_credit",
|
||||
"bank_fee_or_service",
|
||||
"tax_or_budget",
|
||||
"payroll_or_social",
|
||||
"supplier_payment",
|
||||
"unclear"
|
||||
];
|
||||
function normalizeFinancialFlowText(value) {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function rowFinancialFlowCorpus(row) {
|
||||
return [
|
||||
rowTextValue(row, ["ВидОперации", "OperationType", "operation_type"]),
|
||||
rowTextValue(row, ["НазначениеПлатежа", "PaymentPurpose", "payment_purpose"]),
|
||||
rowTextValue(row, ["Комментарий", "Comment", "comment"]),
|
||||
rowContractValue(row),
|
||||
rowDocumentValue(row)
|
||||
]
|
||||
.map(normalizeFinancialFlowText)
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
function classifyRowFinancialFlowHint(row) {
|
||||
const corpus = rowFinancialFlowCorpus(row);
|
||||
if (!corpus) {
|
||||
return "unclear";
|
||||
}
|
||||
if (/(?:кредит|кредитн|заем|займ|овердрафт|процент|ссуд|депозит|loan|credit|overdraft|deposit)/iu.test(corpus)) {
|
||||
return "loan_or_credit";
|
||||
}
|
||||
if (/(?:комисс|эквайр|расчетн[оа -]*кассов|ведение\s+счет|банк[а-я\s-]*услуг|банковск[а-я\s-]*услуг|рко|bank\s*fee|acquiring|bank\s*service)/iu.test(corpus)) {
|
||||
return "bank_fee_or_service";
|
||||
}
|
||||
if (/(?:налог|взнос|фсс|фомс|пфр|бюджет|казнач|пен[ия]|штраф|tax|budget|treasury)/iu.test(corpus)) {
|
||||
return "tax_or_budget";
|
||||
}
|
||||
if (/(?:зарплат|заработн|перечислениезаработнойплаты|salary|payroll|wage)/iu.test(corpus)) {
|
||||
return "payroll_or_social";
|
||||
}
|
||||
if (/(?:оплатапоставщику|оплата\s+поставщ|поставщ|supplier|vendor)/iu.test(corpus)) {
|
||||
return "supplier_payment";
|
||||
}
|
||||
return "unclear";
|
||||
}
|
||||
function createFinancialFlowHintCounts() {
|
||||
return {
|
||||
loan_or_credit: 0,
|
||||
bank_fee_or_service: 0,
|
||||
tax_or_budget: 0,
|
||||
payroll_or_social: 0,
|
||||
supplier_payment: 0,
|
||||
unclear: 0
|
||||
};
|
||||
}
|
||||
function dominantFinancialFlowHint(counts) {
|
||||
let bestHint = null;
|
||||
let bestRows = 0;
|
||||
for (const hint of FINANCIAL_FLOW_HINTS) {
|
||||
const rows = counts[hint] ?? 0;
|
||||
if (rows > bestRows) {
|
||||
bestHint = hint;
|
||||
bestRows = rows;
|
||||
}
|
||||
}
|
||||
if (!bestHint || bestRows <= 0 || bestHint === "unclear") {
|
||||
return { hint: null, rows: 0 };
|
||||
}
|
||||
return { hint: bestHint, rows: bestRows };
|
||||
}
|
||||
function normalizeDateParts(yearText, monthText, dayText) {
|
||||
const year = Number(yearText);
|
||||
const month = Number(monthText);
|
||||
@@ -2119,22 +2193,32 @@ function deriveRankedValueFlow(result, input) {
|
||||
continue;
|
||||
}
|
||||
rowsWithAmount += 1;
|
||||
const current = buckets.get(axisValue) ?? { rows_with_amount: 0, total_amount: 0 };
|
||||
const current = buckets.get(axisValue) ?? {
|
||||
rows_with_amount: 0,
|
||||
total_amount: 0,
|
||||
financial_flow_hint_counts: createFinancialFlowHintCounts()
|
||||
};
|
||||
current.rows_with_amount += 1;
|
||||
current.total_amount += amount;
|
||||
current.financial_flow_hint_counts[classifyRowFinancialFlowHint(row)] += 1;
|
||||
buckets.set(axisValue, current);
|
||||
}
|
||||
if (rowsWithAmount <= 0 || buckets.size <= 0) {
|
||||
return null;
|
||||
}
|
||||
const rankedValues = Array.from(buckets.entries())
|
||||
.map(([axisValue, bucket]) => ({
|
||||
axis_value: axisValue,
|
||||
rows_with_amount: bucket.rows_with_amount,
|
||||
total_amount: bucket.total_amount,
|
||||
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount),
|
||||
counterparty_role_hint: (0, counterpartyRoleHeuristics_1.counterpartyRoleHintForName)(axisValue)
|
||||
}))
|
||||
.map(([axisValue, bucket]) => {
|
||||
const dominantHint = dominantFinancialFlowHint(bucket.financial_flow_hint_counts);
|
||||
return {
|
||||
axis_value: axisValue,
|
||||
rows_with_amount: bucket.rows_with_amount,
|
||||
total_amount: bucket.total_amount,
|
||||
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount),
|
||||
counterparty_role_hint: (0, counterpartyRoleHeuristics_1.counterpartyRoleHintForName)(axisValue),
|
||||
financial_flow_hint: dominantHint.hint ?? undefined,
|
||||
financial_flow_hint_rows: dominantHint.hint ? dominantHint.rows : undefined
|
||||
};
|
||||
})
|
||||
.sort((left, right) => {
|
||||
const amountDelta = right.total_amount - left.total_amount;
|
||||
if (input.rankingNeed === "bottom_asc") {
|
||||
@@ -3430,6 +3514,26 @@ function summarizeBusinessOverviewRows(input) {
|
||||
}
|
||||
return parts.length > 0 ? parts.join("; ") : null;
|
||||
}
|
||||
function financialFlowHintTextRu(bucket) {
|
||||
const rows = bucket?.financial_flow_hint_rows ?? 0;
|
||||
const rowsText = rows > 0 ? ` (${rows} строк)` : "";
|
||||
if (bucket?.financial_flow_hint === "loan_or_credit") {
|
||||
return ` По полям банковского документа доминирует кредитный/заемный признак${rowsText}.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "bank_fee_or_service") {
|
||||
return ` По полям банковского документа доминирует признак банковской комиссии/услуг банка${rowsText}.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "tax_or_budget") {
|
||||
return ` По полям банковского документа доминирует налоговый/бюджетный признак${rowsText}.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "payroll_or_social") {
|
||||
return ` По полям банковского документа доминирует зарплатный/социальный признак${rowsText}.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "supplier_payment") {
|
||||
return ` По полям банковского документа доминирует признак оплаты поставщику${rowsText}; все равно нужна осторожность, если контрагент по названию является банком.`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function buildBusinessOverviewConfirmedFacts(derived) {
|
||||
if (!derived) {
|
||||
return [];
|
||||
@@ -3447,6 +3551,10 @@ function buildBusinessOverviewConfirmedFacts(derived) {
|
||||
const leader = derived.top_customers[0];
|
||||
if ((0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(leader.axis_value)) {
|
||||
facts.push(`Крупнейший входящий денежный источник в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа это не доказанный клиент или бизнес-выручка.`);
|
||||
const financialHintText = financialFlowHintTextRu(leader);
|
||||
if (financialHintText) {
|
||||
facts.push(financialHintText.trim());
|
||||
}
|
||||
const nonFinancialLeader = derived.top_customers.slice(1).find((item) => !(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(item.axis_value));
|
||||
if (nonFinancialLeader) {
|
||||
facts.push(`Крупнейший небанковский входящий контрагент в проверенном срезе: ${nonFinancialLeader.axis_value} — ${nonFinancialLeader.total_amount_human_ru}.`);
|
||||
@@ -3460,6 +3568,10 @@ function buildBusinessOverviewConfirmedFacts(derived) {
|
||||
const leader = derived.top_suppliers[0];
|
||||
if ((0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(leader.axis_value)) {
|
||||
facts.push(`Крупнейший получатель исходящих денег в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора это не доказанный обычный поставщик.`);
|
||||
const financialHintText = financialFlowHintTextRu(leader);
|
||||
if (financialHintText) {
|
||||
facts.push(financialHintText.trim());
|
||||
}
|
||||
const nonFinancialLeader = derived.top_suppliers.slice(1).find((item) => !(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(item.axis_value));
|
||||
if (nonFinancialLeader) {
|
||||
facts.push(`Крупнейший небанковский получатель исходящих денег в проверенном срезе: ${nonFinancialLeader.axis_value} — ${nonFinancialLeader.total_amount_human_ru}.`);
|
||||
|
||||
+26
@@ -415,6 +415,28 @@ function overviewAxisLooksFinancial(row) {
|
||||
return (row.counterparty_role_hint === "bank_or_financial_institution" ||
|
||||
(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(row.axis_value));
|
||||
}
|
||||
function financialFlowHintTextRuFromRecord(row) {
|
||||
const hint = toNonEmptyString(row?.financial_flow_hint);
|
||||
const rows = typeof row?.financial_flow_hint_rows === "number" && Number.isFinite(row.financial_flow_hint_rows)
|
||||
? ` (${row.financial_flow_hint_rows} строк)`
|
||||
: "";
|
||||
if (hint === "loan_or_credit") {
|
||||
return `По полям банковского документа доминирует кредитный/заемный признак${rows}; это не обычная поставка и не клиентская выручка без отдельной проверки назначения.`;
|
||||
}
|
||||
if (hint === "bank_fee_or_service") {
|
||||
return `По полям банковского документа доминирует признак банковской комиссии/услуг банка${rows}; это не обычный поставщик товаров/услуг без отдельной проверки договора.`;
|
||||
}
|
||||
if (hint === "tax_or_budget") {
|
||||
return `По полям банковского документа доминирует налоговый/бюджетный признак${rows}; это не поставщик и не клиентская выручка.`;
|
||||
}
|
||||
if (hint === "payroll_or_social") {
|
||||
return `По полям банковского документа доминирует зарплатный/социальный признак${rows}; это не поставщик и не клиентская выручка.`;
|
||||
}
|
||||
if (hint === "supplier_payment") {
|
||||
return `По полям банковского документа доминирует признак оплаты поставщику${rows}; если получатель по названию является банком, это все равно требует осторожной трактовки.`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function businessOverviewTaxLine(overview) {
|
||||
const tax = toRecordObject(overview.tax_position);
|
||||
if (!tax) {
|
||||
@@ -795,6 +817,10 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
|
||||
if (status === "financial_institution_leads_outgoing_cash") {
|
||||
lines.push(`Коротко: проверенный procurement-concentration route за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
|
||||
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
||||
if (financialHintText) {
|
||||
lines.push(financialHintText);
|
||||
}
|
||||
if (nonFinancialName) {
|
||||
lines.push(`Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user