feat: 选票自动生成报销明细并支持集中分类(08-29-reimbursement-expense-policy-invoice-guard)
这个提交包含在:
@@ -1,5 +1,79 @@
|
||||
# 设计:报销费用口径与票据明细门禁
|
||||
|
||||
## 最新合同:选票建明细与待补分类(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 在真实或专用数据上验证后开始。
|
||||
|
||||
@@ -1,5 +1,156 @@
|
||||
# 实施清单:报销费用口径与票据明细门禁
|
||||
|
||||
## 当前接续点:只读复核与发布候选准备
|
||||
|
||||
### 用户复核与提交授权(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 默认关闭。
|
||||
|
||||
@@ -2,6 +2,41 @@
|
||||
|
||||
## 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
|
||||
|
||||
在新工单中引用
屏蔽一个用户