Добавить financial flow hints для банковских контрагентов

This commit is contained in:
dctouch 2026-05-13 00:57:59 +03:00
parent 9f62d3653f
commit e997449785
12 changed files with 475 additions and 45 deletions

View File

@ -88,7 +88,8 @@ Fresh validation cut:
- Current live canary: `phase96_inventory_reserve_liquidation_quality_rerun` accepted `2/2`.
- Current accepted autorun: `AGENT | Phase 96 inventory reserve/liquidation quality-events` (`gen-ag05122057-c9786e`).
- Implementation breadth: `~99% (Open-World Bounded Autonomy Breadth through Slice 25)`.
- Next active slice: start the broader open-world schema/primitive discovery module and use phase91-phase96 as regression canaries.
- Active broader autonomy slice: `Open-World Schema/Primitive Discovery`, starting with `Financial Counterparty Flow Hints`: bank-document money-flow recipes now expose operation/purpose/comment fields and ranked value-flow buckets carry `financial_flow_hint` so bank-like leaders are not treated as ordinary suppliers/customers by name alone.
- Next active slice: add live semantic replay around bank-like counterparty wording, then continue broader schema/primitive discovery while using phase91-phase96 as regression canaries.
- Active module progress: `~99% (Agentic Semantic Development Loop, accepted dogfood loop + autorun hygiene; manual GUI confirmation still required)`.
## Reporting Rule
@ -101,6 +102,7 @@ Use these labels when reporting progress:
- `Прогресс модуля: 99% (Agentic Semantic Development Loop, accepted dogfood loop + autorun hygiene; manual GUI confirmation still required)` when discussing the current development-loop operating layer.
- `Прогресс модуля: 100% (Open-World Route Candidate Promotion, declared phase90 slice accepted)` when discussing the route-candidate handoff slice itself.
- `Прогресс модуля: 100% (Route-Candidate-Driven Enablement Loop, final reviewed proof-family route accepted; use as regression gate)` when discussing the current candidate-driven enablement loop.
- `Прогресс модуля: 12% (Open-World Schema/Primitive Discovery, active slice: financial counterparty flow hints)` when discussing the current broader schema/primitive discovery module.
- `Open-World Business Overview implementation breadth: ~99%, Semantic Control Gate critical subset accepted, fat GUI pack still pending` when discussing only the already wired Slice 25 breadth.
- `Прогресс модуля: X% (Open-World Bounded Autonomy Breadth, active slice: <name>)` for later breadth work after the Semantic Control Gate is accepted.
@ -158,18 +160,19 @@ For current planning, read:
1. `README.md`
2. this document
3. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md`
4. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md`
5. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md`
6. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md`
7. `27 - proof_family_enablement_candidates_2026-05-10.md`
8. `26 - route_candidate_driven_enablement_loop_2026-05-10.md`
9. `25 - open_world_route_candidate_promotion_2026-05-10.md`
10. `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md`
11. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`
12. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
13. `20 - planner_autonomy_consolidation_2026-05-01.md`
14. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md`
15. `17 - post_f_semantic_integrity_hardening_2026-04-23.md`
16. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md`
4. `32 - financial_counterparty_flow_hints_2026-05-13.md`
5. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md`
6. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md`
7. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md`
8. `27 - proof_family_enablement_candidates_2026-05-10.md`
9. `26 - route_candidate_driven_enablement_loop_2026-05-10.md`
10. `25 - open_world_route_candidate_promotion_2026-05-10.md`
11. `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md`
12. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`
13. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
14. `20 - planner_autonomy_consolidation_2026-05-01.md`
15. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md`
16. `17 - post_f_semantic_integrity_hardening_2026-04-23.md`
17. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md`
Documents `01` through `15` remain valuable, but mostly as the historical architecture trail.

View File

@ -0,0 +1,53 @@
# 32 - Financial Counterparty Flow Hints (2026-05-13)
This note opens the next broader autonomy slice after the closed `Route-Candidate-Driven Enablement Loop`.
The problem came from real business review: bank-like counterparties such as `СБЕРБАНК, ПАО` can dominate incoming or outgoing money-flow rankings. If the assistant treats that row as an ordinary customer or supplier, the business answer becomes misleading even when the amount itself is correct.
## Scope
The first cut adds reviewed document-field hints to bank-document money-flow routes:
- `bank_operations_by_counterparty` now keeps outgoing/incoming bank-document columns aligned across `ОБЪЕДИНИТЬ ВСЕ` while exposing operation type, payment purpose where available, and comment;
- `customer_revenue_and_payments` now includes incoming bank document `ВидОперации` and `Комментарий`;
- `supplier_payouts_profile` now includes outgoing bank document `ВидОперации`, `НазначениеПлатежа`, and `Комментарий`;
- ranked value-flow buckets now carry `financial_flow_hint`;
- current hints are `loan_or_credit` (including deposits/credit-like bank instruments), `bank_fee_or_service`, `tax_or_budget`, `payroll_or_social`, `supplier_payment`, and `unclear`;
- business-overview evidence can now say that a bank-like leader is a bank fee/service, credit/loan, tax/budget, payroll/social, or ordinary supplier-payment signal instead of using name-only caution.
## Validation
Local checks:
- `npm.cmd test -- assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts`: `97/97` passed, `1` skipped;
- `npm.cmd run build`: passed.
Live MCP checks:
- `bank_operations_by_counterparty` for `СБЕРБАНК` returned `fetched=593`, `matched=10`, `error=null`, proving the aligned bank-doc union executes with the enriched column set;
- `supplier_payouts_profile` for `СБЕРБАНК` returned `fetched=5`, `matched=5`, `error=null`;
- the live rows include `ВидОперации=Прочее списание` and `НазначениеПлатежа=Комиссия банка`;
- `customer_revenue_and_payments` for `СБЕРБАНК` returned `fetched=5`, `matched=5`, `error=null`;
- the live rows include incoming `ВидОперации`, including `Прочее поступление` and `Возврат от поставщика`.
## Status
Current module wording:
`Open-World Schema/Primitive Discovery, active slice: financial counterparty flow hints`
Progress: `12%`.
This is not a full financial-purpose classifier yet. It is the first reviewed primitive that lets downstream business answers avoid the worst bank-as-supplier/customer overclaim.
## Next Work
1. Add a small live semantic replay for bank-like counterparty wording in business overview.
2. Extend hints where real 1C rows expose more payment-purpose families.
3. Use this as the first canary for broader schema/primitive discovery after phase96.
See also:
- [21 - current_status_canon_2026-05-01.md](./21%20-%20current_status_canon_2026-05-01.md)
- [26 - route_candidate_driven_enablement_loop_2026-05-10.md](./26%20-%20route_candidate_driven_enablement_loop_2026-05-10.md)
- [31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md](./31%20-%20inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md)

View File

@ -49,6 +49,7 @@ This package answers the next question:
29. [29 - debt_due_date_aging_reviewed_route_2026-05-10.md](./29%20-%20debt_due_date_aging_reviewed_route_2026-05-10.md)
30. [30 - vendor_procurement_quality_reviewed_route_2026-05-12.md](./30%20-%20vendor_procurement_quality_reviewed_route_2026-05-12.md)
31. [31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md](./31%20-%20inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md)
32. [32 - financial_counterparty_flow_hints_2026-05-13.md](./32%20-%20financial_counterparty_flow_hints_2026-05-13.md)
## Current Status Snapshot (2026-05-12)
@ -117,6 +118,7 @@ Status canon for planning:
- The second reviewed proof-family route slice is [29 - debt_due_date_aging_reviewed_route_2026-05-10.md](./29%20-%20debt_due_date_aging_reviewed_route_2026-05-10.md).
- The third reviewed proof-family route slice is [30 - vendor_procurement_quality_reviewed_route_2026-05-12.md](./30%20-%20vendor_procurement_quality_reviewed_route_2026-05-12.md).
- The fourth/final reviewed proof-family route slice is [31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md](./31%20-%20inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md).
- The current broader schema/primitive discovery slice is [32 - financial_counterparty_flow_hints_2026-05-13.md](./32%20-%20financial_counterparty_flow_hints_2026-05-13.md).
It now documents a turnaround that is already operational in code, already materially past the acute regression breakpoint, and already moved through bounded MCP autonomy, Post-F hardening, inventory breadth proof, and the declared Planner Autonomy slice:
@ -187,6 +189,7 @@ Current honest status:
- Planner Autonomy Consolidation progress: `100%` for the declared module, with catalog-fabric, value-flow arbitration, lifecycle bounded inference, broad-evaluation bridge, inventory catalog templates, inventory runtime-boundary honesty, exact inventory recipe bridging, unambiguous metadata-surface lane inference, catalog chain-template scoring, structured chain-match contract exposure, runtime/debug propagation, subject-aware bidirectional comparison arbitration, structured catalog-alignment verdicts, representative alignment regression guard, catalog-alignment reason-code telemetry, explicit `alignment_status` propagation, truth-harness/acceptance-matrix surfacing, soft divergence warning, `catalog_alignment_ok` acceptance invariant, step-level expected catalog-alignment assertions, phase66 and phase32 spec alignment expectations, AGENT source-catalog surfacing, generated phase83 mixed planner-brain replay spec, checked-source user-facing error sanitation, surface-grounded catalog promotion, and guarded live phase83 acceptance validated. Broader unfamiliar 1C asks are now next-module breadth work rather than an open blocker inside this declared slice
- Open-World Route Candidate Promotion progress: `100%` for the declared phase90 slice, with structured `route_candidate` runtime contract, artifact propagation, live semantic replay accepted at `5/5`, and accepted AGENT autorun persistence; broader autonomous route enablement remains the next active slice
- Route-Candidate-Driven Enablement Loop progress: `100%`, with deterministic repair-target grouping, Lead Codex handoff surfacing, local tooling tests, live phase91 canary acceptance, phase92 proof-family candidates accepted/saved as a user-runnable AGENT autorun, `accounting_profit_margin` promoted into reviewed 90/91/99 execution by phase93 live replay, `debt_due_date_aging_quality` promoted into reviewed payment-term/open-balance execution by phase94 live replay, `vendor_risk_procurement_quality` promoted into reviewed procurement-concentration evidence by phase95 live replay, and `inventory_reserve_liquidation_quality` promoted into reviewed inventory quality-event evidence by phase96 live replay; the declared route-candidate-driven enablement loop is now closed and should be used as a regression gate for the next broader autonomy slice
- Open-World Schema/Primitive Discovery progress: `12%`, active slice `financial counterparty flow hints`; bank-document money-flow recipes now expose operation/purpose/comment fields, ranked value-flow buckets carry `financial_flow_hint`, and Sberbank-like leaders can be treated as bank fee/service, credit/loan, tax/budget, payroll/social, supplier-payment, or unclear evidence instead of name-only supplier/customer overclaim.
- graph snapshot after latest rebuild: see `graphify-out/GRAPH_REPORT.md`
- current regression-gate breakpoint:
- the validated hot paths are no longer structurally broken;
@ -357,6 +360,7 @@ Read in this order:
30. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md`
31. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md`
32. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md`
33. `32 - financial_counterparty_flow_hints_2026-05-13.md`
## Planning Rules

