Добавить financial flow hints для банковских контрагентов
This commit is contained in:
parent
9f62d3653f
commit
e997449785
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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__
|
||||
|
|
|
|||
|
|
@ -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} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;
|
||||
|
|
|
|||
|
|
@ -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}.`);
|
||||
|
|
|
|||
|
|
@ -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})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -360,7 +360,10 @@ const BANK_DOCS_QUERY_TEMPLATE = `
|
|||
"" КАК СчетКт,
|
||||
БанкСписание.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
|
||||
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
|
||||
БанкСписание.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.СписаниеСРасчетногоСчета КАК БанкСписание
|
||||
__WHERE_OUT__
|
||||
|
|
@ -372,7 +375,10 @@ __WHERE_OUT__
|
|||
"" КАК СчетКт,
|
||||
БанкПоступление.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
|
||||
"" КАК НазначениеПлатежа,
|
||||
БанкПоступление.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
|
||||
__WHERE_IN__
|
||||
|
|
@ -644,7 +650,9 @@ const CUSTOMER_REVENUE_PROFILE_QUERY_TEMPLATE = `
|
|||
"" КАК СчетКт,
|
||||
БанкПоступление.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
|
||||
БанкПоступление.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
|
||||
__WHERE_IN__
|
||||
|
|
@ -660,7 +668,10 @@ const SUPPLIER_PAYOUT_PROFILE_QUERY_TEMPLATE = `
|
|||
"" КАК СчетКт,
|
||||
БанкСписание.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
|
||||
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
|
||||
БанкСписание.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.СписаниеСРасчетногоСчета КАК БанкСписание
|
||||
__WHERE_OUT__
|
||||
|
|
|
|||
|
|
@ -736,6 +736,27 @@ function businessOverviewDebtDueDateAgingText(overview: BusinessOverview): strin
|
|||
return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`;
|
||||
}
|
||||
|
||||
function financialFlowHintTextRuFromBucket(bucket: BusinessOverviewRankedBucket | null | undefined): string {
|
||||
const rows = bucket?.financial_flow_hint_rows ?? 0;
|
||||
const rowsText = rows > 0 ? ` (${rows} строк)` : "";
|
||||
if (bucket?.financial_flow_hint === "loan_or_credit") {
|
||||
return ` По полям банковского документа доминирует кредитный/заемный признак${rowsText}; это не обычный поставщик и не клиентская выручка без отдельной проверки назначения.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "bank_fee_or_service") {
|
||||
return ` По полям банковского документа доминирует признак банковской комиссии/услуг банка${rowsText}; это не обычный поставщик товаров/услуг без отдельной проверки договора.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "tax_or_budget") {
|
||||
return ` По полям банковского документа доминирует налоговый/бюджетный признак${rowsText}.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "payroll_or_social") {
|
||||
return ` По полям банковского документа доминирует зарплатный/социальный признак${rowsText}.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "supplier_payment") {
|
||||
return ` По полям банковского документа доминирует признак оплаты поставщику${rowsText}; если получатель по названию является банком, это все равно требует осторожной трактовки.`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function businessOverviewVendorProcurementQualityText(overview: BusinessOverview): string | null {
|
||||
const quality = overview.vendor_procurement_quality;
|
||||
if (!quality) {
|
||||
|
|
@ -759,7 +780,7 @@ function businessOverviewVendorProcurementQualityText(overview: BusinessOverview
|
|||
? ` Договорный профиль: используется ${quality.used_contracts} договоров.`
|
||||
: ` Договорный профиль: используется ${quality.used_contracts}/${quality.total_contracts} договоров${quality.used_contract_share_pct === null ? "" : ` (${quality.used_contract_share_pct}%)`}.`;
|
||||
if (quality.evidence_status === "financial_institution_leads_outgoing_cash") {
|
||||
return `Проверенный procurement-concentration route за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим маршрутом не доказаны.`;
|
||||
return `Проверенный procurement-concentration route за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${financialFlowHintTextRuFromBucket(top)}${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим маршрутом не доказаны.`;
|
||||
}
|
||||
if (quality.evidence_status === "reviewed_procurement_concentration") {
|
||||
return `Проверенный procurement-concentration route за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;
|
||||
|
|
|
|||
|
|
@ -65,6 +65,14 @@ export interface AssistantMcpDiscoveryValueFlowMonthBucket {
|
|||
total_amount_human_ru: string;
|
||||
}
|
||||
|
||||
export type AssistantMcpDiscoveryFinancialFlowHint =
|
||||
| "loan_or_credit"
|
||||
| "bank_fee_or_service"
|
||||
| "tax_or_budget"
|
||||
| "payroll_or_social"
|
||||
| "supplier_payment"
|
||||
| "unclear";
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedValueFlow {
|
||||
value_flow_direction: "incoming_customer_revenue" | "outgoing_supplier_payout";
|
||||
counterparty: string | null;
|
||||
|
|
@ -89,6 +97,8 @@ export interface AssistantMcpDiscoveryRankedValueFlowBucket {
|
|||
total_amount: number;
|
||||
total_amount_human_ru: string;
|
||||
counterparty_role_hint?: CounterpartyRoleHint;
|
||||
financial_flow_hint?: AssistantMcpDiscoveryFinancialFlowHint;
|
||||
financial_flow_hint_rows?: number;
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedRankedValueFlow {
|
||||
|
|
@ -2526,6 +2536,88 @@ function rowCounterpartyValue(row: Record<string, unknown>): string | null {
|
|||
return rowAnalyticsTextValues(row).find(isLikelyCounterpartyToken) ?? null;
|
||||
}
|
||||
|
||||
const FINANCIAL_FLOW_HINTS: AssistantMcpDiscoveryFinancialFlowHint[] = [
|
||||
"loan_or_credit",
|
||||
"bank_fee_or_service",
|
||||
"tax_or_budget",
|
||||
"payroll_or_social",
|
||||
"supplier_payment",
|
||||
"unclear"
|
||||
];
|
||||
|
||||
function normalizeFinancialFlowText(value: unknown): string {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function rowFinancialFlowCorpus(row: Record<string, unknown>): string {
|
||||
return [
|
||||
rowTextValue(row, ["ВидОперации", "OperationType", "operation_type"]),
|
||||
rowTextValue(row, ["НазначениеПлатежа", "PaymentPurpose", "payment_purpose"]),
|
||||
rowTextValue(row, ["Комментарий", "Comment", "comment"]),
|
||||
rowContractValue(row),
|
||||
rowDocumentValue(row)
|
||||
]
|
||||
.map(normalizeFinancialFlowText)
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function classifyRowFinancialFlowHint(row: Record<string, unknown>): AssistantMcpDiscoveryFinancialFlowHint {
|
||||
const corpus = rowFinancialFlowCorpus(row);
|
||||
if (!corpus) {
|
||||
return "unclear";
|
||||
}
|
||||
if (/(?:кредит|кредитн|заем|займ|овердрафт|процент|ссуд|депозит|loan|credit|overdraft|deposit)/iu.test(corpus)) {
|
||||
return "loan_or_credit";
|
||||
}
|
||||
if (/(?:комисс|эквайр|расчетн[оа -]*кассов|ведение\s+счет|банк[а-я\s-]*услуг|банковск[а-я\s-]*услуг|рко|bank\s*fee|acquiring|bank\s*service)/iu.test(corpus)) {
|
||||
return "bank_fee_or_service";
|
||||
}
|
||||
if (/(?:налог|взнос|фсс|фомс|пфр|бюджет|казнач|пен[ия]|штраф|tax|budget|treasury)/iu.test(corpus)) {
|
||||
return "tax_or_budget";
|
||||
}
|
||||
if (/(?:зарплат|заработн|перечислениезаработнойплаты|salary|payroll|wage)/iu.test(corpus)) {
|
||||
return "payroll_or_social";
|
||||
}
|
||||
if (/(?:оплатапоставщику|оплата\s+поставщ|поставщ|supplier|vendor)/iu.test(corpus)) {
|
||||
return "supplier_payment";
|
||||
}
|
||||
return "unclear";
|
||||
}
|
||||
|
||||
function createFinancialFlowHintCounts(): Record<AssistantMcpDiscoveryFinancialFlowHint, number> {
|
||||
return {
|
||||
loan_or_credit: 0,
|
||||
bank_fee_or_service: 0,
|
||||
tax_or_budget: 0,
|
||||
payroll_or_social: 0,
|
||||
supplier_payment: 0,
|
||||
unclear: 0
|
||||
};
|
||||
}
|
||||
|
||||
function dominantFinancialFlowHint(
|
||||
counts: Record<AssistantMcpDiscoveryFinancialFlowHint, number>
|
||||
): { hint: AssistantMcpDiscoveryFinancialFlowHint | null; rows: number } {
|
||||
let bestHint: AssistantMcpDiscoveryFinancialFlowHint | null = null;
|
||||
let bestRows = 0;
|
||||
for (const hint of FINANCIAL_FLOW_HINTS) {
|
||||
const rows = counts[hint] ?? 0;
|
||||
if (rows > bestRows) {
|
||||
bestHint = hint;
|
||||
bestRows = rows;
|
||||
}
|
||||
}
|
||||
if (!bestHint || bestRows <= 0 || bestHint === "unclear") {
|
||||
return { hint: null, rows: 0 };
|
||||
}
|
||||
return { hint: bestHint, rows: bestRows };
|
||||
}
|
||||
|
||||
function normalizeDateParts(yearText: string, monthText: string, dayText: string): string | null {
|
||||
const year = Number(yearText);
|
||||
const month = Number(monthText);
|
||||
|
|
@ -3104,7 +3196,14 @@ function deriveRankedValueFlow(
|
|||
return null;
|
||||
}
|
||||
|
||||
const buckets = new Map<string, { rows_with_amount: number; total_amount: number }>();
|
||||
const buckets = new Map<
|
||||
string,
|
||||
{
|
||||
rows_with_amount: number;
|
||||
total_amount: number;
|
||||
financial_flow_hint_counts: Record<AssistantMcpDiscoveryFinancialFlowHint, number>;
|
||||
}
|
||||
>();
|
||||
let rowsWithAmount = 0;
|
||||
for (const row of result.rows) {
|
||||
const axisValue = rowCounterpartyValue(row);
|
||||
|
|
@ -3113,9 +3212,14 @@ function deriveRankedValueFlow(
|
|||
continue;
|
||||
}
|
||||
rowsWithAmount += 1;
|
||||
const current = buckets.get(axisValue) ?? { rows_with_amount: 0, total_amount: 0 };
|
||||
const current = buckets.get(axisValue) ?? {
|
||||
rows_with_amount: 0,
|
||||
total_amount: 0,
|
||||
financial_flow_hint_counts: createFinancialFlowHintCounts()
|
||||
};
|
||||
current.rows_with_amount += 1;
|
||||
current.total_amount += amount;
|
||||
current.financial_flow_hint_counts[classifyRowFinancialFlowHint(row)] += 1;
|
||||
buckets.set(axisValue, current);
|
||||
}
|
||||
|
||||
|
|
@ -3124,13 +3228,18 @@ function deriveRankedValueFlow(
|
|||
}
|
||||
|
||||
const rankedValues = Array.from(buckets.entries())
|
||||
.map(([axisValue, bucket]) => ({
|
||||
.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(
|
||||
|
|
|
|||
|
|
@ -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})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue