"""Build the fund-flow graph from deduplicated transactions.""" from uuid import UUID from collections import defaultdict from sqlalchemy.ext.asyncio import AsyncSession from app.repositories.transaction_repo import TransactionRepository from app.schemas.transaction import FlowGraphOut, FlowNodeOut, FlowEdgeOut async def build_flow_graph(case_id: UUID, db: AsyncSession) -> FlowGraphOut: repo = TransactionRepository(db) transactions = await repo.get_all_by_case(case_id) valid = [tx for tx in transactions if not tx.is_duplicate] nodes_map: dict[str, str] = {} # label -> type edge_agg: dict[tuple[str, str], dict] = defaultdict( lambda: {"amount": 0.0, "count": 0, "trade_time": ""} ) for tx in valid: self_label = _self_label(tx) counter_label = tx.counterparty_name or "未知对手方" if self_label not in nodes_map: nodes_map[self_label] = "self" if counter_label not in nodes_map: nodes_map[counter_label] = "suspect" if not tx.is_transit else "transit" if tx.direction.value == "out": key = (self_label, counter_label) else: key = (counter_label, self_label) edge_agg[key]["amount"] += float(tx.amount) edge_agg[key]["count"] += 1 time_str = tx.trade_time.strftime("%Y-%m-%d %H:%M") if tx.trade_time else "" if not edge_agg[key]["trade_time"]: edge_agg[key]["trade_time"] = time_str nodes = [ FlowNodeOut(id=f"n-{i}", label=label, type=ntype) for i, (label, ntype) in enumerate(nodes_map.items()) ] label_to_id = {n.label: n.id for n in nodes} edges = [ FlowEdgeOut( source=label_to_id[src], target=label_to_id[tgt], amount=info["amount"], count=info["count"], trade_time=info["trade_time"], ) for (src, tgt), info in edge_agg.items() ] return FlowGraphOut(nodes=nodes, edges=edges) def _self_label(tx) -> str: app_names = { "wechat": "微信支付", "alipay": "支付宝", "bank": f"银行卡({tx.self_account_tail_no})" if tx.self_account_tail_no else "银行卡", "digital_wallet": "数字钱包", "other": "其他账户", } return app_names.get(tx.source_app.value, "未知账户")