View File

@ -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__

View File

@ -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} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;

View File

@ -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]) => ({
.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)
}))
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}.`);

View File

@ -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})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`);
}

View File

@ -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__

View File

@ -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} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;

View File

@ -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]) => ({
.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)
}))
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(

View File

@ -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})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`

View File

@ -345,13 +345,13 @@ describe("assistant MCP discovery pilot executor", () => {
const deps = buildSequentialDeps([
{
rows: [
{ Period: "2020-01-15T00:00:00", Amount: 1200000, Counterparty: "СБЕРБАНК, ПАО" },
{ Period: "2020-01-15T00:00:00", Amount: 1200000, Counterparty: "СБЕРБАНК, ПАО", ВидОперации: "Возврат от поставщика", Договор: "Депозитный договор" },
{ Period: "2020-02-15T00:00:00", Amount: 800000, Counterparty: "Группа СВК" }
]
},
{
rows: [
{ Period: "2020-01-20T00:00:00", Amount: 650000, Counterparty: "СБЕРБАНК, ПАО" },
{ Period: "2020-01-20T00:00:00", Amount: 650000, Counterparty: "СБЕРБАНК, ПАО", ВидОперации: "ПрочееСписание", НазначениеПлатежа: "Комиссия банка за ведение счета" },
{ Period: "2020-03-20T00:00:00", Amount: 50000, Counterparty: "ООО Поставщик" }
]
},
@ -365,7 +365,8 @@ describe("assistant MCP discovery pilot executor", () => {
expect(result.derived_business_overview?.top_customers[0]).toMatchObject({
axis_value: "СБЕРБАНК, ПАО",
counterparty_role_hint: "bank_or_financial_institution"
counterparty_role_hint: "bank_or_financial_institution",
financial_flow_hint: "loan_or_credit"
});
expect(result.derived_business_overview?.top_customers[1]).toMatchObject({
axis_value: "Группа СВК",
@ -373,13 +374,16 @@ describe("assistant MCP discovery pilot executor", () => {
});
expect(result.derived_business_overview?.top_suppliers[0]).toMatchObject({
axis_value: "СБЕРБАНК, ПАО",
counterparty_role_hint: "bank_or_financial_institution"
counterparty_role_hint: "bank_or_financial_institution",
financial_flow_hint: "bank_fee_or_service"
});
const confirmedFacts = result.evidence.confirmed_facts.join("\n");
const inferredFacts = result.evidence.inferred_facts.join("\n");
expect(confirmedFacts).toContain("Крупнейший входящий денежный источник");
expect(confirmedFacts).toContain("Крупнейший небанковский входящий контрагент");
expect(confirmedFacts).toContain("Крупнейший получатель исходящих денег");
expect(confirmedFacts).toContain("кредитный/заемный признак");
expect(confirmedFacts).toContain("банковской комиссии/услуг банка");
expect(confirmedFacts).not.toContain("Самый крупный подтвержденный клиент в проверенном срезе: СБЕРБАНК");
expect(confirmedFacts).not.toContain("Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: СБЕРБАНК");
expect(inferredFacts).toContain("outgoing cash concentration proxy");