移交报销真实 OCR 与科目映射余项并归档费用政策票据门禁任务(08-29-reimbursement-expense-policy-invoice-guard)
- 报销发票真实 OCR 验收(RC-OCR-1–4)与财务科目映射(RC-ACCOUNT)移交 09-08-reimbursement-payment-closeout,保留三层既有证据与责任分工 - 归档父任务及部署子任务 08-30-reimbursement-expense-policy-invoice-guard-deploy;实现 6e999d5/e5685f1 包含于生产 4a5cd8f2,原清单不补勾 - 归档后修正指向接收任务的相对链接 Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,124 @@
|
||||
# 设计:报销费用口径与票据明细门禁
|
||||
|
||||
## 最新合同:选票建明细与待补分类(2026-09-09,已实施待复核)
|
||||
|
||||
当前状态以本节与 implement 顶部为准。下文实施前方案对比、B1/B2 数据模型与硬门禁描述保留为历史设计,不构成本次已实现或已验收声明;尤其 assets 批准依据没有现行硬门禁,旧版“应用回滚仅关闭 flag”不适用于本次可空类型数据。
|
||||
|
||||
### 最小实施与已批准的本地持久化合同
|
||||
|
||||
本阶段绑定接口增量:`POST /invoice-pool/:id/bind` 的报销分支增加互斥的 `createItem: true`(不能同时传 linkedItemId),明确一票一明细。旧绑定调用保持原合同。金额及开票日期从锁定的票据读取;可靠铁路票种默认火车票类型,其余类型 null。当前 OCR 没有票面项目时说明保存空字符串(数据库列仍非空),不以商户/文件名充当用途;提交前集中要求补说明,普通编辑仍可修正。不把开票日期声称为已核实发生日期。
|
||||
|
||||
同单重试仅在权限、参与人、单据可编辑状态、发票归属以及现有 linked_item_id 属于该单全部通过后复用现有绑定,不覆盖已编辑的类型/金额/说明;跨单占用仍拒绝。新动作先锁单再锁票(与提交的顺序保持一致),新创建、占用及整单金额重算在同一事务;旧绑定入口不默认切换。真实并发与死锁重试边界另做隔离验证,不以模拟 Prisma 测试证明锁正确。
|
||||
|
||||
真实隔离验证补充:绑定与旧解绑混合并发已复现 500;锁顺序合同扩至所有报销 bind/unbind。绑定按 body 的 reimbursementId 先锁单;解绑先读 linked_reimbursement_id 快照、锁单后锁票,取锁后关联与快照不一致则拒绝 409,不能去锁另一张单继续操作。旧绑定业务语义不改,仅统一取锁顺序;差旅分支不额外引入报销单锁。
|
||||
|
||||
批量分类接口:`POST /reimbursements/:id/items/classify`,请求 `{ itemIds, expenseTypeId }`,上限 100 条、ID 去重,先锁单并重验维护权限/状态,再确认所有 ID 属于该单和类型启用,事务内只更新所选明细类型与更新人。保留金额/说明/日期/批准依据/票据;部分无效整批拒绝。实查当前 assets 路径允许无批准依据保存/提交,现有测试对此有明确断言;本次不另造资产硬门禁,也不声称已有硬门禁,保留批准依据与实际提示规则。
|
||||
|
||||
- 已实现纯分类函数 `suggestInvoiceExpenseType(invoice, expenseTypes)`:仅从服务端持久化的已支持票种读取依据;铁路电子客票匹配启用的 `travel_train`,其余返回 null,不修改输入、不自动兜底其他类型。已接入新绑定意图与双端页面;真实隔离后端集成和指定样票腾讯 OCR 分层验证已完成,完整证据与未覆盖边界见 implement,不代表已可发布。
|
||||
- 当前持久化 OCR 不含票面项目,VAT 本身不足以区分住宿、办公或招待;不拿合成酒店名称替代真实分类证据。类型函数将作为新动作的服务端判定输入,不能接收客户端伪造票种。
|
||||
- 为满足“先生成明细、未知分类之后集中批量补、刷新保留”,候选最小模型变更是仅将 `ReimbursementItem.expense_type_id` 和其 Prisma relation 改为可空,既有分类不回填、不改写;仅草稿/退回可保存待补类型。金额和日期没有可靠值的票仍留待处理,不放宽这两列。
|
||||
- 未知类型必须在调用费用标准、预算、审批等服务前由提交门禁集中阻断;不得用可选链把它当作免票类型放行。详情与编辑 DTO、PC/移动端、材料导出和批量设类需同时支持待补态。空说明如用于草稿也必须有独立提交前校验,不能填虚构业务用途。
|
||||
- 单票创建明细与发票占用同事务;批量设类只处理用户指定明细并重验权限、状态、类型启用,保留 assets 批准依据字段,不新增依据必填门禁,不改金额、日期或发票关系。
|
||||
- 用户已明确批准本项字段调整的本地实施与隔离验证。新增迁移仅 DROP NOT NULL,不回填已有记录;生产迁移与部署仍单独授权。API 中 null 表示明确待补,PATCH 的 undefined 表示保留原值,不得用 `??` 混淆两者。
|
||||
- 回滚风险:一旦保存了空类型明细,旧应用对非空的依赖可能报错,不能只换旧镜像就宣称安全。发布方案须包含待补数据兼容策略;恢复 NOT NULL 前必须确认不存在空值,不得猜类型回填来完成回滚。
|
||||
|
||||
默认一事一单,保留后台明细费用类型,不要求员工逐条手选。选票自动生成可编辑明细;可靠类型自动填,未知类型集中提示并支持对选中的同类明细一次批量设置,不新增关联或确认步骤。既有明细入口只添票,不改写原明细。
|
||||
|
||||
批量操作仅作用于用户明确选中的明细,不暗中覆盖整单或其他手改项。后台重验参与人、可编辑状态、启用类型,保留 assets 批准依据字段与实际提示;提交重验剩余缺失信息。刷新或 OCR 回调不得覆盖手改结果,待处理信息不能仅保存在易丢失的前端状态。
|
||||
|
||||
不新增必填报销单类别或会计科目;“差旅/日常”等宽泛场景不能充当具体费用类型。餐饮用途等票面无法证明的信息不得自动断言,不填“其他”占位。
|
||||
|
||||
历史推进顺序曾撤回“先批准类型可空迁移”的建议;之后按已核定交互批准了上文最小可空方案的本地实施。下文两路径对比仅用于追溯,不再表示当前迁移方案未定,生产授权仍独立。
|
||||
|
||||
### 公开产品调研依据
|
||||
|
||||
- 分贝通官方费用报销介绍:https://www.fenbeitong.com/product/rembursement.php ,支持“一张报销单多类核销”“多笔费用一起报销”及“一事一报”。
|
||||
- 汇联易官方费用管理介绍:https://www.huilianyi.com/products/helios ,支持根据发票信息自动推荐费用类型。
|
||||
- 证据限于公开说明读取,不是登录实操或行业统计;不据此承诺本项目 OCR 能识别全部业务用途。
|
||||
|
||||
## 2026-09-09 后续设计:用户选票触发自动建明细
|
||||
|
||||
这是已确认产品取向的规划增量,替换早先手工关联默认方案;原 B1/B2 设计为已实施历史,不直接恢复其关闭门禁时的旧自动匹配逻辑。用户已批准同范围实施;生产配置、部署与新增迁移风险仍须单独确认。
|
||||
|
||||
### 实施前核查:不完整明细的实际约束(2026-09-09)
|
||||
|
||||
- 独立执行副本基于已发布 `b87cc55`,仅承接本 task 的 PRD/design/implement 增量,未混入付款、退回删除或工作流治理改动。
|
||||
- `server/prisma/schema.prisma` 的 `ReimbursementItem` 要求费用类型、发生日期、金额和说明非空;新增明细 API 还要求说明非空字符串。不能仅取消前端选择明细的禁用条件就实现选票建明细。
|
||||
- `server/src/services/ocr/tencent.js` 当前提取票号、日期、金额与购销方,不提取票面项目;`services/ocr/index.js` 持久化白名单也不包含票面项目。VAT 票种本身不能区分住宿、办公等业务分类;销售方或文件名不能充当可靠类型依据。旧缓存也不会因新增解析代码自动获得项目字段。
|
||||
- `routes/invoicePool.js` 旧自动绑定会复用唯一明细、按金额匹配、选择任意启用类型并以报销日期或当天兜底。新动作不能复用这些猜测。费用类型与说明缺失必须明确处理。
|
||||
- 两条可行路径:A 保持 schema,对无法可靠识别的票在选票面板内只补必要字段,再一次添加;识别完整的票不二次确认。B 支持先生成待补分类的明细,再在报销页编辑补齐,需要迁移及跨层可空兼容,符合“先生成再编辑”的体验但扩大技术范围。
|
||||
- 推荐 B 的体验方向,但不据此自行批准迁移。第一阶段可只允许费用类型待补;金额或日期没有可靠初值的票继续留在待处理集合,不扩成通用空值草稿引擎。具体迁移、说明初值和提交阻断合同需在范围确认后完善,不能将本建议当作已批准设计。
|
||||
- 扩展类型可空需一起检查明细新增/编辑/DTO、提交门禁、费用标准/预算/审批消费者、PC/移动端、导出与历史兼容。现有代码直接访问 `item.expense_type.requires_invoice`,不能只把数据库列改为可空。
|
||||
- 腾讯 OCR 项目解析如需补充,只保存受限业务必需字段,不保存原始响应,不自动重跑真实历史发票;真实样票调用仍须获准材料与用途。
|
||||
|
||||
历史决策点(已被最新裁决替代):曾请求先确认待补类型的草稿迁移,现已撤回该推进顺序,后续按最新分类交互核定实现。
|
||||
|
||||
### 交互与数据边界
|
||||
|
||||
- 复用发票池、上传/OCR 队列与报销明细模型。用户入口称“我的发票”,不强制迁移全部导航命名;提前存票与报销内上传进入同一份数据。退出选票面板不删除已入池发票,也不凭上传动作抢占为本单材料。
|
||||
- 面板区分整单创建明细和指定已有明细两种上下文,不用不同页面串跳。整单默认一票一明细;指定明细多选票只关联,不自动追加金额或改写用户已填内容。添加前显示本次生成条数或目标明细,点击一次即完成,不增加二次关联确认。
|
||||
- 新票识别成功默认勾选,但必须尊重用户取消选择;轮询/OCR 回调不得重新勾上已取消票或代点添加。池中不属于用户、已报销、被其他单占用的票不进入可添加集合;界面禁用不能替代提交时再校验。
|
||||
- 返回单据后明细与发票一起展示。用户只补识别缺失、分类不确定等必要字段;开票日期预填标明来源且可修改,不等于业务发生日期。OCR 金额缺失或无法确定可靠初值时,不写 0/今天/虚构类型使测试通过;明确保留待处理票或可表达的不完整草稿,用户就地补齐,不得伪装已成功生成完整明细。
|
||||
|
||||
### 服务端事务与兼容核查
|
||||
|
||||
- 优先评估复用/扩展现有绑定事务;新动作必须显式表达“由所选票创建明细”或“加入已有明细”,不能让普通绑定接口根据金额暗猜意图。实施前在本文件补接口形状、字段映射、OCR 金额口径及费用类型判定规则,规则之外不新增分类模型或 LLM 服务。
|
||||
- 权威 OCR 字段从服务端读取;客户端不能传伪造票面金额绕过票据校验。每票创建明细和占用关联在同一事务完成,使用既有锁/唯一约束防双击、超时重试及跨单竞态。批量按票返回成功/失败,单票不产生半成品孤立明细;只有失败项可重试,成功项保持幂等且不再次 OCR。
|
||||
- 如果现有 schema 的必填项不支持必要的待补状态,先说明候选实现/迁移影响再交审;不得隐式填占位业务数据。不为轻量流程搭建通用草稿引擎。
|
||||
- 历史未关联材料保留异常修复入口,不批量重建现有明细或关联;提交/删除与添加并发必须重新锁定校验单据状态。与 09-04 删除阶段共用一致的发票释放合同,不能让已删除单据通过旧链接恢复占用。
|
||||
- 既有 feature flag 的金额/缺票/权限门禁保持;新建明细来自用户明确选票动作,不通过关 gate 获得功能。OCR off 的历史兼容实现不是本次交付成功标准,生产配置修复单独授权。
|
||||
|
||||
### 必须验证
|
||||
|
||||
真实后端 E2E 以空单为起点:上传→OCR→默认勾选/取消→添加→自动明细→刷新→补必要项→提交;另跑提前存票后选择、多选相同金额但不同支出、已有明细多票、部分失败、关闭面板/超时重试、双击与跨单并发、金额人工调整、缺字段/错误类型、OCR 不可用及非参与人拒绝。没有额外关联步骤,正常路径不展示关联标签;错误必须定位到票或明细。
|
||||
|
||||
真实腾讯 OCR 单独保留获准样票、provider/调用/结果的脱敏证据,不能用 mock 或空的 OCR 配置替代。桌面/窄屏浏览器覆盖原件预览、键盘/焦点返回及不丢草稿;现有 frontend-design skill 缺失时仅沿用项目面板和组件样式。跨任务验证/发布边界仍见付款任务同日综合方案,执行端负责,不交给员工补测。
|
||||
|
||||
## 边界
|
||||
|
||||
Task B-1 合并交付费用主数据政策和提交门禁代码,但运行时分成两级:B1 数据模型/主数据/表单字段随部署生效,B2 强关联和金额阻断由默认关闭的双闸 feature flag 控制。B3 财务一页式另建任务,等待 B2 在真实或专用数据上验证后开始。
|
||||
|
||||
## 数据模型
|
||||
|
||||
- `ExpenseType.requires_invoice Boolean @default(true)`:用 migration 为既有类型回填 true,再定向把两项补助设为 false。
|
||||
- `ReimbursementItem.asset_prior_approval_basis String?`:仅 assets 明细要求,保存批准人、批准时间和简短依据;非 assets 明细应保持 null 或忽略空白值。
|
||||
- 不为 `account_code_default` 新增字段;财务提供映射前保持 null。重复执行 seed/init 的 update 分支不覆盖已有科目映射,避免后续部署抹掉财务回填。
|
||||
- 费用类型停用只改 `enabled`,不删除记录、不迁移历史明细外键。
|
||||
|
||||
## 费用类型迁移
|
||||
|
||||
| 当前类型 | 动作 | 新提交口径 | 历史口径 |
|
||||
| --- | --- | --- | --- |
|
||||
| `business_client_meal` 客户招待 | 原 code 改名 | 业务招待费 | 追溯显示新名称 |
|
||||
| `business_training` 培训会议 | 停用旧类型并新建两个 code | 会议费、培训费 | 保持培训会议 |
|
||||
| `assets_it_hardware` IT 硬件 | 停用旧类型并新建 code | 低值配件选办公用品;需登记的选择办公电子设备 | 保持 IT 硬件 |
|
||||
|
||||
新 code 在实施前按现有命名规则固定并写入 migration/seed 测试,不复用旧 code 承载不同语义。
|
||||
|
||||
## 写入和提交数据流
|
||||
|
||||
1. 员工建立或编辑明细,服务端读取权威 ExpenseType。
|
||||
2. assets 类型必须校验 `asset_prior_approval_basis`;前端提示只是辅助,服务端是最终门禁。
|
||||
3. 办公用品明细金额达到 `2,000 元`时生成非阻断类型提示,员工端和财务审核视图消费同一服务端事实;该阈值不作为会计资产确认结论。
|
||||
4. 发票上传/绑定显式携带目标明细 ID。B2 flag 开启时禁止无法唯一匹配后的自动建明细。
|
||||
5. 提交事务重新读取明细、有效发票及关联关系,按明细计算票面合计。
|
||||
6. `requires_invoice=false` 跳过票面比较;true 类型执行缺票和超票面阻断。
|
||||
7. flag 关闭只选择旧提交/匹配路径,关联表中的既有数据不做逆向写入。
|
||||
|
||||
## Feature Flag
|
||||
|
||||
- 新增独立 key,不复用 `budget_precheck_v1` 或 `project_linking_v1`。
|
||||
- 配置层新增 env hard gate,默认 false;服务白名单注册 key;seed/受控配置提供 DB `force_off` 初值。
|
||||
- 后续开启先使用专用账号或部门 allowlist(如现场组织结构允许),再全局开启。
|
||||
- flag 状态必须进入诊断/审计输出,但不向普通员工显示内部 key。
|
||||
|
||||
## 兼容和回滚
|
||||
|
||||
- schema 只做向前兼容的 nullable/default 字段新增,不在应用回滚时逆迁移。
|
||||
- 旧草稿保留;B2 开启后提交时补齐,不批量伪造关联或批准依据。
|
||||
- 应用异常时先关闭 B2 flag;这不会删除既有关系。B1 名称/字段回滚只通过新 migration 或后续主数据动作,不直接删列。
|
||||
- OCR 仍可给出候选匹配,但 B2 开启时最终绑定必须由用户确认,不能静默创建业务明细。
|
||||
|
||||
## 延后项
|
||||
|
||||
- PurchaseRequest 强关联、资产自动建卡、数量/单价、B3 一页式审批和财务科目表治理不混入本轮核心门禁。
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
# 实施清单:报销费用口径与票据明细门禁
|
||||
|
||||
## 2026-10-11 余项移交结果(当前接续点)
|
||||
|
||||
已将报销真实 OCR 与财务科目映射移到 [09-08 接收合同](../../../09-08-reimbursement-payment-closeout/prd.md#2026-10-11-接收余项报销真实-ocr-验收与财务科目映射),执行接续见接收任务 implement 顶部。保留指定样票 `SAMPLE-PDF-INVOICE-FIRST` 与 `artifacts/qa-flow/20260909-release-e5685f1/` 原证据;2026-09-10 的 `PASS_DRAFT_ONLY` 仍有效于原合成草稿阶段,不扩为提交/审批或逐字段精度通过。科目映射未回填,财务负责人责任和原目标日期继续记录在 RC-ACCOUNT。
|
||||
|
||||
本轮只移交文档,未调用腾讯、未写生产、未重跑业务或补勾历史清单;原独立复核余额仍在接收任务开放。父任务及部署子任务归档待明确授权,当前未归档、未提交。
|
||||
|
||||
## 当前接续点:已部署,公网草稿闭环验证完成
|
||||
|
||||
### 2026-09-10 公网草稿最终 PASS(本节替代前次样票阻塞状态)
|
||||
|
||||
- 用户再次批准继续验证。未修改产品代码、权限、审批定义或生产配置,运行版本仍 e5685f1;本轮仅维护受控验收脚本和记录,无再次部署。
|
||||
- 生成新版全合成 PDF:采用表格化票面布局,明确标注不可报销/支付,无真实交易/账号/票据内容。每轮独立合成票号,不修改真实样票、不绕查重。先走部署环境已有 provider 检查,再经线上 UI 上传走真实队列及腾讯;最终样本摘要与 probe manifest、生产持久化发票逐一一致。
|
||||
- 最终命令:`node artifacts/qa-flow/20260909-release-e5685f1/probe-synthetic.cjs && node artifacts/qa-flow/20260909-release-e5685f1/verify-live-draft.cjs`,exit=0,证据 live-draft-result-v6.json=PASS_DRAFT_ONLY。专用 employee 真实浏览器登录1次 HTTP200,上传、OCR available、用户点击添加前零明细、一票一明细、批量分类、编辑说明、桌面刷新持久化均通过;同单转手机路径修改说明、保存刷新通过;真实 precheck POST 正常返回。submitRequests=0、blockedWrites=0,不是公网审批验收。
|
||||
- 只读最终回验:6 张本阶段合成票均 archived/manual 且无单据/明细/差旅绑定,标记测试草稿0,测试时间窗口内活动会话0。最后样票的哈希、合计1.00、税额0.00、日期2026-09-10、provider=tencent 均匹配;原始合成文件及OCR审计资料按正常保留机制留存,不能宣称物理删除。证据 live-draft-final-readback.json。
|
||||
- 中间失败保留而不抹掉:v2 桌面通过但拦截10次 precheck;核实该 POST 是只读预检查后放行,仅禁止提交等范围外写入。v3 分类后马上编辑的持久化断言失败,测试改为等待已分类视图并回读类型后再编辑,后续通过;这不证明所有快速点击竞态均无风险。v4/v5 手机说明定位超时;本地最小 HTML 复现 `getByLabel('说明', exact=true)` 对已有内容的 textarea 命中0,按 label 的 span + textarea 定位命中1,v6 补验通过,无产品修补。
|
||||
- 清理工具根因补验:Fastify 合成 DELETE 带 application/json 却没有 body,稳定返回400/FST_ERR_CTP_EMPTY_JSON_BODY;不发送多余 Content-Type 返回200。修正后各轮均经业务API删除草稿及归档合成票,不再直接写库。登录必须带设备指纹;logout 必须使用浏览器Cookie上下文,不把无Cookie返回200写成会话已撤销;最终数据库只读计数提供撤销证据。
|
||||
- 现已补上“公网真实腾讯→队列→选票→可编辑草稿”的同环境证据,不再沿用先前只能分层推断的缺口。边界仍是公网不提交、不审批、不付款;提交及三段审批沿既有隔离环境证据,不能扩成真实用户/全票种/整站全面验收。
|
||||
- 主线本阶段完成;下一阶段按已有顺序进入 09-04 退回删除修复,再做 09-08 付款文件名/下载理由/进度布局,复用原任务、不叠改当前产品。部署/验收记录与新增经验尚未提交,不自动归档整个历史任务。
|
||||
|
||||
### 2026-09-10 公网止于草稿的实测结果
|
||||
|
||||
- 用户批准公网只验上传/OCR/选票/分类编辑/保存刷新,不提交审批。定位到既有仓库外 live-smoke.env(0600),仅在进程内读取 employee 凭据;此前“常规 env 无配置”不等于专用凭据不存在。生产只读前置确认唯一 active、未锁定、无需改密且角色精确 employee 的 smoke 账号,未改账号或生产配置。
|
||||
- 执行 `node artifacts/qa-flow/20260909-release-e5685f1/verify-live-draft.cjs`:真实浏览器登录一次 HTTP 200,创建标记草稿,上传一份 Chromium 生成的中文 PDF,显式标注“合成测试样张、不可报销、无支付效力”。未使用用户真实票据、未改 provider 或注入 OCR 结果。上传创建成功,轮询终态 ocr_failed,结果 STOPPED/SYNTHETIC_OCR_NOT_AVAILABLE。
|
||||
- 这证明登录、草稿创建、上传及识别失败终态路径;不能证明腾讯能识别该合成版式,不据此认定生产 OCR 配置坏了,也不记自动建明细/分类/编辑刷新通过。submitRequests=0;浏览器安全拦截了 6 个未列入本次写入白名单的请求,具体类型未记录,不谎称全部请求均正常。未产生审批任务或付款。
|
||||
- 验收脚本自动清理未成功,后续仅清理用途的 API 登录也未通过;已核实该清理请求缺少生产要求的 x-device-fingerprint,不能记为账号密码失败,不继续重试登录。原脚本 loggedOut=true 仅表示无 Cookie logout 返回成功,不证明会话撤销,该结论撤回。
|
||||
- 随后只读确认恰有本次前缀标记草稿 1 张(draft、明细0)与合成文件名票据1张(ocr_failed、未绑定)。按已获准清理范围执行专用 Serializable 事务:锁单/票后重验唯一数量、smoke精确角色、owner=submitter、标记、状态、无明细及无绑定;只删除该草稿、归档该失败合成票、撤销按该次建单/上传时间窗口唯一定位的测试会话。任一条件不符全事务拒绝。
|
||||
- 最终 SCOPED_CLEANUP_PASS:草稿删除回读0、合成票归档、测试会话撤销。合成 PDF 与 OCR/审计记录按既有保留机制留存,不声称物理文件已删除。未清理其他账号、单据、发票或会话;未做真实业务写入。
|
||||
- 证据在既有发布目录:live-draft-result.json 保留原失败,live-draft-cleanup.json 保留清理API失败,live-draft-cleanup-final.json 记录最终定点清理。仅测试工具与本记录变化,无产品改动、未再部署。
|
||||
- 后续缺口:需要可被腾讯识别且不会占用真实报销票号的专用测试样票,或另行批准真实样票在确认无业务占用/查重风险后的受控用途;不能以随意排版的合成 PDF 冒充已验证发票样本。先修正测试器认证/会话清理与失败诊断,再安排下一次受控验证,不交给用户手工测试,不反复消耗账号登录。
|
||||
|
||||
### 公网合成业务验收前置(用户已授权,停在写入前)
|
||||
|
||||
- 用户已明确授权专用测试账号的合成报销上传/OCR/选票/分类编辑/提交审批及测试数据清理;不包含真实报销、真实付款、修改真人账号或生产审批路由。
|
||||
- 本轮只读查询运行容器的账号状态及当前报销工作流,事务/连接强制 read only,仅回传测试别名、角色与状态。四个既有 smoke 候选:两个 active employee+manager(其中一个 must_change_password=true),一个 active employee,一个 archived employee+finance;启用 smoke 角色集合只有 employee、manager,没有 ceo。不得将已归档财务视为可直接登录,也不以管理员替代财务验收。
|
||||
- 当前活动报销工作流 assignee_rule 为 role=ceo,不是直接经理步骤。即使测试员工有测试直接经理,也不能据此保证提交不会派给真人;目前缺少仅限本次测试的审批隔离路径。不能为了跑通而给测试账号增加 ceo(会影响全局角色路由)、修改活动工作流或擅自启用财务。
|
||||
- 当前执行进程、常规本地 env 与生产容器未配置 LIVE_SMOKE 凭据键。这不证明仓库外凭据不存在,也未尝试密码、重置账号或复制生产身份。获准的此前真实样票只做过 provider 检查,不能直接占用为合成报销材料或绕过真实票据查重。
|
||||
- 本次 loginAttempts=0、businessMutationCount=0、workflowSubmitCount=0,无新建单据/票据/会话需清理;没有尝试完整公网闭环,不能记 PASS。已部署健康检查结论不受此影响。
|
||||
- 建议调整验收边界:公网用获准专用凭据及腾讯可识别的测试样票验到可编辑草稿,不提交进入真人待办;提交审批继续沿已通过的隔离数据库/真实认证/队列/浏览器三段审批证据。若要求公网全链独立通过,先另审仅测试数据的审批隔离方案和财务启停窗口,不在本轮临时改生产规则。
|
||||
|
||||
### e5685f1 已部署与后置检查补齐(2026-09-09)
|
||||
|
||||
- 生产实际运行 `e5685f18b233901cffc9a12c9c277cc5c642725c`。原 deploy-existing 调用最终 exit=255,日志仅回传到 build-init,不能改写为原脚本 exit=0;255 原因尚未定位,不凭退出码断言产品失败或发布完全没发生。
|
||||
- 中断后只读核实已完成构建、切换与迁移,未盲目重发整次部署或回滚。运行源码摘要与候选一致;init 日志顺序为 validate_config → migrate → check_required_configuration,INIT_PASS/existing,无 seed 阶段。
|
||||
- 单独补跑生产身份/迁移后置预检 PASS,pending=none;数据库只读校验费用类型列 nullable,金额/日期仍 NOT NULL,本迁移成功记录一条、当时 null 分类明细为 0。该计数会随用户操作变化,不构成以后随时切旧镜像安全的保证。
|
||||
- 单独执行既有 external-deps smoke 与 runtime smoke 均 exit=0;公网 ready/home/主脚本 HTTP 200,buildSha=e5685f18b233;既有公网 bundle-copy 扫描 106 项、focused=2、buildMarker PASS、findings=0。
|
||||
- 受控身份 probe/refresh 均 PASS,最终回读 backupContractBuildMatches=true。本次属于应用已部署后补齐后置检查,不把原 exit=255 回执覆盖成成功。未新增产品修复、未执行数据库恢复。
|
||||
- 发布前正式备份完成于 15:33:41 UTC,属于 b87cc55,manifest/数据库/附件大小与摘要匹配;旧 b87cc55 镜像保存为独立文件,14 层摘要通过,未演练 load/恢复。刷新备份合同不等于已产出 e5685f1 新版本备份;旧备份及回滚制品保留。发布后可用空间 43.55 GiB,无追加清理。
|
||||
- 公网 Chromium 打开站点后进入登录页,登录内容可见;未登录业务账号、未创建真实或合成报销。两条控制台资源错误均为 favicon.ico 404,本任务未改图标,记录为非阻断观察,不擅自扩修。首次 CLI 默认 Chrome 缺失,改用已安装 Chromium 后通过;首次 /ready 检查误读顶层 buildSha,按既有 data 包装修正检查后通过,均非产品修复。
|
||||
- 证据入口仍为 `artifacts/qa-flow/20260909-release-e5685f1`:原 deployment-result.json、备份与独立制品回执、recovery-smoke.json、post-preflight-result.json、final-postcheck-result.json、public-check.json、浏览器快照。原日志受控留存,不输出原始环境或票据。
|
||||
- 当前结论:已部署、发布健康及结构检查完成;不是公网带账号的报销完整业务验收。下一步由执行端取得专用测试账号及线上合成单据写入/清理的明确授权后,验证上传→识别→选票→分类编辑→提交审批,不让真实员工代测,不以此授权真实付款。退回删除、付款体验两个 task 仍未实施,整个 task 保持 in_progress。
|
||||
|
||||
### e5685f1 发布执行计划(2026-09-09,用户明确授权部署)
|
||||
|
||||
- 固定已推送候选 `e5685f18b233901cffc9a12c9c277cc5c642725c`;使用干净发布 clone,不将本记录或其他未提交内容同步生产。证据目录 `artifacts/qa-flow/20260909-release-e5685f1`。
|
||||
- 只读前置已通过:运行版本 b87cc55、最低配置 39/3/1;待迁移仅 `20260909210000_reimbursement_draft_expense_type`;磁盘可用 45.52 GiB。预检 rollbackWindow=OPEN 仅是 terminated 状态统计,不是数据兼容或恢复演练结论。
|
||||
- 发布前保存当前 b87cc55 独立镜像并核归档层摘要,触发既有正式备份服务,再核同运行版本的 manifest/数据库/附件完整性。失败停止,不绕权限、不清理旧制品。
|
||||
- 沿 deploy-existing.sh:existing、SYNC_ENV=0,预检待迁移显式匹配,执行本次 DROP NOT NULL,不运行 seed、不重建数据库容器;随后核迁移无 pending、版本、ready、依赖/runtime smoke、公网资源及合同 refresh。
|
||||
- 此次仍无完整候选门禁通过证据,附件/应用恢复未演练;分层腾讯与本地流程证据不等于公网完整报销验收。用户已授权准备并部署,不记录为完整门禁通过。
|
||||
- 回退目标为 b87cc55,不是此前 f223bfb。迁移后若未生成 null 分类,旧应用兼容仍需核验;若存在 null,不自动切旧镜像、不逆迁移、不猜分类回填。失败先确定应用切换和数据库阶段,保全证据并报告;数据库恢复及可能丢失恢复点后数据不自动执行。
|
||||
- 上线自动化由执行端承担;不处理真实报销、不真实付款。本次未额外授权线上合成单据写入,先做既有发布健康/smoke与只读版本/结构检查,不能以此冒称已走通公网报销全链路。
|
||||
|
||||
### 用户复核与提交授权(2026-09-09)
|
||||
|
||||
- 用户回复“通过”,接受本次复核材料;这是用户复核结论,不改写为执行端独立 reviewer 已运行。
|
||||
- 用户随后明确授权仅本 task 的 28 个文件提交并推送,冻结新候选;未授权部署、生产迁移或线上写入验证。
|
||||
- 提交前 fetch 核对远端 main 仍与执行副本基线 `b87cc55` 一致。本次以独立副本的提交快进推送 main,不切换或覆盖主工作树的其他未提交任务;不强推、不新建分支。
|
||||
- 下文“未提交/待独立复核”为此前阶段记录。提交与推送结果以 Git 对象及远端回读为准;发布仍待后续明确批准,task 保持 in_progress。
|
||||
|
||||
### 2026-09-09 最终收口与复核入口
|
||||
|
||||
- 执行副本:`artifacts/isolated-worktrees/20260909-invoice-first`,基线 `b87cc55`,官方 current 指向本 task;基线不是包含本次修复的候选 SHA。当前产品增量未提交,主工作树其他任务不得整体合入。
|
||||
- 范围为 28 个文件(21 个 tracked 修改、7 个新增文件)。复核必须同时读取 `git diff HEAD` 和 `git ls-files --others --exclude-standard` 列出的新文件,尤其真实 HTTP 测试脚本、迁移、分类服务与共享组件;只看 diff stat 会漏新增文件。此记录是复核入口,不是已冻结的哈希清单。
|
||||
- 本轮核对服务端事务、提交拦截、双端选票/批量分类和最新合同,纠正 PRD/design 的历史状态冲突:assets 无现行硬门禁,不能声称保留了不存在的校验;可空迁移已批准本地实施,不再列为方案未定。产品代码未因这次文档收口变化,沿用下方同输入的分层验证结果,不重复累加测试数量。
|
||||
- 本地验证已覆盖:可空迁移及反向约束、同单幂等/跨单竞争/失败回滚/绑定解绑锁顺序、真实认证与队列、桌面/手机上传到审批完成。provider mock 的端到端与真实腾讯单次样票验证分开记录;付款、企业微信 OAuth、真实腾讯与候选队列浏览器同一次整链仍未覆盖。
|
||||
- 待独立只读复核:权限与参与人、草稿/退回状态、提交前分类和说明阻断、null 与省略的区别、所有报销 bind/unbind 先单后票、批量分类原子范围、OCR 取消/目标选择不被回调覆盖、隔离测试不接触共享数据库及退出清理。当前 inline 未有独立 reviewer,不记独立 PASS。
|
||||
- 发布前置仍需:复核后的明确文件集合提交并冻结新 SHA;重新核生产运行版本/变化、migration 状态、证据时效、备份和独立回滚制品。备份完整性不等于恢复能力;本次一旦产生空分类,旧镜像可能不兼容,不能承诺仅镜像回退。若需恢复 NOT NULL,先确认 null 为零,禁止猜分类回填。
|
||||
- 下一动作由执行端提供本入口供只读复核并处理 findings;复核后按明确授权提交/推送,再提出发布方案。生产迁移、部署及线上合成写入验证另批。上线验证仍由执行端承担,不把真实员工试错当验收。
|
||||
- 退回删除与付款中文文件名/下载理由/进度布局继续留在原 task,本阶段未实施,不归档本 task,也不叠加第三笔工作。
|
||||
|
||||
### 2026-09-09 真实认证与 pg-boss 补验(最新)
|
||||
|
||||
- 已移除业务请求的 x-synthetic-actor 身份注入,改为真实 auth routes、auth plugin、密码哈希、登录接口发 JWT、Cookie 会话和浏览器启动时的 refresh;账号和密码只在隔离环境合成。员工/经理/CEO/财务各自登录,业务权限从真实 employee_roles 读取,不在测试中直接授予审批权限。
|
||||
- 无令牌/伪造令牌访问被 401 拒绝,token_version 增加后旧令牌也被 401 拒绝。浏览器测试通过真实登录接口获取 Cookie,未伪造 has_session 或 accessToken;个人信息/导航/通知展示壳仍是测试适配,企业微信 OAuth 和登录表单交互不据此声称已测。
|
||||
- `RUN_ISOLATED_DB_TEST=1 RUN_BROWSER_TEST=1 node scripts/qa/test-invoice-first-http.cjs`:真实认证+手动 worker 模式 14 组 PASS。
|
||||
- `RUN_ISOLATED_DB_TEST=1 RUN_BROWSER_TEST=1 RUN_JOB_QUEUE=1 node scripts/qa/test-invoice-first-http.cjs`:真实认证+真实 pg-boss 模式 14 组 PASS;真实 startPgBossJobs 注册实际 OCR 队列和消费者,上传路由自动 enqueue,未手动调用 worker;独立查询队列中对应 invoiceId 的 ocr.invoice job 已 completed,且发票 available。桌面/手机均完成添加、分类、保存刷新、提交、同单三段审批及只读回读。
|
||||
- 队列测试只连接一次性库 synthetic_jobs schema;非本次关注的周期任务设置年度时间避免在测试期间执行,验的是 OCR 即时投递/消费,不宣称生产 cron 时序已验。provider 仍为 mock,不复制腾讯凭据、不使用真实票据;先前指定样票的真实腾讯 PASS 保留为独立证据。
|
||||
- 两种模式均已清理 pg-boss、连接池、HTTP/preview、浏览器、临时上传目录和数据库容器。JS 语法、git diff --check、Trellis 治理 PASS;本轮只扩测试,无新增产品修复,未提交、推送或部署。
|
||||
- 本地实现与分层验证可进入只读复核准备;独立 reviewer 仍未执行(当前 inline 模式不派发 implement/check 子代理),不把主会话验证记作独立通过。发布候选需包含本次可空分类 migration 及回滚约束,继续另批,不复用旧部署授权。
|
||||
|
||||
### 2026-09-09 桌面/手机串联验证接续(最新)
|
||||
|
||||
- 在既有隔离脚本增加 `RUN_BROWSER_TEST=1`:使用当前候选的 client/dist、Vite localhost preview 和 Chromium;业务 API 透传至真实 Fastify/Prisma/一次性 PostgreSQL,未拦截伪造报销详情、上传、状态轮询、分类、保存、提交或审批响应。
|
||||
- 认证/个人信息/导航/通知壳使用合成适配器,权限来自实际 ROLE_PERMISSIONS。OCR worker 使用真实 runOcrForInvoiceId、存储读取、领取锁、结果落库与 available 状态转换,但 provider=mock 且手动触发 worker;不能据此宣称腾讯网络、pg-boss/cron 调度或真实登录的同环境整链通过。
|
||||
- 合成空白 PDF 由脚本生成,桌面/手机分别新上传不同哈希,轮询识别完成默认勾选,添加前真实数据库明细数为 0;一次添加后批量分类、编辑说明、等待 PATCH 200、查库并刷新核对,再从页面提交生成经理待办。双端到待办阶段已通过。
|
||||
- 测试开发失败均如实保留原因:Vite CJS 导入形状、未限定明细区域的“编辑”/隐藏文本定位、手机按钮带数量、保存未等响应即刷新;未因这些测试适配问题修改产品合同或放宽断言。现在同时验保存响应、真实数据库值与可见明细内容。
|
||||
- 同一张浏览器提交单的经理/CEO/财务三段 HTTP 审批及审批完成后双端只读回读最终通过;真实库状态 approved、页面不再出现添加发票入口。复现命令(先确保 client/dist 来自当前候选构建):`RUN_ISOLATED_DB_TEST=1 RUN_BROWSER_TEST=1 node scripts/qa/test-invoice-first-http.cjs`。最终 12 组检查 PASS;测试数据库容器、合成文件目录、浏览器、preview/HTTP 服务和连接池均已清理。JS 语法、Trellis 治理及 diff 检查 PASS。保留 Node punycode/Vite CJS 弃用提示,不扩依赖升级。
|
||||
- 此前真实腾讯样票 PASS 仍是独立证据,不将其与本节合成 provider 串联结果拼成一次真实云端整链。未重用真实票据、未连接生产、未提交或部署。独立复核和正式调度/认证边界仍待收口。
|
||||
|
||||
### 2026-09-09 用户指定样票的真实腾讯 OCR 验证(最新)
|
||||
|
||||
- 用户指定部署服务器 OCR 环境及一份 PDF 样票,授权本次受控识别;材料仅以 `SAMPLE-PDF-INVOICE-FIRST` 记录,不保存原文件名、票面内容、账号或完整路径。
|
||||
- 先做只读前置核验:运行 build `b87cc55aca0003130cf8a5bbe38d0dd15528389c`;OCR_PROVIDER=tencent、腾讯凭据存在。远端既有 `check-ocr-provider-samples.mjs`、OCR index/tencent 三文件摘要与执行副本逐一一致,识别相关代码本 task 无差异。
|
||||
- 指定 PDF 经 SSH/stdin 传入现有容器中的 `check-ocr-provider-samples.mjs --alias=SAMPLE-PDF-INVOICE-FIRST`,使用现有环境凭据,调用一次腾讯接口;不把凭据复制到本地、不生成服务器样票副本、不调用业务上传/写库/队列接口、不改配置、不部署。
|
||||
- 结果 `PASS`,`transportMode=pdf_native`,`elapsedMs=826`,`documentCategory=full_vat_invoice`,`documentSubtype=VatElectronicInvoiceFull`,`documentCount=1`。号码、日期、金额、税额、合计、销售方、购买方字段均命中;invoiceCode 未命中但不是全电发票必需字段;missingRequiredFields 为空、errorCode=null。
|
||||
- 此证据证明指定样票通过真实腾讯调用、归一化与必填字段检查;没有人工逐字核对识别精度,也不证明所有票种或实际费用分类正确。该票种不提供可靠费用类型推断,仍按既定规则集中补分类,不从商户名称猜用途。
|
||||
- 真实 OCR 专项前置缺口已关闭;下节“待指定环境/样票”是历史状态。剩余是候选版本上传→队列识别→浏览器选票→提交审批的同环境串联验收,以及独立只读复核,不能将分层 PASS 相加成整链通过。未提交、推送、部署、写生产业务数据或真实付款。
|
||||
|
||||
### 2026-09-09 上传与审批链补验(最新)
|
||||
|
||||
- `RUN_ISOLATED_DB_TEST=1 node scripts/qa/test-invoice-first-http.cjs` 最终 10 组检查通过,真实 PostgreSQL/Prisma/HTTP;本轮只扩测试,未改审批产品代码。
|
||||
- 真实 multipart 上传合成空白 PDF:文件落入专用临时目录,发票保存 pending_ocr 且未绑定;再次上传同文件复用原记录,不新增票。此用例只验证上传/落盘/待识别状态,不启动 OCR worker,也没有把空白 PDF 当作能识别出票面字段的样票。
|
||||
- 使用真实角色权限常量、合成经理/CEO/财务账号与三级规则:提交人自审 403;return 回 draft 后可修改重提;reject 回 rejected 后可修改重提;重提创建新实例;经理→CEO→财务逐步审批最终 approved,重复审批 409,无遗留 pending 任务,不标为 paid。
|
||||
- 测试设计初次把 return 当作 rejected、把 CEO/财务角色待办当作个人待办,产生断言失败;核对既有实现后按 draft/角色分派分别断言,保留 reject 独立测试,未为通过测试修改生产状态或审批合同。
|
||||
- 所有数据库容器、HTTP 服务、连接池及合成上传目录已清理。认证仍是合成测试适配器,真实登录、腾讯识别、队列消费与真实付款不在这次通过结论内。
|
||||
- 真实 OCR 前置:当前进程没有注入腾讯凭据;仅检查主工作树常规本地 env 的键是否存在,结果未配置,不读取回传密钥值,不据此推断生产关闭 OCR。专用执行副本没有已确认获准样票;现有 `check-ocr-provider-samples.mjs` 空输入演练返回 `HOLD/sample_empty`,不是实际腾讯调用。下一步需指定获准 OCR 测试环境及脱敏 PDF 样票别名,不从生产复制凭据或擅自发送真实票据。
|
||||
- 仍未提交、推送、部署;独立只读复核待完成,旧“仅到派单”是上一阶段证据,已被本节三级审批补验扩展,但不等于上传→腾讯 OCR→浏览器→审批的完整集成验收。
|
||||
|
||||
### 2026-09-09 真实隔离数据库 / HTTP 验证
|
||||
|
||||
- 使用 `RUN_ISOLATED_DB_TEST=1 node scripts/qa/test-invoice-first-http.cjs`:启动一次性 PostgreSQL 16(仅随机 localhost 端口、tmpfs),执行历史 SQL 与本次迁移,使用独立 Prisma 客户端和真实 Fastify HTTP;不读取共享 DATABASE_URL,不连接生产。结束关闭 HTTP/连接池并删除容器。
|
||||
- 已验证空单从已识别合成票添加、未知类型持久化、类型/说明缺失提交阻断、HTTP 批量分类与编辑、正式 submit 路由创建 reviewing 状态和经理待办;重复提交不重复生成实例。认证是合成测试适配器,不冒充真实登录验证;OCR 输入是合成持久化字段,不是腾讯调用证据。
|
||||
- 同单 4 请求并发只生成 1 条明细,重试不覆盖人工修改;跨单抢票只有一方成功且无孤立明细;数据库触发器注入绑定更新失败后,明细/占用/总额完整回滚,解除故障重试成功。
|
||||
- 实测发现:外部锁屏障下,createItem 重试先锁单后锁票,旧 unbind 先锁票后锁单,两者并发返回 `[200,500]`,不能用顺序模拟测试证明混合路径安全。修复为所有报销绑定先锁单;解绑先读关联快照、锁单再锁票,取得票锁后关联变化则 409,不操作新关联。修复后的真实 HTTP 并发为 `[200,200]`。
|
||||
- 扩展路由/服务回归 18 文件 349/349 通过(随后新增一条解绑快照变化反例,单文件另验);JS 语法、错误码 strict 合同与 diff 检查同步执行。初次集成测试漏配合成账号的 expense.edit_own 导致断言失败,修正测试权限后通过,未放宽产品权限。
|
||||
- 当前只证明到提交/派单,不证明经理实际审批、付款、真实 OCR 或上传到 OCR 队列全链路完成。独立复核仍待完成,当前 inline 主会话无独立 reviewer。未提交、推送、部署或真实数据写入。
|
||||
|
||||
最终补测:真实 DB/HTTP 脚本 8 组检查全部通过(含同金额票不误合并、已有明细加票不改金额、混入异单 ID 的分类整批回滚);解绑快照变化反例加入后 `cd server && npm test -- tests/unit/routes/invoicePool.travel-bind.test.js` 20/20 通过。此前扩展 349 用例与这 20 条有重叠,不累加成独立总数。所有自建数据库容器、HTTP 服务和连接池已关闭;没有真实业务数据参与。
|
||||
|
||||
### 2026-09-09 双端面板接续
|
||||
|
||||
- [x] 桌面/手机“我的发票”上传和 OCR 完成只默认勾选;点击添加才提交,整单传 createItem,已有明细入口传 linkedItemId,目标可修改。
|
||||
- [x] 返回报销单不占用、不删除池票;关闭清理本次选择。已成功添加从选择中移除,部分失败保留失败项重试。
|
||||
- [x] 双端接入共享批量分类组件:只更新用户选中的明细,未知类型集中提示,已分类项可编辑,刷新详情与移动列表缓存。
|
||||
- [x] 补桌面添加成功后的目标清理,避免从明细入口添加后,下次整单入口仍指向旧明细。
|
||||
- [x] 真实 Prisma + HTTP + 隔离 PostgreSQL 验证同单幂等、跨单竞争、事务失败回滚与空单提交/派单;真实 OCR/上传和完整审批仍按上节留缺口。
|
||||
- [ ] 获准合成样票的真实腾讯 OCR 验证;不发送真实业务票据,不以 mock 证明 OCR 可用。
|
||||
- [ ] 独立只读复核(当前 inline 执行未由独立 reviewer 验证);未提交、未推送、未部署,不交真实员工补测。
|
||||
|
||||
本节为最新状态,下文“尚未接入页面”等表述保留为历史阶段记录,不代表当前实施状态。退回删除和付款体验仍未启动,继续保持任务隔离。
|
||||
|
||||
本轮浏览器与治理证据(均在 invoice-first 执行副本):
|
||||
|
||||
- 最终页面输入 `cd client && npm run typecheck && npm run build` 通过,2422 模块构建完成;保留 Browserslist 数据老旧提示,不扩依赖升级。自建本地 Vite 验证服务已停止。
|
||||
- `E2E_BASE_URL=http://127.0.0.1:5187 npx playwright test tests/e2e/r111-invoice-reimbursement-guard.spec.ts --workers=1 --reporter=line --timeout=90000`:12/12 通过。
|
||||
- 同命令追加 `--grep '已有明细入口|多票添加'`:4/4 通过,包含补充的多票部分失败仅重试失败票,以及成功后重开整单不残留旧目标。
|
||||
- 同命令追加 `--grep '选票优先空单'`:2/2 通过,补充分类响应后的界面更新、刷新保留断言。三次有用例重叠,不计作 18 个独立业务场景。
|
||||
- 使用真实浏览器和合成 API 拦截,覆盖桌面、手机与企业微信布局、空单上传不绑定、退出留池、一次选票添加、已有明细目标、刷新与局部重试。未覆盖真实腾讯 OCR、真实 Prisma 事务并发或完整审批链。
|
||||
- `node scripts/qa/check-trellis-governance.mjs`、`node scripts/qa/check-error-reason-contract.mjs --strict`、`git diff --check` 通过;`node scripts/qa/audit-docs.mjs --check-registry` errors=0,保留既有 22 个未注册文件 warning。
|
||||
|
||||
### 接续阶段:选票创建与批量设类服务端
|
||||
|
||||
- [x] 明确 `createItem: true` 的新绑定意图,和 linkedItemId 互斥;门禁开/关均允许用户显式创建,旧绑定合同保留。
|
||||
- [x] 新动作先锁单再锁票,权威金额/日期、可靠类型或 null、未知说明空字符串;新明细+占用+总额重算在事务内,不按相同金额或已有唯一明细匹配。
|
||||
- [x] 同单重试经权限/状态/归属重验复用已占用明细,拒绝跨单占用/非参与人/缺金额日期,不重复写入。
|
||||
- [x] 新增批量设类接口,只改选中 ID 的类型和更新人;异单/失效 ID、停用类型、非参与人或不可编辑状态整批拒绝。保留批准依据,不宣称既有 assets 路径存在硬阻断。
|
||||
- [x] 空说明草稿允许编辑;预检/提交按明细集中阻断,编辑器明确传空字符串。当前 OCR 没有票面项目,不以销售方/文件名伪造支出用途。
|
||||
- [ ] 下一步先接 PC/移动“我的发票”面板:上传/OCR 完成只勾选,不自动添加;整单入口传 createItem,已有明细入口传 linkedItemId。接批量分类控件,再做真实后端并发与空单浏览器回归;本阶段不发布。
|
||||
|
||||
本轮验证:服务端新绑定/草稿分类/费用门禁/错误码镜像 4 文件 219/219 通过;扩展报销路由组前一轮为 12 文件 311/311(之后仅增加空说明预检/提交及相应测试,相关组已重跑)。测试均为合成 Fastify 注入+模拟 Prisma,不据此声称已验证真实锁/事务回滚。旧一组 SQL 隔离验证只覆盖结构迁移,不覆盖本轮 API 并发。未调用腾讯真实 OCR、未改生产、未提交推送;独立复核及完整前端验收待后续。
|
||||
|
||||
本轮最终扩展回归:`cd server && npm test -- tests/unit/routes/invoicePool.*.test.js tests/unit/routes/reimbursements.*.test.js tests/unit/services/invoiceExpenseClassification.test.js tests/unit/scripts/error-reason-client.test.js` 为 18 文件 349/349 通过。`cd client && npm run typecheck`、错误码 strict 合同、Trellis 治理检查、JS 语法与 `git diff --check` 均通过。本轮未重跑浏览器或生产构建,不继承上一阶段构建为新页面验收(新页面尚未接入)。
|
||||
|
||||
### 已批准字段调整:本地阶段
|
||||
|
||||
- [x] 用户明确批准仅费用类型可空的本地实施与隔离验证,生产迁移和部署仍未授权。
|
||||
- [x] 新增可空字段迁移,API 在 draft/rejected 接受明确 null;PATCH 省略类型保留原值,明确 null 清空类型,不调用空 ID 查询。
|
||||
- [x] 报销提交及该单预检在财务计算前拒绝缺失/停用分类并返回明细 ID;保留身份与状态检查。补齐前不启动预算占用或审批。
|
||||
- [x] DTO 与 PC/移动编辑器保留待补值,可保存其他字段,不因打开编辑器默认选中第一类型;金额、日期约束未放宽。
|
||||
- [x] 一次性 PostgreSQL 16、无网络、tmpfs 库执行历史 SQL 建立基线,再验证新迁移:旧分类不变、空分类持久化、外键及金额/日期非空约束保留;存在空值时逆迁移拒绝、合成记录明确设类后可恢复非空。测试退出删除专用容器,不连接共享开发库或生产;这是 SQL/约束测试,不是 Prisma migrate deploy 或完整业务 E2E。
|
||||
- [ ] 接续实现显式选票建明细事务、批量设类 API 与面板,完成真实后端/浏览器验证。当前仍不是可发布的完整发票流程;暂不叠加退回删除和付款改动。
|
||||
- 只读独立复核待完成:当前 inline 模式仅由主会话检查,没有虚报独立 reviewer。
|
||||
|
||||
本阶段最终验证:
|
||||
|
||||
- `cd server && npm test -- tests/unit/routes/reimbursements.*.test.js tests/unit/services/invoiceExpenseClassification.test.js tests/unit/scripts/error-reason-client.test.js`:12 文件,295/295 通过;详情测试初次漏传合成认证头产生 401,补齐测试头后单文件 37/37 及全组通过,不曾放宽产品认证。
|
||||
- `prisma:validate`、独立依赖目录中的 `prisma:generate` 通过。生成 schema 与 TypeScript unchecked-create 输入确认类型可空、金额/日期必填。最初使用 DMMF 的旧 `isRequired` 元数据断言不适用于当前客户端,未将该失败归为产品缺陷,改为核验实际生成产物后通过。
|
||||
- `cd client && npm run typecheck && npm run build` 通过;Browserslist 数据老旧提示保留,不混入依赖升级。
|
||||
- `RUN_ISOLATED_DB_TEST=1 bash scripts/qa/test-reimbursement-draft-migration.sh` 通过;仅使用现有本地 Postgres 镜像与合成记录,无网络专用库退出已清理。
|
||||
- `node scripts/qa/check-error-reason-contract.mjs --strict`、`node scripts/qa/check-trellis-governance.mjs`、`git diff --check`、改动 JS 的 `node --check` 与新脚本的 `bash -n` 通过。
|
||||
- 路由测试使用 Fastify 注入与模拟 Prisma;SQL 测试使用真实隔离 PostgreSQL。二者互补,但不等于真实 Prisma+HTTP+浏览器端到端验收。实际选票/OCR/批量分类仍未接通,独立复核及桌面/窄屏浏览器验收待后续阶段,不能据本阶段发布。
|
||||
|
||||
### 2026-09-09 首批代码验证
|
||||
|
||||
- 已新增纯分类服务和 10 条合成单元测试,尚未接入 API/页面。仅认可持久化铁路电子客票与启用火车票类型,覆盖未知 VAT、销售方/文件名误导、类型停用/缺失、历史 provider 默认值、客户端字段冒充及输入不变性。
|
||||
- 测试先运行得到预期模块缺失失败;实现后 `cd server && npm test -- tests/unit/services/invoiceExpenseClassification.test.js tests/unit/services/invoiceDocumentMeta.test.js tests/unit/services/ocr.tencent.test.js` 为 3 文件、22/22 通过。`node --check src/services/invoiceExpenseClassification.js` 与 `git diff --check` 通过。
|
||||
- OCR 测试为合成单元测试,不是腾讯真实调用证据;未测完整浏览器流程、真实数据库并发或生产 OCR。未独立复核、未提交、未部署。
|
||||
- 当前需确认具体持久化范围:只放宽明细费用类型列以保存待补草稿,提交前强制补齐;已将跨层兼容及旧镜像回滚风险补入 design。分类逻辑不要求用户每条手填,也不把本次开始实施视为生产迁移授权。
|
||||
|
||||
- [x] 用户采纳“一事一单、自动分类为主、同类批量设置、只处理例外”,同步本 task PRD/design,不新建任务。
|
||||
- [ ] 核定自动类型的可靠输入与批量更新接口,审计草稿保存、刷新及提交约束;不再以逐条手选分类作为数据库非空字段的默认解决方案。
|
||||
- [ ] 在空单流程回归中加入混合类型、同类批量设类、未知项集中处理、用户编辑保留与停用类型拒绝。
|
||||
- 代码、发布、业务验收:本轮仅更新需求和验收合同,未实现分类功能、未部署、未宣称业务验证通过。
|
||||
- 下一步由执行端完成最小技术方案和针对性回归,不等待员工代测;如确需迁移,应给出具体差异和兼容风险,不能复用早先概括性迁移建议。
|
||||
|
||||
## 2026-09-09 后续阶段清单(已进入实施前核查)
|
||||
|
||||
后续实施授权:用户明确“自动填充的明细不需要用户二次确认,但允许再次编辑”,并批准按上述方案执行。该取向已补入 PRD,可继续同范围的最小技术设计、代码实施与本地验证,不因补录合同重复索取授权;产品代码尚未实施。迁移或范围/风险发生实质变化仍先报告,提交、推送、部署、生产配置与真实票据发送按既有单独授权边界执行。
|
||||
|
||||
- [x] 按用户确认更新 PRD/design:选票一次触发 OCR 辅助生成明细及自动关联;上一版手工关联默认方案撤回,不新建 task。
|
||||
- [x] 从已发布 `b87cc55` 建立独立执行副本 `artifacts/isolated-worktrees/20260909-invoice-first`,通过官方 task start/current 对齐本 task;仅承接本 task 三份规划文件。
|
||||
- [x] 核查上传/OCR/绑定/费用类型和 schema,记录不可空类型与 OCR 缺票面项目的实际约束;旧自动匹配不能作为新入口实现。
|
||||
- [ ] 按已采纳分类交互补齐最小接口事务设计与稳定类型判定规则,再审计具体持久化影响;不擅自新增迁移或改成强制先填分类。
|
||||
- [ ] 核实真实 OCR 启用及测试样票/调用权限的可用条件;仅回传脱敏状态,不读回或暴露密钥,不自动修改生产配置。
|
||||
- [ ] 建立空单、已有明细、多选和异常重试的失败用例;实现用户明确选票触发的自动建明细,不调用旧静默金额匹配作为替代。
|
||||
- [ ] 实施双入口面板与后端单票原子事务/重试幂等,完成与删除阶段的释放协议检查;不伪造必填字段、不批量修真实单据。
|
||||
- [ ] 完成真实后端角色 E2E、腾讯 OCR 专项、浏览器桌面/窄屏及反例,保留每层验证与缺口。
|
||||
- [ ] 只读独立复核与规则文档同步;按明确授权提交、推送、部署和专用线上合成检查,旧发布授权不复用。
|
||||
|
||||
下文为历史 B1/B2 已完成阶段,不把其勾选继承为本次自动建明细功能已实现或已验证。
|
||||
|
||||
## 1. 规划门禁
|
||||
|
||||
- [x] 固定 B1+B2 同包开发、分级启用;B2 默认关闭。
|
||||
- [x] 选择 assets “事前批准依据”字段方案,不在本轮关联 PurchaseRequest。
|
||||
- [x] 用户批准以 `2,000 元/条明细`作为办公用品/办公电子设备选类提示阈值,并明确不等同于会计固定资产标准。
|
||||
- [x] 财务负责人为会计科目映射回填责任角色;应在 B1 生产启用前提供,目标日期 `2026-09-05`。届时仍无正式科目表则保持 null,不得由开发猜测。
|
||||
- [x] PRD、design、implement 经用户审查并明确批准实施。
|
||||
|
||||
## 2. Schema 与主数据
|
||||
|
||||
- [x] 新增 `ExpenseType.requires_invoice` 和 `ReimbursementItem.asset_prior_approval_basis` migration,验证既有数据默认值。
|
||||
- [x] 逐条实现改名、停用和新建类型;同步 seed、API DTO、管理页和历史兼容测试。
|
||||
- [x] 为费用类型补员工可读 description;将 `2,000 元/条明细`阈值和多件同类分开记载要求写入办公用品/办公电子设备提示。
|
||||
|
||||
## 3. B1 表单与服务端合同
|
||||
|
||||
- [x] PC/移动端展示材料要求和 assets 事前批准依据字段。
|
||||
- [x] 服务端按权威费用类型校验 assets 字段,覆盖新增、编辑、类型切换后的清理和提交复核。
|
||||
- [x] 基于服务端权威费用类型与金额生成 `2,000 元`办公用品非阻断提示,PC 员工端、移动端和财务审核视图保持一致。
|
||||
- [x] 两项补助保持无票但需要说明/算式的现有业务合同;flag 开关两种状态均覆盖纯补助无票基线。
|
||||
|
||||
## 4. B2 双闸和关联
|
||||
|
||||
- [x] 新增 env config、feature key、DB 初始记录和诊断/审计测试,默认 hard-off/force-off。
|
||||
- [x] flag 开启时要求发票显式选择明细并关闭自动新建;关闭时保持兼容路径。
|
||||
- [x] 提交事务按明细聚合有效票面金额,仅对 `requires_invoice=true` 执行缺票/超额门禁。
|
||||
- [x] 错误返回可定位到明细并使用员工可理解文案;财务保留差异和风险核验信息。
|
||||
|
||||
## 5. 验证与上线
|
||||
|
||||
- [x] 执行 PRD Test Plan 和 Trellis quality gate,做权限/状态/历史草稿/flag 回退专项复核。
|
||||
- [x] B1 部署与真实单据处置顺序已调整:先部署 B1、隔离验证并启用 B2,再由真实财务统一退回 `BX-20260828-0014/0016/0017/0018`,避免员工在无系统指引时重复返工。
|
||||
- [x] 已只读核对生产存量:除审批中的 4 张单据外,没有 draft/rejected assets 明细会被 B1 新校验拦住;不得批量伪造事前批准依据。
|
||||
- [ ] 首次部署只启用 B1;B2 保持关闭,完成 owner 基线和无票补助验证。
|
||||
- [ ] B2 开启必须单独获得生产配置批准;先专用账号/部门灰度,再按证据决定全局。
|
||||
- [ ] 关闭 B2 后复核旧提交路径恢复且关联数据未被删除或解绑。
|
||||
|
||||
## 6. 交接 B3
|
||||
|
||||
- [ ] 汇总真实关联、差额和错误分布的脱敏结论,作为 B3 一页式信息结构输入。
|
||||
- [ ] 明确交接财务行内费用标准提示、票据—明细对应关系和差额解释,不在 B1 中提前重做审批页面。
|
||||
|
||||
## 7. 本地实施与验证记录(2026-08-29)
|
||||
|
||||
- 业务实现提交:`6e999d5`。
|
||||
- Schema、B1、B2 代码均已实现;B2 env hard gate 和 DB 初值仍为关闭,未执行任何生产配置写入。
|
||||
- `prisma validate`、`prisma generate`、generated client 检查通过;只读 `prisma migrate status` 正确识别待应用 migration `20260829120000_reimbursement_expense_policy_invoice_guard`。本地回归库未获 migration 写入批准,因此未执行 `migrate deploy`;existing-data/default/backfill 由静态 migration 合同测试覆盖。
|
||||
- 后端聚焦回归 `59/59` 通过;使用固定 BIFF8 Python 依赖的全量 Vitest 为 `183/183 files`、`1329/1329 tests` 通过。系统 Python 直接运行会产生既有的 3 个付款指令环境型失败,不属于本次 diff。
|
||||
- 前端 `typecheck`、production build 通过;r102 普通报销回归 `6/6`,r111 B1/B2/PC/移动/企业微信回归 `3/3` 通过。
|
||||
- Trellis task validate、governance、process contract、docs registry audit 和 `git diff --check` 通过;docs audit 保留既有的 `22` 个未注册文件 warning,exit 0。
|
||||
- 权限、migration、feature flag、历史兼容和 seed 科目保护已完成三轮只读专项复核,最终 PASS。复核发现并推动修复:pending OCR 关联丢失、flag-off 纯无票补助服务端门禁、PC 无票补助主操作投影。
|
||||
- 未部署 B1、未应用生产 migration、未开启 B2、未做真实数据写入;本 task 保持 `in_progress`,上线与 B3 交接项不得提前勾选。
|
||||
- 生产只读审计把业务处置范围扩大为 4 张审批中单据:0014 为 5 个发票日期压成 1 条明细且 B2 无法自动拦截;0016/0017/0018 存在未关联票据,0017 另有票面差额。最终顺序改为先部署 B1、隔离验证并全局启用 B2,再由真实财务统一退回,日期粒度继续依赖明确退回说明与人工复核。部署与灰度合同见子任务 `08-30-reimbursement-expense-policy-invoice-guard-deploy`。
|
||||
@@ -0,0 +1,142 @@
|
||||
# 报销费用口径与票据明细门禁
|
||||
|
||||
## 2026-10-11 余项移交(待归档授权)
|
||||
|
||||
本地 Git 检查确认实现 `6e999d5`、后续发票优先版本 `e5685f1` 均已包含于用户报告的生产 `4a5cd8f2`。本轮未连接生产;下方“未部署”等是原阶段记录,原未勾选项不补勾,不把包含关系当作全部验收通过。
|
||||
|
||||
报销发票真实 OCR 的跟进入口移到 [报销付款收尾 PRD](../../../09-08-reimbursement-payment-closeout/prd.md#2026-10-11-接收余项报销真实-ocr-验收与财务科目映射) 的 RC-OCR-1–4,财务科目映射移到同节 RC-ACCOUNT。责任、原样票别名、既有证据、退出条件和云/生产授权边界均保留;借款回单 OCR 不能代替报销发票验收。原独立复核未完成记录由接收任务既有复核余项继续跟进,不因移交判为通过。
|
||||
|
||||
原 implement 顶部已记录 2026-09-10 真实腾讯线上合成样票到草稿的 PASS,不能继续描述为“合成样票从未真实识别”;尚未具备同次真实腾讯到提交/审批及约定字段精度核对证据。移交后本任务可进入归档审批,当前仍 `in_progress`,未归档、未提交。
|
||||
|
||||
## Goal
|
||||
|
||||
### 当前交付口径(2026-09-09,最终自检收口)
|
||||
|
||||
- 本次选票自动建明细与集中分类已在独立副本实现并完成分层本地验证;不是下文早期记录所述“尚未实现”。尚未独立复核、提交或部署。
|
||||
- 已批准本地实现费用类型可空迁移,未知类型与空说明只用于可编辑草稿,提交前阻断;生产迁移仍须单独批准。最新接口与回滚合同见 design 的“最小实施与已批准的本地持久化合同”。
|
||||
- assets 事前批准依据目前是字段与提示,不是保存/提交硬门禁。本次保持实际行为,不新增硬门禁。下文 R3、R5 及旧验收勾选中的硬门禁表述属于历史设计/记录,不能作为当前已实现或已验结论;本次批量分类验收是保留依据字段、不改金额及票据关系。
|
||||
- 下文 B1/B2 历史清单不代表本次验收。当前证据分为真实隔离数据库/认证/队列/浏览器(OCR provider 为 mock),以及指定样票单独调用腾讯 OCR;没有同一次真实腾讯到候选浏览器的整链证据。付款和退回删除属于其他既有 task,未在此完成。
|
||||
|
||||
### 2026-09-09 分类交互补充(用户已采纳)
|
||||
|
||||
- 一事一单:同一次出差可在同一张报销单包含火车票和住宿,不为费用分类强制拆单。报销单场景与明细费用类型是不同层级,不把“日常报销”等宽泛场景当作精确费用类型。
|
||||
- 后台保留逐明细费用类型,用于材料要求、费用标准与财务统计;前台不得要求每条明细重复手选,不让员工选择会计科目。
|
||||
- 可靠识别的类型自动填入已启用类型,生成的明细可编辑,无逐条确认、整批二次确认或已确认标记。
|
||||
- 无法确定的类型集中提示并支持对用户选中的同类明细一次批量设置。正常已分类明细不被未识别项拖入重复填写;用户手工修正后不被 OCR 回调、刷新或后续默认值覆盖。
|
||||
- 只有用户明确选择的具体费用类型才可作为相应所选明细的默认值;不把“差旅”“日常”或销售方名称推断为精确费用类型,不因餐饮票面就直接判为业务招待。
|
||||
- 添加与分类是不同动作:分类不得增加一次发票关联步骤,也不要求用户逐字段确认 OCR 已填值。提交前检查仍缺少的必要信息,保留原审批、费用标准、assets 批准依据字段/提示与票面金额门禁,不宣称 assets 已有硬门禁。
|
||||
- 先落实交互再裁定持久化设计;撤回“先请用户批准类型可空迁移”的推进顺序。本次产品取向确认不是数据库迁移、生产写入或部署授权。
|
||||
|
||||
验收增加:同单火车票加住宿、同类多条一次设类、混合类型不整单覆盖、未知类型集中处理、手改类型刷新保留、已识别项零确认、停用类型与非参与人拒绝、批量改为 assets 时保留批准依据字段及实际提示规则。仍需验证刷新后的待处理信息与服务端提交阻断,不能仅有前端临时状态就宣称完整支持。
|
||||
|
||||
### 2026-09-09 已确认后续流程:选票自动生成明细
|
||||
|
||||
用户明确选择省操作的默认流程,并已批准按确认范围实施。本节替换此前“待关联区域→手工建明细→再关联”的默认方案;不新建 task。当前产品代码尚未实现,提交、部署与生产写入不由此自动授权。下文原 B1/B2 要求与历史已验结果保留;涉及自动建明细的后续目标以本节及最新分类补充为准,不能拿旧禁令否定已确认的新交互,也不能恢复旧版按金额猜测的静默自动匹配。
|
||||
|
||||
- 报销页点击“添加发票”打开“我的发票”面板,桌面弹窗、手机全屏面板,不跳离报销单、不丢填写内容。票据中心/发票池是同一数据来源,不复制第二份资料;允许提前存票,报销时再选。
|
||||
- 面板支持多选已有可用票、批量上传新 PDF;腾讯真实 OCR 默认启用,上传自动识别。新上传且识别成功、可用的票默认勾选,用户可取消;未点“添加到报销单”前不创建明细,不能因 OCR 后台完成或刷新自动提交选择。
|
||||
- 从整单入口点击“添加到报销单”后,一张所选发票生成一条可编辑费用明细并自动关联;多选批量生成,不按相同金额或票面多行擅自合并/拆分,不让员工再手工关联一次。
|
||||
- 用户补充确认:自动填充的明细直接保存为可编辑草稿,不增加逐条确认或整批二次确认,不设置“已确认”标记作为提交前置;用户可主动修正,只有必填缺失或现有业务校验不通过时提示处理。该要求不取消报销本身的“提交审批”动作。
|
||||
- 价税合计预填报销金额;开票日期可作为发生日期的可编辑初值,不能假定必然相同;票面项目生成可编辑说明。费用类型仅可靠匹配启用类型时推荐,不确定时只要求补必要字段,不凭空填其他费用类型或编码,不强迫逐字段重复确认。
|
||||
- 从已有明细入口添加发票时,明确目标,直接关联到该明细且不创建另一条明细;允许同一笔支出的多票关联一条明细,不把同一票分配多条明细,不擅自改写原明细日期/类型/申报金额。
|
||||
- 正常页面在明细内显示“发票 N 张”和预览入口,不展示绑定/关联技术状态。只有识别异常、历史未关联、需票明细缺票等需要动作时显示具体提示和就地补救,手动修复不是默认必经步骤。
|
||||
- 上传成功不等于 OCR 成功,OCR 成功不等于用户已把票加入报销。识别中/失败/缺字段/重复/占用均要逐票可恢复;刷新或返回能找到已上传票和本单已保存明细,不要求重传或重复 OCR。
|
||||
- 归属、状态、查重、有效占用、明细归属、申报金额与原提交门禁保持服务端强约束。批量、双击、超时重试、跨单并发不得产生重复明细、孤立明细或一票多占用。
|
||||
- OCR 关闭/误配视为需要处理的系统异常,不作为本轮正常交付路径;是否实际启用及识别成功须核配置和调用证据,不以用户陈述或旧数据库 provider 默认值冒充已验证。不自动启用 mock,不擅自变更生产密钥、开关或发送未经授权票据。
|
||||
- 必测空单开始的“上传新票/选择旧票→自动生成明细并关联→核对补齐→提交”及提前存票、多选、刷新、已有明细加票、异常重试、退回修改、删除后重新使用;保留非参与人/重复票/已报销/并发等反例。
|
||||
|
||||
把现有自创或含糊的费用类型收敛为财务可理解的企业费用口径,明确发票要求和 assets 个人报销例外条件;同时建立可核验的票据—明细关系与申报金额门禁,避免多日期、多支出、多张发票被合并为无法复核的一条笼统明细,并通过默认关闭的双闸门保护现有唯一真实报销链路。
|
||||
|
||||
## Background and Confirmed Facts
|
||||
|
||||
- `ExpenseType` 已有 `enabled`、`description`、`account_code_default`,但没有 `requires_invoice`;本任务必须新增 migration。
|
||||
- 当前种子费用类型的 `account_code_default` 全部为空,不能由开发人员自行编造会计科目编码。
|
||||
- 现有 feature flag 是 env、`FEATURE_FLAG_KEYS` 白名单和 DB 记录三件套;env 关闭时 DB `force_on` 也不能生效。
|
||||
- 当前多明细发票匹配允许金额无法唯一匹配时自动创建明细;这与“员工明确选择明细、每张有效票据可核验”目标冲突。
|
||||
- 伙食补助费和市内交通补助费按天包干且不要求发票;数量/天数乘算能力不在本任务内。
|
||||
- assets 类当前只提示报销完成后人工登记资产,没有“事前批准”结构化证据。
|
||||
|
||||
## Scope
|
||||
|
||||
- B1:费用类型 schema、历史兼容主数据、员工可读说明、`requires_invoice` 与 assets 事前批准依据。
|
||||
- B2:明确票据—明细关系、停止静默自动建明细、按需票明细执行金额门禁,以及默认关闭的分级启用机制。
|
||||
- PC、移动端、服务端、Prisma migration、种子/配置、测试与现有业务事实源同步纳入同一交付合同。
|
||||
|
||||
## Requirements
|
||||
|
||||
### R1 费用类型与历史兼容
|
||||
|
||||
- `business_client_meal` 保持 code 不变,将名称从“客户招待”改为“业务招待费”;历史单据追溯显示新名称,视为同一概念的术语纠正。
|
||||
- 停用 `business_training`“培训会议”,新建“会议费”和“培训费”两个独立类型;历史单据继续显示原类型,新报销不能再选择已停用类型。
|
||||
- 停用 `assets_it_hardware`“IT 硬件”;低值配件使用既有“办公用品”,需登记资产的办公电子设备使用新类型“办公电子设备”。历史单据保持原类型名称。
|
||||
- 停用均复用 `enabled=false` 模式,不删除类型、不重写历史明细外键。
|
||||
- 逐项补充面向员工的 `description`,说明适用范围和必要材料;不得把内部 category/code 当作员工文案。
|
||||
|
||||
### R2 发票要求与会计科目
|
||||
|
||||
- `ExpenseType` 新增 `requires_invoice`,默认 `true`;“伙食补助费”和“市内交通补助费”明确为 `false`,其余首期启用类型为 `true`。
|
||||
- `requires_invoice=false` 只免除发票要求,不免除业务说明、补助算式、审批或其他必要附件。
|
||||
- `account_code_default` 不填猜测值。财务负责人应在 B1 生产启用前提供现行会计科目映射;若届时仍无法提供,本任务保留 null 并在同一 task 留下具名责任角色、缺口和回填日期,不以虚构编码阻塞材料门禁。
|
||||
|
||||
### R3 assets 个人报销例外
|
||||
|
||||
- assets 类允许个人报销,但属于经事前批准的例外,不作为普通日常采购入口。
|
||||
- 采用低成本方案:`ReimbursementItem` 增加单一结构化“事前批准依据”字段;选择 assets 类时必须填写批准人、批准时间和可核验的简短依据,服务端在保存和提交时校验,PC/移动端合同一致。
|
||||
- 采购申请关联不纳入本任务;未来如需替代自由文本,另行设计与 `PurchaseRequest` 的审计关联。
|
||||
- 员工按单条明细金额区分“办公用品”和“办公电子设备”:`2,000 元`及以上通常选择“办公电子设备”,低于 `2,000 元`通常选择“办公用品”。该阈值只用于内部选类和资产登记提示,不等同于会计固定资产确认标准。
|
||||
- 阈值同时写入费用类型 `description` 和表单提示;在数量/单价模型暂缓期间,一条明细应是一件或一批同日、同类、同一业务支出的物品,多件同类物品应分开记载,以便按本条明细金额判断。
|
||||
- 明细金额达到 `2,000 元`但选择“办公用品”时,员工端和财务审核视图均显示非阻断提示:“该金额通常应选择办公电子设备并填写事前批准依据”;提示不得自动改类型或阻断提交,财务仍按实际物品性质复核。
|
||||
|
||||
### R4 票据—明细强关联
|
||||
|
||||
- 每张有效发票必须显式关联到一条费用明细;员工在上传/绑定时明确选择目标明细。
|
||||
- B2 启用后关闭“无法唯一匹配时自动新建明细”;服务端不得在前端要求明确选择的同时继续静默建明细。
|
||||
- 一条明细只表达同一费用类型、同一发生日期和同一业务支出;采购物品还应是一件或一批同类物品。不同日期、不同支出或需要分别判断 `2,000 元`设备提示的多件物品必须拆分。多张同日、同类型且同一业务支出的发票可以关联同一明细。
|
||||
- 发票、明细及关联变更保留既有审计和权限边界;非参与人不得借新入口维护他人报销。
|
||||
|
||||
### R5 申报金额门禁
|
||||
|
||||
- 仅对 `requires_invoice=true` 的明细比较申报金额与其关联有效发票票面合计。
|
||||
- 无票补助不参与票面金额比较,不得因票面合计为 0 阻断正数补助。
|
||||
- 需票明细未关联有效发票,或申报金额高于关联票面合计时,B2 启用状态下阻断提交并返回逐条可操作提示。
|
||||
- 申报金额低于票面合计允许提交,但必须保留现有差异说明/财务核验信息;退款、折扣、他人承担等不得重复报销。
|
||||
- 历史草稿不批量改写;在 B2 开启后再次提交时按新门禁补齐关联、差异说明和 assets 批准依据。
|
||||
|
||||
### R6 分级启用与回退
|
||||
|
||||
- B1 schema、主数据、字段和展示随代码部署生效,但不得主动阻断不涉及 assets 的既有提交路径。
|
||||
- B2 的自动建明细关闭、强关联和金额提交门禁由新增 feature flag 控制,必须接入 env 配置、`FEATURE_FLAG_KEYS` 白名单和 DB 记录,初始保持 env hard-off / DB force-off。
|
||||
- 开启前必须满足:owner 正常提交基线;两项无票补助不被阻断;多票关联正确;申报金额超票面能准确阻断;历史草稿可补齐后提交;关闭 flag 能立即恢复旧提交门禁;合成或专用账号验证无越权副作用。
|
||||
- 关闭 flag 只回退提交门禁和自动匹配行为选择,不删除、不解绑、不回退已经建立的票据—明细关联数据。
|
||||
- B2 的生产开启属于独立配置写入,必须单独批准并完成角色化公网检查。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] migration 安全新增 `ExpenseType.requires_invoice` 和明细事前批准依据字段,既有数据有确定默认值且 Prisma validate/status 通过。
|
||||
- [x] 费用类型逐条按“改名”或“停用+新建”执行;历史单据无丢失,新建/编辑列表不显示停用类型。
|
||||
- [x] assets 明细在 PC、移动端和服务端均强制填写事前批准依据,提示明确 `2,000 元/条明细`是选类门槛而非会计固定资产标准。
|
||||
- [x] 办公用品明细金额达到 `2,000 元`时,员工端和财务审核视图均显示非阻断类型不一致提示,提交仍可继续且服务端不自动改类。
|
||||
- [x] 两项补助 `requires_invoice=false`,其他首期启用类型为 true;无票补助可提交且不会触发票面金额门禁。
|
||||
- [x] B2 flag 三件套完整、默认关闭;关闭状态保持当前提交兼容,开启状态关闭自动建明细并执行强关联/金额门禁。
|
||||
- [x] 每张有效发票关联一条明确明细,不同日期/支出必须拆分;同日同类型同一支出允许多票关联。
|
||||
- [x] 需票明细缺票或申报高于关联票面合计时逐条阻断,低于票面时保留差异核验信息。
|
||||
- [x] flag 关闭后既有关联数据保持不变,不进行数据回滚。
|
||||
- [x] owner、submitter、财务/管理员只读边界和 `precheck` 对象授权不回归。
|
||||
- [ ] 财务科目映射由财务负责人提供并在 task 内记录回填结果;没有正式科目表时保持 null,不虚构编码。
|
||||
- [x] 聚焦单测、API 集成、PC/移动 Playwright、Prisma validate、前后端类型/构建、Trellis governance 和 migration 静态/只读检查通过。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 数量、天数、单价自动乘算和差旅主线扩建。
|
||||
- B3 财务一页式审批工作台及其行内费用标准提示恢复。
|
||||
- 强制关联采购申请、自动建资产卡、会计凭证、总账或 ERP 对接。
|
||||
- 处理 `BX-20260828-0017` 真实单据或其他生产报销。
|
||||
- 在本任务部署授权中推定 B2 flag 开启授权。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- Prisma:migration review、`npm run prisma:validate`、existing-data migration fixture、pre/post migration status。
|
||||
- 后端:费用类型映射、assets 字段校验、bind/submit 强关联、金额边界、无票补助、flag on/off、历史草稿和权限负例。
|
||||
- 前端:PC/移动费用类型提示、事前批准依据、明确选择明细、逐条错误展示和非参与人只读回归。
|
||||
- 回归:报销创建/编辑/提交、invoice pool bind/unbind、OCR 自动匹配、差旅补助、材料预览、付款准备相关聚焦套件。
|
||||
- 全局:server/client typecheck/build、Trellis governance、docs audit、`git diff --check`。
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"id": "reimbursement-expense-policy-invoice-guard",
|
||||
"name": "reimbursement-expense-policy-invoice-guard",
|
||||
"title": "报销费用口径与票据明细门禁",
|
||||
"description": "统一企业报销费用类型与材料政策,并以默认关闭的双闸门逐步启用票据—明细强关联和申报金额门禁。",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": "expense type policy/schema/master data; reimbursement item invoice relation; staged submit guard",
|
||||
"package": "server,client,docs",
|
||||
"priority": "P1",
|
||||
"creator": "shunli",
|
||||
"assignee": "shunli",
|
||||
"createdAt": "2026-08-29",
|
||||
"completedAt": "2026-10-11",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": "6e999d5",
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [
|
||||
"08-30-reimbursement-expense-policy-invoice-guard-deploy",
|
||||
"08-30-reimbursement-assets-prior-approval-correction",
|
||||
"08-30-reimbursement-employee-guide-b2-sync",
|
||||
"08-30-b2-submit-error-ocr-fallback"
|
||||
],
|
||||
"parent": null,
|
||||
"relatedFiles": [
|
||||
"server/prisma/schema.prisma",
|
||||
"server/src/scripts/seed-config.js",
|
||||
"server/src/services/featureFlags/index.js",
|
||||
"server/src/config.js",
|
||||
"server/src/routes/reimbursements.js",
|
||||
"server/src/routes/invoicePool.js",
|
||||
"client/src/pages/ReimbursementDetailPage.tsx",
|
||||
"client/src/pages/MobileReimbursementDetailPage.tsx",
|
||||
"docs/产品口径.md",
|
||||
"docs/报销发票附件规则.md",
|
||||
"docs/需求-报销发票OCR规约.md"
|
||||
],
|
||||
"notes": "2026-10-11 报销真实OCR余项与财务科目映射已移交09-08-reimbursement-payment-closeout的RC-OCR-1–4/RC-ACCOUNT;责任、证据及原未勾选项保留,独立复核由接收任务既有余项跟进。6e999d5/e5685f1本地祖先检查确认包含于用户报告的生产4a5cd8f2,本轮未查生产。已具备原合成草稿阶段PASS,不扩成审批或精度通过;待归档授权,未提交。以下为历史阶段说明:\n本地实施和只读专项复核已完成;B2 env/DB 双闸仍默认关闭,未部署、未应用生产 migration、未写生产配置。2,000 元/条明细的选类提示阈值已批准。会计科目映射由财务负责人在 B1 生产启用前提供,目标回填日期 2026-09-05;未取得正式科目表时保持 null,不由开发人员猜测。生产只读审计确认 0014/0016/0017/0018 均需退回修正;顺序改为先部署 B1、隔离验证并启用 B2,再由真实财务统一退回。B2 不校验日期粒度,0014 必须按 5 个发票日期拆分并由财务人工复核。部署合同见子任务 08-30-reimbursement-expense-policy-invoice-guard-deploy。B3 财务一页式及行内费用标准提示恢复仍为后续任务。",
|
||||
"meta": {}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# 设计:报销费用政策与票据门禁生产部署
|
||||
|
||||
## 发布边界
|
||||
|
||||
本任务不再修改业务代码。候选包含父任务业务提交 `6e999d5` 和本部署合同;规划文档提交完成后以最终干净 `HEAD` 冻结,并在外部审批记录写完整 SHA 与 12 位 bundle marker。当前生产 `59dc5f6` 是固定应用回滚基线。
|
||||
|
||||
生产切换分为三个独立状态:
|
||||
|
||||
```text
|
||||
B1 部署:新 schema + 主数据 + 表单合同
|
||||
env hard gate = off(或 env on 但 DB force_off)
|
||||
DB flag = force_off
|
||||
B2 effective = false
|
||||
|
||||
B2 隔离灰度:同一应用候选
|
||||
env hard gate = on
|
||||
DB flag = force_on + department_allowlist + 隔离空部门
|
||||
B2 只对 smoke employee 生效
|
||||
|
||||
B2 全局启用:同一应用候选
|
||||
env hard gate = on
|
||||
DB flag = force_on + global
|
||||
B2 对全局提交生效
|
||||
```
|
||||
|
||||
任何状态跃迁都必须先完成上一状态的验收,并取得对应生产授权。
|
||||
|
||||
## 验收身份与数据隔离
|
||||
|
||||
- admin smoke:只用于系统设置、非参与人视图和 flag 操作,不代行财务审批。
|
||||
- employee smoke:从当前 5 人部门迁入已确认零成员的隔离部门,建立合成草稿并验证 owner 合同;终态常驻该部门,不再迁回。
|
||||
- manager smoke:新建 manager + employee 的 OA-only 专用身份并设为 employee smoke 的直属 manager。普通 employee 没有直属 manager 时,提交会被 `no_manager_found` 阻断,不会走 finance fallback。
|
||||
- finance smoke:新建 finance + employee 的 OA-only 专用身份,放入隔离部门,用于财务只读投影;不恢复已消失旧账号,不绑定真人身份。因为完整 finance 角色包含付款能力,账号在验收窗口外保持 disabled,每次启停均需生产账号写入授权。
|
||||
- 真实 finance:仅负责 4 张真人单据的业务退回和重提复核,凭据不交给自动化。
|
||||
|
||||
隔离部门改名为“系统测试专用(勿分配员工)”。名称用于降低 HR 误分配概率,但每次灰度仍必须以实时成员集合为准:只允许 employee/manager/finance 三个 smoke,任何第四个成员都使灰度门关闭。
|
||||
|
||||
合成材料必须是无真实抬头、号码、账户和业务正文的测试 PDF/图片。成功提交的合成单据通过 owner 撤回回到草稿再删除;阻断场景直接删除草稿。审计记录保留。
|
||||
|
||||
## Migration 与 seed
|
||||
|
||||
唯一 migration:`20260829120000_reimbursement_expense_policy_invoice_guard`。
|
||||
|
||||
- migration 新增向前兼容列并回填两项免票类型。
|
||||
- `seed:config` 落地费用类型改名、停用、新建、description、requires_invoice 和 B2 DB `force_off` 初值。
|
||||
- seed 更新路径不写 `account_code_default`,部署前后须按 code 对比,防止既有财务映射丢失。
|
||||
- deploy init 同时运行 `seed:trialrun`,因此 pre/post 证据必须覆盖 migration 与主数据,而不能只看 `_prisma_migrations`。
|
||||
|
||||
## B2 配置数据流
|
||||
|
||||
生产环境不由 `SYNC_ENV=0` 自动同步。正确顺序为:
|
||||
|
||||
1. B1 init 后确认 DB flag 记录已由 seed 显式创建为 `force_off`,不得接受 `missing_flag_record`,并保存完整快照。
|
||||
2. 经生产配置授权把 env hard gate 设为 true,重启同一候选。
|
||||
3. 验证 `envEnabled=true`、`effectiveEnabled=false`、reason=`flag_force_off`。
|
||||
4. 经灰度授权前实时确认隔离部门只有三个 smoke,并在获批窗口临时启用 finance smoke;随后把 DB 更新为 department allowlist,仅含隔离部门。
|
||||
5. 执行合成验证;失败恢复调用前快照,优先 `force_off`。
|
||||
6. 灰度 PASS 后,经全局授权更新为 global force_on;验收窗口结束立即 disabled finance smoke。
|
||||
|
||||
flag 更新使用 API 的 expectedVersion 并发控制并自动写审计。关闭 flag 只改变后续行为,不删除已建立的关联。
|
||||
|
||||
门禁状态证据使用精确 reason:`env_hard_off` → `flag_force_off` → 名单外 `department_not_allowed` / 隔离部门 `flag_force_on_department_allowed` → 全局 `flag_force_on_global`。普通 flag 列表无法按 actor 完成 strict department 判断,须在受控容器内调用同一 evaluator 做只读诊断,并与详情 DTO 的 `invoiceGuardEnabled` 交叉核对。
|
||||
|
||||
## 真实单据处置
|
||||
|
||||
B2 全局启用后再退回 4 张真人单据,使员工只重做一次并获得系统关联/金额提示。日期粒度仍由退回说明与财务人工复核承担:
|
||||
|
||||
| 单号 | 系统可拦 | 人工要求 |
|
||||
| --- | --- | --- |
|
||||
| 0014 | 否 | 5 个日期拆 5 条明细,重提后人工确认 |
|
||||
| 0016 | 未关联票据 | 至少按 3 个日期拆分并逐票关联 |
|
||||
| 0017 | 未关联、超票面 | 2 个日期重做、正式 PDF、差额说明 |
|
||||
| 0018 | 未关联票据 | 修正日期;同日同类可保留一条 |
|
||||
|
||||
已付款 0008 与空草稿 0013 不进入处置动作。
|
||||
|
||||
## 回滚与补偿
|
||||
|
||||
回滚分三层,不能混用:
|
||||
|
||||
1. B2 行为异常:DB flag 立即 `force_off`,关联数据保留。
|
||||
2. 应用异常:确保 B2 off,使用 existing 入口回滚到 `59dc5f6`;不逆 migration。
|
||||
3. B1 主数据错误:应用回滚不会恢复 seed 结果。只在单独批准后,依据部署前快照对受影响 code 做精确补偿;不执行整库 restore,不删除新增类型或历史外键。
|
||||
|
||||
回滚后重复 build、migration status、runtime/external smoke、挂载、备份合同与公网入口检查。
|
||||
|
||||
## 证据与隐私
|
||||
|
||||
报告只保存 PASS/FAIL、SHA、migration 名、类型 code/状态、flag 状态、合成标识和计数。不得保存密码、token、完整账号、真实票据、姓名、业务正文或原始 API payload。材料预览只记录页数、页序和比例,允许保留系统生成的审计记录。
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# 实施清单:报销费用政策与票据门禁生产部署
|
||||
|
||||
## 2026-10-11 文档接续:余项已移交,未执行旧部署清单
|
||||
|
||||
报销真实 OCR 与财务科目映射已登记至 [09-08 接收合同](../../../09-08-reimbursement-payment-closeout/prd.md#2026-10-11-接收余项报销真实-ocr-验收与财务科目映射),由其 implement 顶部继续安排。父任务 2026-09-10 已有真实腾讯线上合成草稿证据,提交/审批、约定字段核对与科目回填仍开放;原独立复核和真实业务义务不因移交关闭。
|
||||
|
||||
本任务原部署步骤未逐项留有执行证据,保留全部未勾选清单,不用后续部署替它补勾。后续发版须按当时最终候选重新核迁移、运行版本、备份/回滚及授权,不执行下方过期参数。本轮仅本地文档移交,归档、提交待明确授权。
|
||||
|
||||
## 1. 规划与冻结
|
||||
|
||||
- [ ] PRD、design、implement 经用户审查并明确批准进入实施。
|
||||
- [ ] 本规划提交完成后确认工作区完全干净,以最终 `HEAD` 冻结候选;外部审批记录写死完整 SHA 与 12 位 bundle marker。
|
||||
- [ ] 冻结后无任何提交、修改或 untracked 文件;发生漂移则重新冻结、重新审查。
|
||||
|
||||
## 2. 验收准备(独立生产写入授权)
|
||||
|
||||
- [ ] 再次只读确认隔离部门零真人成员,经生产写入批准改名为“系统测试专用(勿分配员工)”,并固定为常驻 smoke 部门。
|
||||
- [ ] 新建 OA-only manager smoke(manager + employee)与 finance smoke(finance + employee),凭据写入私有 smoke env,不输出、不绑定真人。
|
||||
- [ ] 把 employee smoke 迁入常驻 smoke 部门,将 manager smoke 设置为其直属 manager;先用合成空壳草稿确认 workflow 可分配,再创建明显标识的合成历史草稿和非真实票据/附件。
|
||||
- [ ] finance smoke 创建后默认 disabled;验收窗口开始前经单独批准临时启用,窗口结束立即 disabled。不得让持付款能力的 smoke finance 常驻 active。
|
||||
- [ ] 明确授权材料合并预览产生设计内审计记录;真实材料不落测试报告。
|
||||
|
||||
## 3. Push 与来源门禁
|
||||
|
||||
- [ ] 用户单独批准 push;只推送冻结候选所在 `main`。
|
||||
- [ ] 确认 `origin/main` 等于或包含候选,严格 provenance PASS,`published=true`、`remoteVerified=true`、`overrideUsed=false`。
|
||||
- [ ] push/provenance 不推定部署、配置或业务写入授权。
|
||||
|
||||
## 4. B1 部署前门禁
|
||||
|
||||
- [ ] 用户在 provenance 通过后单独批准 B1 部署。
|
||||
- [ ] `check-existing-deployment.sh`、生产 identity、existing 数据/附件挂载和备份合同均 PASS。
|
||||
- [ ] init 前显式记录 Prisma status,pending 只能是 `20260829120000_reimbursement_expense_policy_invoice_guard`。
|
||||
- [ ] 保存受影响费用类型 code/name/enabled/description/category/account_code 的脱敏快照;记录 B2 env/DB flag 调用前状态。
|
||||
- [ ] 部署前记录 B2 flag 是否尚不存在;不得把缺失解释为最终关闭合同。若记录已存在则必须为 `force_off`;若 env hard gate 已提前获批设为 true,必须证明 effective 仍为 false。
|
||||
|
||||
## 5. B1 正式发布
|
||||
|
||||
- [ ] 使用 `deploy-existing.sh`、existing 模式、`SYNC_ENV=0` 执行 migration、seed 和应用切换。
|
||||
- [ ] init 后显式记录 migration status;核对新增列、两项免票、其余启用类型需票、费用类型改名/停用/新建和 account code 保留。
|
||||
- [ ] 核对 B2 DB flag 记录明确存在、override=`force_off`、reason 不为 `missing_flag_record`;缺失立即阻断,不继续 B1 验收。
|
||||
- [ ] `/ready`、12 位 bundle marker、runtime/external smoke、服务状态和备份合同 refresh 全部 PASS。
|
||||
- [ ] B2 effective off;owner 普通维护、assets 批准依据、2,000 元提示、无票补助和非参与人只读投影 PASS。
|
||||
- [ ] admin/finance/employee 角色化公网验收 PASS;材料合并预览页序/比例正常并只产生获批审计。
|
||||
|
||||
## 6. B2 env hard gate(独立生产配置授权)
|
||||
|
||||
- [ ] 保存 env/flag 快照,确认 DB `force_off`。
|
||||
- [ ] 将生产 `REIMBURSEMENT_INVOICE_GUARD_ENABLED=true` 写入受控 env,重启同一候选,不同步其他 env。
|
||||
- [ ] 验证 env on、DB force_off、effective off 且 reason=`flag_force_off`;失败则恢复 env 快照并重启。
|
||||
|
||||
## 7. B2 隔离灰度(独立 flag 授权)
|
||||
|
||||
- [ ] 灰度前实时读取隔离部门成员,确认恰好只有 employee/manager/finance 三个 smoke、无真人;临时启用 finance smoke 后,用 expectedVersion 把 flag 更新为 `force_on + department_allowlist + 隔离部门`。受控容器内只读诊断确认隔离部门 reason=`flag_force_on_department_allowed`、名单外 reason=`department_not_allowed`,并与详情 DTO `invoiceGuardEnabled` 交叉核对。
|
||||
- [ ] 合成验证 owner 普通提交成功。
|
||||
- [ ] 合成验证两项无票补助不被阻断。
|
||||
- [ ] 合成验证多票显式关联成功。
|
||||
- [ ] 合成验证申报金额高于票面时准确阻断。
|
||||
- [ ] 合成验证旧草稿补齐关联/批准依据后可提交。
|
||||
- [ ] 合成验证 admin/finance 非参与人无法写入且零业务副作用。
|
||||
- [ ] 切回 `force_off` 验证旧提交路径恢复、既有关联不删除不解绑,再恢复灰度快照继续最终判断。
|
||||
- [ ] 任一失败立即恢复调用前 flag 快照,至少先落 `force_off`;不操作 4 张真人单据。
|
||||
|
||||
## 8. B2 全局启用(独立授权)
|
||||
|
||||
- [ ] 灰度七项全 PASS,成功合成单据已撤回并删除,失败草稿已删除;审计历史保留。
|
||||
- [ ] 用户单独批准 global enable;用 expectedVersion 更新为 global force_on。
|
||||
- [ ] 验证全局 effective on、reason=`flag_force_on_global`、owner 基线、非参与人只读、错误提示、runtime 指标和审计记录。
|
||||
- [ ] employee/manager/finance smoke 常驻隔离部门,确认成员集合仍只有三个 smoke且无真人;不再执行迁回写入,finance smoke 在窗口结束后立即 disabled。
|
||||
|
||||
## 9. 四张真实单据退回与重提
|
||||
|
||||
- [ ] 由真实 finance 退回 0014:说明“5 个发票日期拆成 5 条明细”,重提后人工确认。
|
||||
- [ ] 由真实 finance 退回 0016:至少按 3 个日期拆分并逐票关联。
|
||||
- [ ] 由真实 finance 退回 0017:按 2 个日期重做、从发票入口上传正式 PDF、逐票关联并解释 1,949.81 元差额。
|
||||
- [ ] 由真实 finance 退回 0018:修正日期并逐票关联;同日同类可保留一条。
|
||||
- [ ] 0008 已付款只留历史关联缺口;0013 空草稿不改。
|
||||
|
||||
## 10. 失败与回滚
|
||||
|
||||
- [ ] 切换前失败只停止;切换后失败先 B2 `force_off`,再用同一 existing 入口回滚固定应用基线 `59dc5f6`。
|
||||
- [ ] 不 reset、不逆 migration、不删除关联;主数据问题只凭部署前快照和单独批准做精确补偿。
|
||||
- [ ] 回滚后复核 build、migration status、runtime/external smoke、DB/附件挂载、备份合同与公网入口。
|
||||
|
||||
## 11. 收口
|
||||
|
||||
- [ ] 所有生产证据脱敏回填;不保存凭据、真人信息、真实票据或原始 payload。
|
||||
- [ ] B1/B2 与四张单据处置全部完成后才通知普通用户测试、归档本部署子任务并更新父 task。
|
||||
- [ ] B3 一页式继续作为独立后续任务;日期粒度自动门禁仍明确不在本轮。
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
# 报销费用政策与票据门禁生产部署
|
||||
|
||||
## 2026-10-11 余项移交与旧部署合同边界(待归档授权)
|
||||
|
||||
父任务实现 `6e999d5` 与后续发票优先版本 `e5685f1` 已由后续发布带入用户报告的生产 `4a5cd8f2`,本轮以本地 Git 祖先关系核对,未连接生产。原 B1/B2 分阶段冻结、测试身份准备、灰度/全局启用及单据退回清单保留当时合同;本任务没有逐项执行记录,不能因为后续版本包含而补勾或声称这些步骤都在本任务完成。资产纠偏已由原子任务归档记录单独收口,不回退到旧“资产事前批准硬门禁”重做。
|
||||
|
||||
当前报销发票真实 OCR 及财务科目映射统一移到 [09-08 接收合同](../../../09-08-reimbursement-payment-closeout/prd.md#2026-10-11-接收余项报销真实-ocr-验收与财务科目映射) 的 RC-OCR-1–4 与 RC-ACCOUNT,含原证据、财务/执行端责任及验收边界;未完成责任不取消,不放入借款回单 OCR。原未满足的真实业务、发布/独立证据义务仍按接收任务现有工作线跟进,不由移交判为通过。
|
||||
|
||||
本轮未执行任何生产部署或配置/业务写入;下方固定回滚版本与迁移预期属于旧发布时点,不是下次发版的执行参数。移交后本子任务可进入归档审批,仍 `in_progress`,归档及提交待明确授权。
|
||||
|
||||
## Goal
|
||||
|
||||
把已完成并复核的报销费用政策与票据门禁安全发布到生产:先以 B2 有效关闭的状态完成 B1 migration、主数据和 owner 基线验收,再在隔离部门内验证 B2,最后全局启用并让财务统一退回 4 张真实审批中单据。全过程不得拿真人单据做灰度试验,不得因应用回滚误删 migration 或已建立的票据—明细关系。
|
||||
|
||||
## Background and Confirmed Facts
|
||||
|
||||
- 生产当前运行候选为 `59dc5f6`;本地 `main` 比 `origin/main` 领先 4 个提交,B1/B2 业务实现为 `6e999d5`,尚未推送、部署或应用生产 migration。
|
||||
- 待应用 migration 固定为 `20260829120000_reimbursement_expense_policy_invoice_guard`,只新增 `expense_types.requires_invoice`(非空、默认 true)、`reimbursement_items.asset_prior_approval_basis`(可空),并把两项补助精确回填为免票。
|
||||
- init 会运行 migration、`seed:config` 与 `seed:trialrun`;费用类型改名、停用和新建由 seed 落地,不能只核 migration 表。
|
||||
- B2 是 env hard gate、代码白名单和 DB flag 三件套。生产部署使用 `SYNC_ENV=0`,不会自动把新增 env 写入生产;env 未开启时 DB `force_on` 无效。
|
||||
- B2 提交门禁只检查:已绑定发票是否关联明细、需票明细是否有关联票面金额、申报金额是否高于关联票面合计。它不自动校验“同一费用类型 + 同一发生日期”的明细粒度。
|
||||
- 当前 4 张审批中单据均需退回重做:`0014` 票据关联/金额完整但 5 个日期被压成 1 条明细,B2 无法拦截;`0016`、`0017`、`0018` 可被关联或金额门禁拦住,但日期/粒度仍需人工说明和复核。
|
||||
- 生产现有 admin/employee smoke 可登录,旧 finance smoke 身份已不在 OA 用户表;employee smoke 当前没有自有报销。现有 3 个部门中有 1 个无成员,可在明确授权后作为隔离灰度范围;employee smoke 当前所在部门有 5 个用户,不能直接作为灰度范围。
|
||||
- 材料合并预览会创建短期进程内预览包并写预览审计记录;这是设计内审计写入,必须纳入验收授权,而不是伪装成零写入检查。
|
||||
|
||||
## Scope
|
||||
|
||||
- 创建生产验收条件:把现有空部门固定为常驻 smoke 部门,新建专用 OA-only manager/finance smoke 身份,把 employee smoke 迁入并长期保留,建立专用合成草稿和合成票据;凭据只通过私有 env 注入。
|
||||
- 冻结、推送并严格验证包含 B1/B2 实现与本部署合同的最终候选。
|
||||
- 使用 existing 部署入口完成 migration、seed、应用切换、构建标识、runtime/external smoke、备份合同刷新和角色化公网验收。
|
||||
- B1 验收通过后,在 DB 继续 `force_off` 时显式启用生产 env hard gate 并重启同一候选;随后仅对隔离部门开启 B2,验证通过后再全局开启。
|
||||
- B2 全局开启后,由真实财务角色统一退回 `BX-20260828-0014/0016/0017/0018`;员工按新规则修正并重提,财务人工复核日期粒度。
|
||||
- 已付款 `BX-20260730-0008` 仅留历史关联缺口记录,不退回、不改付款数据;空草稿 `BX-20260805-0013` 不处理。
|
||||
|
||||
## Requirements
|
||||
|
||||
### R1 授权与候选冻结
|
||||
|
||||
- 规划批准、生产验收准备写入批准、push 批准、生产配置批准、部署批准、B2 灰度批准、B2 全局启用批准和真实单据退回批准互不推定。
|
||||
- 本次规划文档提交完成后,以最终干净 `HEAD` 冻结候选;具体完整 SHA 和 12 位 bundle marker 记在外部审批记录,不写回仓库。冻结后任何提交、tracked/untracked 变化或 HEAD 漂移都使审批失效。
|
||||
- push 后必须确认候选是 upstream 祖先,严格 provenance 为 PASS,`published=true`、`remoteVerified=true`、`overrideUsed=false`;禁止 unpublished override。
|
||||
- 固定应用回滚基线为当前生产候选 `59dc5f6`。不得临场选择其他提交。
|
||||
|
||||
### R2 验收身份与合成数据
|
||||
|
||||
- 不恢复已从用户表消失的旧 manager/finance smoke;新建角色纯净的 OA-only manager smoke(manager + employee)和 finance smoke(finance + employee),不绑定或重置真实员工身份。
|
||||
- 把已确认无成员的部门改名为“系统测试专用(勿分配员工)”并固定为常驻 smoke 部门;employee smoke 迁入后不再迁回,manager smoke 设置为其直属 manager,三类 smoke 身份留在该部门供后续隔离灰度复用。迁移前以及每次部门灰度开启前,都必须确认成员集合恰好只有这三个 smoke;部门名称只是防误用提示,不能替代成员集合校验。
|
||||
- finance smoke 持有完整 finance + employee 角色,包含付款和全量财务职责,因此只允许在单独获批的验收窗口内启用。窗口结束后立即置为 disabled;后续再次启用、使用和停用都属于生产账号写入,不得从既往验收授权推定。
|
||||
- 普通 employee 缺直属 manager 时,报销提交会因 `blockOnUnresolvedAssignee=true` 和 `no_manager_found` 返回 409;`owner_self_fallback_role=finance` 只处理 owner 本人持有 CEO 角色的特殊情形,不得把 finance fallback 当作普通 smoke 的兜底方案。
|
||||
- 合成草稿、明细、票据和附件必须使用明显测试标识与非真实材料;不得复制真人票据、金额、说明或附件。
|
||||
- 所有成功提交的合成单据在验证后按合法 owner 流程撤回并删除;失败场景草稿直接删除。审计/工作流历史按系统合同保留,不做数据库硬删。
|
||||
- 材料预览允许产生 `REIMBURSEMENT_MATERIAL_PREVIEW_BUNDLE_ISSUED` / `REIMBURSEMENT_MATERIAL_PREVIEWED` 审计记录;证据只保留状态、页数、页序、比例与构建标识。
|
||||
|
||||
### R3 B1 部署
|
||||
|
||||
- 部署前必须显式记录 migration status,期望 pending migration 只能包含 `20260829120000_reimbursement_expense_policy_invoice_guard`;多、少或漂移均停止。
|
||||
- 使用 `deploy/trialrun/deploy-existing.sh`、existing 数据模式和 `SYNC_ENV=0`。不 reset 数据库,不覆盖环境、DB 或附件目录。
|
||||
- init 后再次显式记录 migration status,并核对:新增列存在、两项补助 `requires_invoice=false`、其他启用类型为 true、费用类型改名/停用/新建准确、`account_code_default` 未被 seed 清空。
|
||||
- 若正式科目表仍未提供,`account_code_default` 保持 null;责任角色为财务负责人,原目标回填日期 `2026-09-05` 保留在父 task,不由开发猜测编码,也不以此阻断本轮材料门禁上线。
|
||||
- B1 首次切换后 B2 DB 记录必须由 seed 显式创建且为 `force_off`,不得以 `missing_flag_record` 代替关闭意图;B2 必须保持 effective off。owner 普通草稿维护、assets 事前批准、2,000 元提示、无票补助和非参与人只读投影均须通过。
|
||||
|
||||
### R4 B2 hard gate 与隔离灰度
|
||||
|
||||
- B1 验收通过后,先确认 DB 为 `force_off`,再经单独生产配置批准把 `REIMBURSEMENT_INVOICE_GUARD_ENABLED=true` 写入受控生产 env,并重启同一候选。重启后应显示 env enabled 但 effective off。
|
||||
- 隔离灰度只能将 DB flag 设置为 `force_on + department_allowlist + 隔离部门 ID`;不得使用 employee smoke 原部门,不得把 4 张真人单据作为灰度样本。
|
||||
- 更新部门 allowlist 前必须重新读取部门成员与状态,确认只有 employee/manager/finance 三个 smoke、无真人成员,且 finance smoke 仅在当前获批窗口临时启用;任何额外成员或身份不确定都阻断灰度。
|
||||
- 每个状态必须记录精确 reason:env 未开为 `env_hard_off`;env on + DB off 为 `flag_force_off`;灰度名单外部门为 `department_not_allowed`;隔离部门在本轮 `force_on` 模式下为 `flag_force_on_department_allowed`;全局开启为 `flag_force_on_global`。不得只凭提交成功/失败推断门禁已运行。
|
||||
- 普通 feature flag 列表使用 `strictDepartment=false`,在部门 allowlist 状态只能给出延后判断,不能证明具体 actor 命中。灰度证据必须运行受控容器内只读 actor/department 诊断,并与报销详情 DTO 的 `invoiceGuardEnabled` 交叉验证;诊断不输出部门 ID、账号或原始 payload。
|
||||
- 隔离灰度必须验证:owner 普通提交成功;两项无票补助不被阻断;多票显式关联成功;超票面准确阻断;旧草稿补齐后可提交;非参与人零写入副作用;切回 `force_off` 后旧提交路径恢复且既有关联数据不删除、不解绑。
|
||||
- B2 不校验日期粒度的事实必须在验收结论和财务退回说明中保留;不得把 flag PASS 描述为“系统已自动保证明细粒度”。
|
||||
- 灰度失败立即把 DB 恢复为调用前完整快照,最迟先落 `force_off`;关闭 flag 只回退门禁,不回滚关联数据。
|
||||
|
||||
### R5 全局启用与真实单据处置
|
||||
|
||||
- 隔离灰度全部通过并清理合成单据后,必须再次获得全局启用批准,才可把 rollout 从部门 allowlist 改为 global;更新使用版本并发控制并产生 feature flag 审计。
|
||||
- 全局启用后先验证 effective global on、owner 基线、财务/管理员只读投影、错误提示和运行指标,再安排真实财务账号退回 4 张单据。
|
||||
- `0014` 退回说明必须写死“按 5 个发票日期拆成 5 条明细”,重提后由财务人工确认;B2 不会自动拦截原样重提。
|
||||
- `0016` 至少按 3 个发票日期拆分并逐票关联;`0017` 按 2 个发票日期重做、上传正式 PDF、逐票关联并解释 `1,949.81 元`差额;`0018` 若同日同类可保留一条,但须改正日期并关联全部 5 张票。
|
||||
- 退回必须由真实 finance 审批主体执行。admin、smoke admin 或脚本不得代行财务职责;Codex 不保存真实财务凭据。
|
||||
|
||||
### R6 失败与回滚
|
||||
|
||||
- 切换前失败只停止,不执行应用回滚。migration/seed 或应用切换开始后任一必需门禁失败,先确保 B2 DB `force_off`,再按同一 existing 入口回滚应用到 `59dc5f6`。
|
||||
- 应用回滚不逆向 migration、不删新增列、不删除票据—明细关联。新增列对旧应用为向前兼容。
|
||||
- init 已改变费用类型主数据,应用回滚不会自动恢复旧名称/启用状态。部署前必须保存受影响类型的脱敏主数据快照;只有 B1 主数据本身被判定错误且取得单独补偿批准时,才按快照执行精确补偿,不运行整库回滚或重放猜测 SQL。
|
||||
- 回滚后必须核对 `/ready`、bundle marker、migration status、runtime/external smoke、DB/附件挂载和备份合同;若 env hard gate 已写入,可保持 true,但 DB 必须 `force_off`。若回滚原因涉及 flag 配置或新代码误触发,则同时恢复 env hard-off 并重启旧应用。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] 常驻部门已改名为“系统测试专用(勿分配员工)”;专用 manager/finance smoke、employee smoke 迁入和直属 manager 关系经单独生产写入批准,成员集合只有三个 smoke且不涉及任何真实员工身份或材料。
|
||||
- [ ] 最终候选冻结、单独 push、upstream 祖先关系和严格 provenance 全部通过。
|
||||
- [ ] 用户在 provenance 通过后另行批准 B1 生产部署;pre/post migration status 均明确记录,唯一 migration 正确应用。
|
||||
- [ ] B1 schema、费用主数据、requires_invoice、assets 批准依据、2,000 元提示和 account code 保留检查通过;B2 DB 记录明确存在且为 force_off,effective off reason 不是 missing record。
|
||||
- [ ] `/ready`、12 位 bundle marker、runtime/external smoke、备份合同刷新和角色化公网检查通过。
|
||||
- [ ] owner 合成草稿维护正常;finance/admin 非参与人无维护入口;材料合并预览成功并只产生获批的设计内审计。
|
||||
- [ ] env hard gate 在 DB `force_off` 状态下单独启用并重启,确认 env on / effective off 后才进入隔离灰度。
|
||||
- [ ] B2 隔离灰度七项合同全部通过;失败时 DB 快照恢复,既有关联数据未回滚。
|
||||
- [ ] 用户单独批准后 B2 全局 effective on;合成数据已按合法流程清理,employee/manager/finance smoke 常驻隔离部门且无真人混入,finance smoke 已在验收窗口结束后 disabled。
|
||||
- [ ] 真实财务按明确说明退回 0014/0016/0017/0018,重提后人工复核日期粒度;0008 与 0013 未被误改。
|
||||
- [ ] 任一失败按固定回滚/补偿边界处理,无 reset、无逆 migration、无批量伪造批准依据或票据关联。
|
||||
- [ ] 所有公网门禁完成前不通知普通用户测试、不归档父任务。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 修改 B1/B2 业务代码、把日期粒度新增为服务端阻断、补数量/单价模型或提前实施 B3 一页式。
|
||||
- 反向修改已付款 0008、替真人财务审批、保存真实凭据、复制真实票据作为测试材料。
|
||||
- 在本轮编造会计科目编码、重置数据库、回退 migration、批量写入事前批准依据或清空审计记录。
|
||||
- 把合成灰度批准推定为真实单据退回批准,或把部门灰度 PASS 推定为全局启用批准。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- 来源:干净工作区、候选 SHA、独立 push、upstream 祖先关系、严格 provenance。
|
||||
- 生产 preflight:existing 部署合同、身份、数据挂载、唯一 pending migration、受影响主数据快照、B2 env/DB 初始状态。
|
||||
- B1:init 前后 migration status、schema/seed/account code 断言、build marker、runtime/external smoke、备份合同和角色化页面。
|
||||
- B2:env on + DB off、隔离部门 allowlist、七项合成业务合同、flag 快照恢复、关联数据保留、全局启用后复核。
|
||||
- 真实业务:四张单据退回说明逐单核对,重提后财务人工核验日期粒度;0008/0013 零改动。
|
||||
- 治理:task validate、Trellis governance、process contract、docs registry audit、敏感信息扫描和 `git diff --check`。
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "reimbursement-expense-policy-invoice-guard-deploy",
|
||||
"name": "reimbursement-expense-policy-invoice-guard-deploy",
|
||||
"title": "报销费用政策与票据门禁生产部署",
|
||||
"description": "受控部署 B1 migration 与费用主数据,在隔离部门验证 B2 后全局启用,并统一处置四张审批中报销。",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": "production deployment, migration, feature flag rollout, smoke identities, reimbursement remediation",
|
||||
"package": "deploy,server,client,trellis",
|
||||
"priority": "P0",
|
||||
"creator": "shunli",
|
||||
"assignee": "shunli",
|
||||
"createdAt": "2026-08-30",
|
||||
"completedAt": "2026-10-11",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": "08-29-reimbursement-expense-policy-invoice-guard",
|
||||
"relatedFiles": [
|
||||
"deploy/trialrun/deploy-existing.sh",
|
||||
"deploy/trialrun/deploy.sh",
|
||||
"server/prisma/migrations/20260829120000_reimbursement_expense_policy_invoice_guard/migration.sql",
|
||||
"server/src/scripts/seed-config.js",
|
||||
"server/src/services/featureFlags/index.js",
|
||||
"server/src/routes/reimbursements.js"
|
||||
],
|
||||
"notes": "2026-10-11 报销真实OCR余项及财务科目映射移交09-08-reimbursement-payment-closeout的RC-OCR-1–4/RC-ACCOUNT;原部署清单无逐项执行证据,未补勾。后续发布已包含父任务实现,不虚报按本任务旧冻结/灰度步骤执行。旧回滚/迁移参数不用于下次发版。待归档授权,未提交、未查生产。以下为历史阶段说明:\n规划阶段;不执行未单独授权的 push、生产账号/部门/合成数据写入、migration、部署、env 配置、feature flag 更新或真实单据退回。固定应用回滚基线为 59dc5f6。B2 不校验日期粒度,0014 必须依赖退回说明和财务人工复核。employee/manager/finance smoke 终态常驻‘系统测试专用(勿分配员工)’部门;每次灰度前成员集合必须恰好为三个 smoke。finance smoke 含付款能力,仅在获批验收窗口临时 active,窗口外 disabled。普通 employee 无直属 manager 时 workflow 会以 no_manager_found 阻断。B2 DB 记录必须显式存在为 force_off,灰度必须记录 actor 级精确 reason。",
|
||||
"meta": {}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户