Добавить 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 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`). - 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)`. - 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)`. - Active module progress: `~99% (Agentic Semantic Development Loop, accepted dogfood loop + autorun hygiene; manual GUI confirmation still required)`.
## Reporting Rule ## 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. - `Прогресс модуля: 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% (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. - `Прогресс модуля: 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. - `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. - `Прогресс модуля: 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` 1. `README.md`
2. this document 2. this document
3. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md` 3. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md`
4. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md` 4. `32 - financial_counterparty_flow_hints_2026-05-13.md`
5. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md` 5. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md`
6. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md` 6. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md`
7. `27 - proof_family_enablement_candidates_2026-05-10.md` 7. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md`
8. `26 - route_candidate_driven_enablement_loop_2026-05-10.md` 8. `27 - proof_family_enablement_candidates_2026-05-10.md`
9. `25 - open_world_route_candidate_promotion_2026-05-10.md` 9. `26 - route_candidate_driven_enablement_loop_2026-05-10.md`
10. `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md` 10. `25 - open_world_route_candidate_promotion_2026-05-10.md`
11. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md` 11. `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md`
12. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md` 12. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`
13. `20 - planner_autonomy_consolidation_2026-05-01.md` 13. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
14. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md` 14. `20 - planner_autonomy_consolidation_2026-05-01.md`
15. `17 - post_f_semantic_integrity_hardening_2026-04-23.md` 15. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md`
16. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.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. 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) 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) 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) 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) ## 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 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 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 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: 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 - 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 - 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 - 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` - graph snapshot after latest rebuild: see `graphify-out/GRAPH_REPORT.md`
- current regression-gate breakpoint: - current regression-gate breakpoint:
- the validated hot paths are no longer structurally broken; - 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` 30. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md`
31. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md` 31. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md`
32. `31 - inventory_reserve_liquidation_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 ## Planning Rules

View File

@ -346,7 +346,10 @@ const BANK_DOCS_QUERY_TEMPLATE = `
"" КАК СчетКт, "" КАК СчетКт,
БанкСписание.СуммаДокумента КАК Сумма, БанкСписание.СуммаДокумента КАК Сумма,
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент, ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
БанкСписание.Комментарий КАК Комментарий
ИЗ ИЗ
Документ.СписаниеСРасчетногоСчета КАК БанкСписание Документ.СписаниеСРасчетногоСчета КАК БанкСписание
__WHERE_OUT__ __WHERE_OUT__
@ -358,7 +361,10 @@ __WHERE_OUT__
"" КАК СчетКт, "" КАК СчетКт,
БанкПоступление.СуммаДокумента КАК Сумма, БанкПоступление.СуммаДокумента КАК Сумма,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент, ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
"" КАК НазначениеПлатежа,
БанкПоступление.Комментарий КАК Комментарий
ИЗ ИЗ
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
__WHERE_IN__ __WHERE_IN__
@ -624,7 +630,9 @@ const CUSTOMER_REVENUE_PROFILE_QUERY_TEMPLATE = `
"" КАК СчетКт, "" КАК СчетКт,
БанкПоступление.СуммаДокумента КАК Сумма, БанкПоступление.СуммаДокумента КАК Сумма,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент, ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
БанкПоступление.Комментарий КАК Комментарий
ИЗ ИЗ
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
__WHERE_IN__ __WHERE_IN__
@ -639,7 +647,10 @@ const SUPPLIER_PAYOUT_PROFILE_QUERY_TEMPLATE = `
"" КАК СчетКт, "" КАК СчетКт,
БанкСписание.СуммаДокумента КАК Сумма, БанкСписание.СуммаДокумента КАК Сумма,
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент, ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
БанкСписание.Комментарий КАК Комментарий
ИЗ ИЗ
Документ.СписаниеСРасчетногоСчета КАК БанкСписание Документ.СписаниеСРасчетногоСчета КАК БанкСписание
__WHERE_OUT__ __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}.`; 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) { function businessOverviewVendorProcurementQualityText(overview) {
const quality = overview.vendor_procurement_quality; const quality = overview.vendor_procurement_quality;
if (!quality) { if (!quality) {
@ -617,7 +637,7 @@ function businessOverviewVendorProcurementQualityText(overview) {
? ` Договорный профиль: используется ${quality.used_contracts} договоров.` ? ` Договорный профиль: используется ${quality.used_contracts} договоров.`
: ` Договорный профиль: используется ${quality.used_contracts}/${quality.total_contracts} договоров${quality.used_contract_share_pct === null ? "" : ` (${quality.used_contract_share_pct}%)`}.`; : ` Договорный профиль: используется ${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") { 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") { if (quality.evidence_status === "reviewed_procurement_concentration") {
return `Проверенный procurement-concentration route за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`; 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; 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) { function normalizeDateParts(yearText, monthText, dayText) {
const year = Number(yearText); const year = Number(yearText);
const month = Number(monthText); const month = Number(monthText);
@ -2119,22 +2193,32 @@ function deriveRankedValueFlow(result, input) {
continue; continue;
} }
rowsWithAmount += 1; 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.rows_with_amount += 1;
current.total_amount += amount; current.total_amount += amount;
current.financial_flow_hint_counts[classifyRowFinancialFlowHint(row)] += 1;
buckets.set(axisValue, current); buckets.set(axisValue, current);
} }
if (rowsWithAmount <= 0 || buckets.size <= 0) { if (rowsWithAmount <= 0 || buckets.size <= 0) {
return null; return null;
} }
const rankedValues = Array.from(buckets.entries()) const rankedValues = Array.from(buckets.entries())
.map(([axisValue, bucket]) => ({ .map(([axisValue, bucket]) => {
axis_value: axisValue, const dominantHint = dominantFinancialFlowHint(bucket.financial_flow_hint_counts);
rows_with_amount: bucket.rows_with_amount, return {
total_amount: bucket.total_amount, axis_value: axisValue,
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount), rows_with_amount: bucket.rows_with_amount,
counterparty_role_hint: (0, counterpartyRoleHeuristics_1.counterpartyRoleHintForName)(axisValue) 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) => { .sort((left, right) => {
const amountDelta = right.total_amount - left.total_amount; const amountDelta = right.total_amount - left.total_amount;
if (input.rankingNeed === "bottom_asc") { if (input.rankingNeed === "bottom_asc") {
@ -3430,6 +3514,26 @@ function summarizeBusinessOverviewRows(input) {
} }
return parts.length > 0 ? parts.join("; ") : null; 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) { function buildBusinessOverviewConfirmedFacts(derived) {
if (!derived) { if (!derived) {
return []; return [];
@ -3447,6 +3551,10 @@ function buildBusinessOverviewConfirmedFacts(derived) {
const leader = derived.top_customers[0]; const leader = derived.top_customers[0];
if ((0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(leader.axis_value)) { if ((0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(leader.axis_value)) {
facts.push(`Крупнейший входящий денежный источник в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа это не доказанный клиент или бизнес-выручка.`); 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)); const nonFinancialLeader = derived.top_customers.slice(1).find((item) => !(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(item.axis_value));
if (nonFinancialLeader) { if (nonFinancialLeader) {
facts.push(`Крупнейший небанковский входящий контрагент в проверенном срезе: ${nonFinancialLeader.axis_value}${nonFinancialLeader.total_amount_human_ru}.`); facts.push(`Крупнейший небанковский входящий контрагент в проверенном срезе: ${nonFinancialLeader.axis_value}${nonFinancialLeader.total_amount_human_ru}.`);
@ -3460,6 +3568,10 @@ function buildBusinessOverviewConfirmedFacts(derived) {
const leader = derived.top_suppliers[0]; const leader = derived.top_suppliers[0];
if ((0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(leader.axis_value)) { if ((0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(leader.axis_value)) {
facts.push(`Крупнейший получатель исходящих денег в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора это не доказанный обычный поставщик.`); 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)); const nonFinancialLeader = derived.top_suppliers.slice(1).find((item) => !(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(item.axis_value));
if (nonFinancialLeader) { if (nonFinancialLeader) {
facts.push(`Крупнейший небанковский получатель исходящих денег в проверенном срезе: ${nonFinancialLeader.axis_value}${nonFinancialLeader.total_amount_human_ru}.`); 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" || return (row.counterparty_role_hint === "bank_or_financial_institution" ||
(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(row.axis_value)); (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) { function businessOverviewTaxLine(overview) {
const tax = toRecordObject(overview.tax_position); const tax = toRecordObject(overview.tax_position);
if (!tax) { if (!tax) {
@ -795,6 +817,10 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : ""; const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
if (status === "financial_institution_leads_outgoing_cash") { if (status === "financial_institution_leads_outgoing_cash") {
lines.push(`Коротко: проверенный procurement-concentration route за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`); lines.push(`Коротко: проверенный procurement-concentration route за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
if (financialHintText) {
lines.push(financialHintText);
}
if (nonFinancialName) { if (nonFinancialName) {
lines.push(`Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`); lines.push(`Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`);
} }

View File

@ -360,7 +360,10 @@ const BANK_DOCS_QUERY_TEMPLATE = `
"" КАК СчетКт, "" КАК СчетКт,
БанкСписание.СуммаДокумента КАК Сумма, БанкСписание.СуммаДокумента КАК Сумма,
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент, ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
БанкСписание.Комментарий КАК Комментарий
ИЗ ИЗ
Документ.СписаниеСРасчетногоСчета КАК БанкСписание Документ.СписаниеСРасчетногоСчета КАК БанкСписание
__WHERE_OUT__ __WHERE_OUT__
@ -372,7 +375,10 @@ __WHERE_OUT__
"" КАК СчетКт, "" КАК СчетКт,
БанкПоступление.СуммаДокумента КАК Сумма, БанкПоступление.СуммаДокумента КАК Сумма,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент, ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
"" КАК НазначениеПлатежа,
БанкПоступление.Комментарий КАК Комментарий
ИЗ ИЗ
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
__WHERE_IN__ __WHERE_IN__
@ -644,7 +650,9 @@ const CUSTOMER_REVENUE_PROFILE_QUERY_TEMPLATE = `
"" КАК СчетКт, "" КАК СчетКт,
БанкПоступление.СуммаДокумента КАК Сумма, БанкПоступление.СуммаДокумента КАК Сумма,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент, ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
БанкПоступление.Комментарий КАК Комментарий
ИЗ ИЗ
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
__WHERE_IN__ __WHERE_IN__
@ -660,7 +668,10 @@ const SUPPLIER_PAYOUT_PROFILE_QUERY_TEMPLATE = `
"" КАК СчетКт, "" КАК СчетКт,
БанкСписание.СуммаДокумента КАК Сумма, БанкСписание.СуммаДокумента КАК Сумма,
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент, ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
БанкСписание.Комментарий КАК Комментарий
ИЗ ИЗ
Документ.СписаниеСРасчетногоСчета КАК БанкСписание Документ.СписаниеСРасчетногоСчета КАК БанкСписание
__WHERE_OUT__ __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}.`; 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 { function businessOverviewVendorProcurementQualityText(overview: BusinessOverview): string | null {
const quality = overview.vendor_procurement_quality; const quality = overview.vendor_procurement_quality;
if (!quality) { if (!quality) {
@ -759,7 +780,7 @@ function businessOverviewVendorProcurementQualityText(overview: BusinessOverview
? ` Договорный профиль: используется ${quality.used_contracts} договоров.` ? ` Договорный профиль: используется ${quality.used_contracts} договоров.`
: ` Договорный профиль: используется ${quality.used_contracts}/${quality.total_contracts} договоров${quality.used_contract_share_pct === null ? "" : ` (${quality.used_contract_share_pct}%)`}.`; : ` Договорный профиль: используется ${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") { 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") { if (quality.evidence_status === "reviewed_procurement_concentration") {
return `Проверенный procurement-concentration route за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`; return `Проверенный procurement-concentration route за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;

View File

@ -65,6 +65,14 @@ export interface AssistantMcpDiscoveryValueFlowMonthBucket {
total_amount_human_ru: string; 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 { export interface AssistantMcpDiscoveryDerivedValueFlow {
value_flow_direction: "incoming_customer_revenue" | "outgoing_supplier_payout"; value_flow_direction: "incoming_customer_revenue" | "outgoing_supplier_payout";
counterparty: string | null; counterparty: string | null;
@ -89,6 +97,8 @@ export interface AssistantMcpDiscoveryRankedValueFlowBucket {
total_amount: number; total_amount: number;
total_amount_human_ru: string; total_amount_human_ru: string;
counterparty_role_hint?: CounterpartyRoleHint; counterparty_role_hint?: CounterpartyRoleHint;
financial_flow_hint?: AssistantMcpDiscoveryFinancialFlowHint;
financial_flow_hint_rows?: number;
} }
export interface AssistantMcpDiscoveryDerivedRankedValueFlow { export interface AssistantMcpDiscoveryDerivedRankedValueFlow {
@ -2526,6 +2536,88 @@ function rowCounterpartyValue(row: Record<string, unknown>): string | null {
return rowAnalyticsTextValues(row).find(isLikelyCounterpartyToken) ?? 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 { function normalizeDateParts(yearText: string, monthText: string, dayText: string): string | null {
const year = Number(yearText); const year = Number(yearText);
const month = Number(monthText); const month = Number(monthText);
@ -3104,7 +3196,14 @@ function deriveRankedValueFlow(
return null; 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; let rowsWithAmount = 0;
for (const row of result.rows) { for (const row of result.rows) {
const axisValue = rowCounterpartyValue(row); const axisValue = rowCounterpartyValue(row);
@ -3113,9 +3212,14 @@ function deriveRankedValueFlow(
continue; continue;
} }
rowsWithAmount += 1; 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.rows_with_amount += 1;
current.total_amount += amount; current.total_amount += amount;
current.financial_flow_hint_counts[classifyRowFinancialFlowHint(row)] += 1;
buckets.set(axisValue, current); buckets.set(axisValue, current);
} }
@ -3124,13 +3228,18 @@ function deriveRankedValueFlow(
} }
const rankedValues = Array.from(buckets.entries()) const rankedValues = Array.from(buckets.entries())
.map(([axisValue, bucket]) => ({ .map(([axisValue, bucket]) => {
axis_value: axisValue, const dominantHint = dominantFinancialFlowHint(bucket.financial_flow_hint_counts);
rows_with_amount: bucket.rows_with_amount, return {
total_amount: bucket.total_amount, axis_value: axisValue,
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount), rows_with_amount: bucket.rows_with_amount,
counterparty_role_hint: counterpartyRoleHintForName(axisValue) 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) => { .sort((left, right) => {
const amountDelta = right.total_amount - left.total_amount; const amountDelta = right.total_amount - left.total_amount;
if (input.rankingNeed === "bottom_asc") { if (input.rankingNeed === "bottom_asc") {
@ -4683,6 +4792,27 @@ function summarizeBusinessOverviewRows(input: {
return parts.length > 0 ? parts.join("; ") : null; 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[] { function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDerivedBusinessOverview | null): string[] {
if (!derived) { if (!derived) {
return []; return [];
@ -4706,6 +4836,10 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
facts.push( facts.push(
`Крупнейший входящий денежный источник в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа это не доказанный клиент или бизнес-выручка.` `Крупнейший входящий денежный источник в проверенном срезе: ${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)); const nonFinancialLeader = derived.top_customers.slice(1).find((item) => !isLikelyFinancialInstitutionCounterparty(item.axis_value));
if (nonFinancialLeader) { if (nonFinancialLeader) {
facts.push( facts.push(
@ -4724,6 +4858,10 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
facts.push( facts.push(
`Крупнейший получатель исходящих денег в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора это не доказанный обычный поставщик.` `Крупнейший получатель исходящих денег в проверенном срезе: ${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)); const nonFinancialLeader = derived.top_suppliers.slice(1).find((item) => !isLikelyFinancialInstitutionCounterparty(item.axis_value));
if (nonFinancialLeader) { if (nonFinancialLeader) {
facts.push( 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 { function businessOverviewTaxLine(overview: Record<string, unknown>): string | null {
const tax = toRecordObject(overview.tax_position); const tax = toRecordObject(overview.tax_position);
if (!tax) { if (!tax) {
@ -948,6 +971,10 @@ function buildCompactBusinessOverviewReply(
lines.push( lines.push(
`Коротко: проверенный procurement-concentration route за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.` `Коротко: проверенный procurement-concentration route за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
); );
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
if (financialHintText) {
lines.push(financialHintText);
}
if (nonFinancialName) { if (nonFinancialName) {
lines.push( lines.push(
`Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.` `Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`

View File

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