# 005 规范转账事件与双边归并技术决策

对应 Issue：B-43/B-83/B-114（`docs/issues/006-p1-canonical-transfer-matching.md`）。

## 分层模型

- 银行源行是不可变观察；匹配层从不修改 `source_rows`。
- 新增“不可变决定日志 + 可重建当前投影”的规范事件层：
  - `transfer_match_decisions` 等证据表只增不改（UPDATE/DELETE 被触发器拒绝）。
  - `current_transfer_decisions` 与 `transfer_observation_claims` 是可更新、可重建的
    当前投影，`source_row_id` 主键保证一条观察不可能同时属于两个当前事件。
- B-44 余额计算只读 `eligible_intercompany_events` 视图，只含
  `active + current + classification='intercompany'`，且单边事件必须
  `locked = 1`（管理员按证据确认）才可计算；paired 事件不受锁定状态限制；
  单边内部、待审、同公司调拨、外部事件全部排除。

## 已确认业务口径（B-114）

1. 单边流水即使双方公司都能由批准账号唯一确认，也先进入未决金额，不进入 B-44
   已确认往来余额；只有完成双边归并（`intercompany` + `paired`）或管理员按证据确认
   （`intercompany` + locked 单边）后才可计算。
2. 跨日事件经济日期取付款方 outgoing 银行入账时间，与导入顺序无关。
3. 自动窗口 v1：非空参考号相同且账号镜像 <= 3 自然日（M1）；无参考号精确账号镜像
   <= 1 自然日（M2）；别名/个人映射镜像 <= 1 自然日且参考号或摘要相等（M3）。
   超窗或同层多候选全部进入人工审核，禁止按行号、导入顺序或名字破平局。
4. 个人过账映射绑定具体账号、生效区间、允许方向和代表公司，必须管理员批准；
   姓名只作辅助证据，不能单独定案。

## 参与方解析

- 本方：源行 `own_account` 精确命中生效期内已批准账户（`own_exact`）；缺失时才用
  上传时持久化的批准上传账户（`upload_account`），且必须与批次公司一致。冲突进入
  审核，不以 `company_id` 或户名反推。
- 对方：严格按账号层解析——精确账号 -> 已审核账号别名 -> 已批准且在窗口内、方向
  允许的个人过账映射；每层唯一最佳结果才继续。户名/户名别名只用于一致性/冲突证据，
  冲突降级到审核。
- 方向：`expense > 0 且 income = 0` 为 outgoing；`income > 0 且 expense = 0` 为
  incoming；双正/双零/负数/缺币种进入 `unresolved`。金额用 `Decimal` 精确相等，
  不设手续费容差。

## 匹配与状态机

- 候选硬门槛：双方工作表 confirmed、方向相反、金额与币种相等、公司端点互为反向。
- M1/M2/M3 唯一候选自动 paired；R1（端点可确认但镜像/参考号证据不足）与 R2（同层
  多候选）进入 `needs_review`；待审行不能被其他自动匹配抢占。
- 状态：`unresolved -> internal_single -> matched`、`needs_review -> matched |
  same_company_transfer | external`；单边确认两端同公司立即为同公司调拨。
- 幂等：rule_version + 当前观察集合 + 证据未变时零写入；locked 人工决定自动重跑
  跳过；纠错只能通过 `reverse`（`mode='reversal'` 新记录）+ 原因，之后允许重跑。

## 事务、并发与回滚

- “确认工作表 + 为新增确认行建立/更新事件 + audit”在同一个服务事务，任一失败整体
  回滚，不存在 confirmed 但未匹配的半成品。
- 独立重跑使用 `BEGIN IMMEDIATE`，按 `source_row_id` 升序写，SQLite 30 秒 busy
  timeout 保留；投影可从只增日志重建（`rebuild_current_projection`）。
- migration down 只用于测试/上线前回退；生产产生决定后默认保留 migration 5 与审计
  数据，不执行破坏性 down。

## Decimal

金额全程以规范十进制字符串存储和比较，任何路径都不转 `float`；`100.0` 与 `100.00`
视为相等，`100.00` 与 `100.01` 不匹配。
