Добавить financial flow hints для банковских контрагентов
This commit is contained in:
@@ -360,7 +360,10 @@ const BANK_DOCS_QUERY_TEMPLATE = `
|
||||
"" КАК СчетКт,
|
||||
БанкСписание.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
|
||||
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
|
||||
БанкСписание.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.СписаниеСРасчетногоСчета КАК БанкСписание
|
||||
__WHERE_OUT__
|
||||
@@ -372,7 +375,10 @@ __WHERE_OUT__
|
||||
"" КАК СчетКт,
|
||||
БанкПоступление.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
|
||||
"" КАК НазначениеПлатежа,
|
||||
БанкПоступление.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
|
||||
__WHERE_IN__
|
||||
@@ -644,7 +650,9 @@ const CUSTOMER_REVENUE_PROFILE_QUERY_TEMPLATE = `
|
||||
"" КАК СчетКт,
|
||||
БанкПоступление.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
|
||||
БанкПоступление.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
|
||||
__WHERE_IN__
|
||||
@@ -660,7 +668,10 @@ const SUPPLIER_PAYOUT_PROFILE_QUERY_TEMPLATE = `
|
||||
"" КАК СчетКт,
|
||||
БанкСписание.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
|
||||
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
|
||||
БанкСписание.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.СписаниеСРасчетногоСчета КАК БанкСписание
|
||||
__WHERE_OUT__
|
||||
|
||||
@@ -736,6 +736,27 @@ function businessOverviewDebtDueDateAgingText(overview: BusinessOverview): strin
|
||||
return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`;
|
||||
}
|
||||
|
||||
function financialFlowHintTextRuFromBucket(bucket: BusinessOverviewRankedBucket | null | undefined): string {
|
||||
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: BusinessOverview): string | null {
|
||||
const quality = overview.vendor_procurement_quality;
|
||||
if (!quality) {
|
||||
@@ -759,7 +780,7 @@ function businessOverviewVendorProcurementQualityText(overview: BusinessOverview
|
||||
? ` Договорный профиль: используется ${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} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;
|
||||
|
||||
@@ -65,6 +65,14 @@ export interface AssistantMcpDiscoveryValueFlowMonthBucket {
|
||||
total_amount_human_ru: string;
|
||||
}
|
||||
|
||||
export type AssistantMcpDiscoveryFinancialFlowHint =
|
||||
| "loan_or_credit"
|
||||
| "bank_fee_or_service"
|
||||
| "tax_or_budget"
|
||||
| "payroll_or_social"
|
||||
| "supplier_payment"
|
||||
| "unclear";
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedValueFlow {
|
||||
value_flow_direction: "incoming_customer_revenue" | "outgoing_supplier_payout";
|
||||
counterparty: string | null;
|
||||
@@ -89,6 +97,8 @@ export interface AssistantMcpDiscoveryRankedValueFlowBucket {
|
||||
total_amount: number;
|
||||
total_amount_human_ru: string;
|
||||
counterparty_role_hint?: CounterpartyRoleHint;
|
||||
financial_flow_hint?: AssistantMcpDiscoveryFinancialFlowHint;
|
||||
financial_flow_hint_rows?: number;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedRankedValueFlow {
|
||||
@@ -2526,6 +2536,88 @@ function rowCounterpartyValue(row: Record<string, unknown>): string | null {
|
||||
return rowAnalyticsTextValues(row).find(isLikelyCounterpartyToken) ?? null;
|
||||
}
|
||||
|
||||
const FINANCIAL_FLOW_HINTS: AssistantMcpDiscoveryFinancialFlowHint[] = [
|
||||
"loan_or_credit",
|
||||
"bank_fee_or_service",
|
||||
"tax_or_budget",
|
||||
"payroll_or_social",
|
||||
"supplier_payment",
|
||||
"unclear"
|
||||
];
|
||||
|
||||
function normalizeFinancialFlowText(value: unknown): string {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function rowFinancialFlowCorpus(row: Record<string, unknown>): string {
|
||||
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: Record<string, unknown>): AssistantMcpDiscoveryFinancialFlowHint {
|
||||
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(): Record<AssistantMcpDiscoveryFinancialFlowHint, number> {
|
||||
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: Record<AssistantMcpDiscoveryFinancialFlowHint, number>
|
||||
): { hint: AssistantMcpDiscoveryFinancialFlowHint | null; rows: number } {
|
||||
let bestHint: AssistantMcpDiscoveryFinancialFlowHint | null = 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: string, monthText: string, dayText: string): string | null {
|
||||
const year = Number(yearText);
|
||||
const month = Number(monthText);
|
||||
@@ -3104,7 +3196,14 @@ function deriveRankedValueFlow(
|
||||
return null;
|
||||
}
|
||||
|
||||
const buckets = new Map<string, { rows_with_amount: number; total_amount: number }>();
|
||||
const buckets = new Map<
|
||||
string,
|
||||
{
|
||||
rows_with_amount: number;
|
||||
total_amount: number;
|
||||
financial_flow_hint_counts: Record<AssistantMcpDiscoveryFinancialFlowHint, number>;
|
||||
}
|
||||
>();
|
||||
let rowsWithAmount = 0;
|
||||
for (const row of result.rows) {
|
||||
const axisValue = rowCounterpartyValue(row);
|
||||
@@ -3113,9 +3212,14 @@ function deriveRankedValueFlow(
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -3124,13 +3228,18 @@ function deriveRankedValueFlow(
|
||||
}
|
||||
|
||||
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: 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: 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") {
|
||||
@@ -4683,6 +4792,27 @@ function summarizeBusinessOverviewRows(input: {
|
||||
return parts.length > 0 ? parts.join("; ") : null;
|
||||
}
|
||||
|
||||
function financialFlowHintTextRu(bucket: AssistantMcpDiscoveryRankedValueFlowBucket | null | undefined): string {
|
||||
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: AssistantMcpDiscoveryDerivedBusinessOverview | null): string[] {
|
||||
if (!derived) {
|
||||
return [];
|
||||
@@ -4706,6 +4836,10 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
|
||||
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) => !isLikelyFinancialInstitutionCounterparty(item.axis_value));
|
||||
if (nonFinancialLeader) {
|
||||
facts.push(
|
||||
@@ -4724,6 +4858,10 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
|
||||
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) => !isLikelyFinancialInstitutionCounterparty(item.axis_value));
|
||||
if (nonFinancialLeader) {
|
||||
facts.push(
|
||||
|
||||
@@ -486,6 +486,29 @@ function overviewAxisLooksFinancial(row: Record<string, unknown> | null): boolea
|
||||
);
|
||||
}
|
||||
|
||||
function financialFlowHintTextRuFromRecord(row: Record<string, unknown> | null): string | null {
|
||||
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: Record<string, unknown>): string | null {
|
||||
const tax = toRecordObject(overview.tax_position);
|
||||
if (!tax) {
|
||||
@@ -948,6 +971,10 @@ function buildCompactBusinessOverviewReply(
|
||||
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