diff --git a/.trellis/spec/hyb-oa/backend-fastify-prisma.md b/.trellis/spec/hyb-oa/backend-fastify-prisma.md index 9c7ed75b..3b529615 100644 --- a/.trellis/spec/hyb-oa/backend-fastify-prisma.md +++ b/.trellis/spec/hyb-oa/backend-fastify-prisma.md @@ -127,6 +127,31 @@ QA script patterns over new abstractions. ### Receipt OCR Matching Service Boundary +- Ordinary bank-payment evidence must retain a versioned `document` classification + (`bank_receipt`, `non_final`, `unknown`) in the encrypted recognition/proposal. + Bound local PDF inspection by input size, page count, worker memory and time; + unreadable text remains unknown. Preserve provider disclaimers as restrictions. + A title, matching fields, or absence of an extracted disclaimer is not sufficient. + Automatic matching needs a supported final receipt, explicit successful payment + and no incoming direction or conflicting document facts. Keep this gate in + evidence construction and final reconstruction, not the shared `matchBankSlip` + helper used by failed/voided/terminal-fact comparisons. +- Manual `paymentProof` must attest a checked original and record the basis/note. + `booked_debit` requires a valid non-future bank booking date and an empty original + success status; preserve `transactionStatusRaw=null` rather than fabricate bank + wording. New proof always needs the batch's applicable confirmation mode. + Non-final disclaimers, incoming direction and known contradictory facts cannot + be removed by manual corrections. Manual-source evidence is not automatic OCR. +- Old unconsumed evidence without document inspection cannot default to final. + Finance may explicitly POST `receipt-attachments/:attachmentId/document-check` + with `expectedVersion`; use the existing batch/claim/attachment locks, scope and + maker checks. Verify stored bytes against size/SHA, create a new version and audit + atomically, preserve history and never call cloud OCR. Old manual approval does + not transfer to the new version. Reinspect and settle under the same batch lock; + runtime tests must cover both orders, stale versions and audit rollback. + A historical OCR result with zero known pages must still expose the inspection + action; reject malformed/out-of-range counts without allocating from them. + - Failed evidence persistence is not OCR failure. In a separate bounded transaction, lock the batch then conditionally update the attachment by version/SHA/processing and active governance predicates. Persist only allowlisted system_failed, diff --git a/.trellis/spec/hyb-oa/frontend-react-vite-design.md b/.trellis/spec/hyb-oa/frontend-react-vite-design.md index c8eb47ca..00701215 100644 --- a/.trellis/spec/hyb-oa/frontend-react-vite-design.md +++ b/.trellis/spec/hyb-oa/frontend-react-vite-design.md @@ -10,6 +10,17 @@ ## Receipt Verification Dialog +- Show document nature separately from extracted transaction status. A booked + official debit receipt may use the actual booking date and checked-original + declaration; do not ask the operator to invent a successful bank status. + Show that basis to the reviewer and keep save/review separate from settlement. + Non-final receipts or contradictory directions have no manual override. +- Old evidence needing local document inspection exposes an explicit finance + action in the existing dialog; retain history and explain the need to recheck. + Reset document-proof choices along with corrections on refresh. Verify the + actual desktop/mobile proof entry, required original check, applicable reviewer, + final settlement and non-final refusal through the rendered dialogs. + - When readiness is unmet and an active receipt has a system/key/snapshot failure, the batch banner must direct prerequisite repair, not an unavailable verification dialog. This is display only: a retained failed attachment must not block other diff --git a/.trellis/tasks/09-17-company-loan-principal-repayment/design.md b/.trellis/tasks/09-17-company-loan-principal-repayment/design.md index 861b05c8..e8ad169a 100644 --- a/.trellis/tasks/09-17-company-loan-principal-repayment/design.md +++ b/.trellis/tasks/09-17-company-loan-principal-repayment/design.md @@ -1,3 +1,36 @@ +## 2026-10-08 实样发现后的凭证处理方案(N1–N2 已本地实现) + +本节对应 PRD 顶部 R4/AC5 补充及 implement 的 N1–N2,用户已明确“开始执行”。沿用已实现的两人关联模式、回单存储、人工核对和核销流程;不重新设计 S1–S8/SR,不把 S002 专项格式兼容纳入必修范围。 + +实现收敛:本地使用 PDF 文本解析读取票面限制,无法可靠解析的维持未知;新增的结构化凭证依据随既有加密核验/人工提案保存。人工可按正式回单的已记账支出和记账日期确认,不生成虚假的银行状态原文。旧证据通过有权经办显式发起的本地重检生成新版本,保留历史、不调用云 OCR;最终资金动作不默认接受缺少新版依据的旧记录。 + +本轮采用现有表,无新增迁移。新增 `paymentReceiptDocument` 服务和受限 PDF 文本工作线程,使用 `pdfjs-dist`;单文件最多 5 MB、20 页,工作线程限时 5 秒、旧生代内存上限 128 MB。结构化凭证类型为正式回单、非最终回执或未知,原始票面文字不落日志;识别异常维持未知,不能自动核销。 + +人工依据分“原件明确成功”和“正式回单已记账支出”。后者须填写真实且不晚于当天的记账日期、原件已核对声明及依据说明,保存 `transactionStatusRaw=null`;新增人工依据必须按该批次模式确认。既有非最终说明、贷方入账、多个回单或失败/处理中矛盾均不能由人工覆盖。自动凭证检查位于核验构建与最终重建,未放入失败/终态事实也使用的低层字段匹配函数。 + +旧记录显式重检接口为 `POST /payment-batches/:id/receipt-attachments/:attachmentId/document-check`,要求当前版本、财务权限和经办资格,核对原件实际大小与摘要、未被消费,再在同一事务新增核验版本与审计;旧人工确认不得转移到新版本。历史 OCR 为 0 页时详情仍可打开重检入口,负数、超上限等无效页数继续拒绝。已核销历史不自动重写。实现及验证证据见 implement 顶部;独立代码复核和真实还款闭环不在本轮完成结论内。 + +### 凭证性质、字段读取、交易归属分别判断 + +1. 凭证性质先区分已确认的最终付款依据、明确非最终依据、尚不确定。银行限制说明、实际记账方向和入账/付款信息属于判断依据;文件名、标题、OCR 请求成功或没有提取到免责声明均不足以判为最终依据。公司贷方借入和借方还本分开处理。 +2. 自动路径只开放经过实际材料核验的格式与字段组合;识别来源必须能覆盖判断凭证性质所需内容。当前 `BankSlipOCR` 的六类归一化字段不能证明完整覆盖票面说明。可在本地核对 PDF 文字,缺文字/提取异常保持未知;本阶段不以接入新的云服务或实现所有扫描件 OCR 为前提。 +3. 人工路径在原件实际信息足够时使用。沿既有财务录入、马经理关联方确认,补充可追溯的凭证性质及已记账依据,不补造成功状态。明确非最终的回执不能经普通人工决定升级;需要取得对应正式付款证据。没有可用最终证据则保持待核对,不能以“以后会补回单”先核销。 +4. 凭证足以证明付款也不自动证明属于哪个批次。金额、币种、双方账户、付款方向、银行交易身份及既有候选/提交关联继续分别检查;回单编号、客户方流水号和网银指令号分开保存或引用,不靠人工照抄使之相等。编号不足时保持待对账。 + +### 依据留存、旧数据和最终消费 + +- 在既有证据模型上留存原件摘要/版本、凭证判断及其依据、自动或人工来源、决定上下文;核验结果持久化、人工冷重建和最终核销都必须保留并重验这些信息。不能只在上传时或页面增加提示。 +- 换件、重新识别、作废和候选变化继续使旧依据失效。旧核验记录缺少凭证判断时不能默认通过;已记账记录保持历史不可变,需要核查的问题另行登记,不自动撤销本金核销或重写生产数据。 +- 本轮凭证依据随现有加密核验和人工提案留存,未新增迁移。涉及共享回单服务时回归报销消费者,既有独立性、交易唯一性、失败/受理与资金约束保持。 +- 验收覆盖明确非最终说明、字段中出现成功但票面有限制、无法完整判别、正式凭据没有字面成功状态、借入/支出方向不同、旧结果缺依据、人工重建及最终核销。去除关键凭证检查后,测试必须能区分错误放行或合同退化。 + +### 实际材料与完成边界 + +- 原四份获授权 PDF 和 OCR 配置读取范围继续有效,无需为相同范围重复授权;先复用既有结果和本地材料,不无目的重跑云端。新增两份正式借入回单目前只做了本地读取;确有必要发送新文件时再明确文件范围。 +- S001/S003 作为非最终凭证反例;S002 维持既有需复核结果,可用于材料性质/人工依据分析,不要求兼容其全部 OCR 字段,也不为它索取保证金退款材料。新增两份借入回单用于本金及格式核对,不能充作还本付款正例。 +- 优先用现有材料验证原件字段与人工处理是否可行;只有一个具体交易的完整归属无法核对时,才列明需要该笔交易的正式支出回单及网银实际提交详情。现有无真实借款还款时,不要求制造付款获取样本;借款全链用合成数据,首笔真实银行闭环在获准实际还款后验收。 +- 正常办理必须有一条经验证可用的自动或人工路径;仅能拦截不代表可启用。首笔前的软件/材料验证与首笔后的实际银行闭环分别记录。异常缺实样保持未验,不提前认定失败释放等已具备真实银行接受性,缺证继续占用。 + ## 2026-10-08 财务经办与马经理关联方确认方案(本地实现及合成验证完成,已授权本地提交) 用户已确认马经理有权代表公司批准,并在规划完成后明确“开始实施”。马经理同时为股东和个人出借收款人;技术员工不参与复核、审批、见证,不增加权限或分派待办。公司授权这一业务事实不再待决。本节是本次权限模式变更的执行合同;本地实现及合成验证已完成,具体证据与剩余验收见 implement 顶部。下方历史设计中的独立复核,除 Q6 外按本节明确区分模式;本地改动不等于生产已具备该模式。 diff --git a/.trellis/tasks/09-17-company-loan-principal-repayment/implement.md b/.trellis/tasks/09-17-company-loan-principal-repayment/implement.md index 360368a5..de2ba16a 100644 --- a/.trellis/tasks/09-17-company-loan-principal-repayment/implement.md +++ b/.trellis/tasks/09-17-company-loan-principal-repayment/implement.md @@ -1,3 +1,90 @@ +## 2026-10-09 N3 本地提交与新候选验证(独立代码复核待完成) + +用户在 N1–N2 完成回执后明确“继续下一步”,本轮按已说明的 N3 接续本地提交和新候选验证。提交范围为已核对的 31 项改动;源码、测试及规约与上一轮最终清单逐项摘要一致,本轮仅补本节和 PRD 接续记录。提交、验证的实际结果以本轮回执及 Git 记录为准,不提前记为通过。 + +- 单独形成一个本地提交:`完善付款回单凭证核验与本地重检(09-17-company-loan-principal-repayment)`,包含 27 个修改文件、4 个新增文件;不加入输入材料、本地验证产物或个人日志。提交说明保留“独立代码复核及真实还款验收待完成”,不推送、不部署。 +- 复用 N1–N2 已通过且输入未变化的针对性结果;完成本轮任务/流程/敏感/差异检查后提交。新提交固定后运行 `bash scripts/qa/run-release-gate.sh verify-candidate`,其范围为 static 和完整受管 full-local;不调用发布入口,不以旧候选或限定重验替代。 +- 本轮回执固定为 `artifacts/release-gate/loan-receipt-n3-20261009/`,准备与清理记录为 `artifacts/qa-flow/loan-receipt-n3-20261009/`。测试期间冻结源码、测试和任务文档;结果、当前候选及复核交接记录写入本轮回执,不修改正在验证的提交。 +- 本轮使用专用的临时浏览器目录和已有制单 Python 依赖环境;完整验证由既有受管入口建立新数据库及隔离网络,使用合成账号/材料。验证结束后只清理本轮拥有的临时资源,不读取真实 OCR 配置、不外发银行文件、不查询生产。 +- 当前 developer 的 inline 指令明确禁止派 implement/check 子代理。主会话已完成可执行检查,但独立代码复核仍未完成,已有旧阶段代理报告不能覆盖关联模式和 N1–N2;保持独立复核、真实还款、生产启用的开放状态,不因本地提交或候选门禁通过而勾选整体任务完成。 +- 门禁如发现本任务问题,在既有范围内修复并做相关验证,再形成新的本地修复提交与对应回执;保留失败证据,不修改原提交、不忽略失败或缩减正式门禁范围。N4 的生产查询、推送、部署和 N5–N6 的真实业务/付款不在本轮范围。 + +下方“未提交”等为 2026-10-08 完成时点;本轮候选与验证结论以以上回执为准。 + +## 2026-10-08 实际材料核对后的后续计划(N1–N2 本地完成,未提交) + +用户先要求重新梳理规划,随后批准 N1–N2 本地实施、针对性验证和主会话检查。实施基线为已提交的 `ce11dea9`,其 `verify-candidate` 为 PASS:82 迁移、服务端 4519 通过/1 项既有可选跳过、浏览器 372 通过且无失败/跳过/重试;基线回执见 `artifacts/release-gate/loan-ce11dea9-20261008/`。本轮实现和验证见下方完成回执,旧候选门禁不覆盖本轮差异。没有本轮提交、推送、部署、生产访问或真实业务写入;任务仍为 `in_progress`。下方其他阶段状态保留为历史时点。 + +### 已完成及无需重复索取 + +- [x] A/B/C、S1–S8、SR 补证恢复、电脑/手机界面和两人关联模式已本地实现并提交,不再按未开发项目排期。 +- [x] 两笔正式借入回单本地核对完成;用户已确认历史未还本金、本年无新增借还、开卡小额往来排除及沿用原个人收款账户。实际值见受控材料,不抄入版本库;真实 OA 台账尚未登记。 +- [x] 财务经办、马经理公司代表权及关联身份已确认;技术员工不参与业务审批/复核/见证。关联 Q6 关闭,不再将第三人或查询无结果 PDF 列为普通还款前提。 +- [x] 原四份获授权 PDF 已做真实 OCR 实测。S001/S003 字段匹配但为非最终回执;S002 保持需复核、专项适配后置。两份新借入原件为正式回单,不代表未来支出。没有执行真实核销或银行验真。 + +### 下一阶段顺序与完成条件 + +| 顺序 | 执行内容 | 完成条件及依赖 | 人工配合 | +|---|---|---|---| +| N1 凭证判断与最小修复 | 已实现正式回单、非最终回执及未知分类;自动核验、人工记账依据、旧证据本地重检、证据重建和最终消费共用有效依据 | 本地完成。非最终和未知不得自动核销;人工不得覆盖已知限制或补造银行成功文字,资金反例与变异通过;无新增迁移 | 现有余额、账户、人员不重问;当前无需新增人工配合 | +| N2 正常路径及材料预验 | 已复用六份材料本地核对,并以一份既有正式支出样本做真实字段的内存人工录入预验;隔离环境完成财务与经理的自动/人工流程、提交记录及拒绝场景 | 本地预验完成;合成借款可实际核销,电脑/手机均实操。历史样本只证明其原交易与格式,未证明真实借款还本及网银提交对应;真实闭环仍待 N6 | 当前不用重新下载材料或制造交易。首笔真实还款时保留本笔提交详情与正式付款回单;新增文件外发 OCR 才需明确新增文件范围 | +| N3 独立代码复核与候选验证 | 固定最终差异及证据,完成独立只读复核和修复后的针对性验证;获准本地提交后对干净新候选运行正式门禁 | 依赖 N1/N2;范围覆盖当前关联模式以及本轮凭证变化。记录复核来源与结论,当前 inline 主会话检查不替代独立复核。源码变化后不能沿用 `ce11dea9` 回执 | 不让公司技术员工做业务复核;独立审查执行条件不足时如实列明。提交另按明确授权办理 | +| N4 发布准备及获准发布 | 准备候选、真实迁移差集核实方案、数据库及附件快照、兼容处置和部署后电脑/手机检查;获准后只读复核生产,再按批准候选推送部署 | 依赖 N3 与正常路径可用;2026-10-07 空存量不是发布前现状,73→82 只是历史推算,实际差集现场核实。保留未验异常边界;产生关联决定后不能直接降级到旧语义代码 | 生产只读查询、推送、部署分别取得明确授权。独立备份切换仍按原范围处理,不能借安装脚本发布整个 main | +| N5 真实台账及授权登记 | 财务按已有原件和用户确认登记两笔历史借入、未还本金、核对截止日、双方账户及会计引用;登记已有公司代表权和覆盖借款,由马经理在 OA 确认 | 依赖已获准可用环境和真实业务录入授权;逐笔核对与现有会计负债记录一致,不重复生成历史现金流或会计凭证,不把 OCR 结果当成余额确认 | 财务实际录入并核对,马经理确认授权范围及相关声明;不重复向用户索取已确认事实,不要求技术员工参与 | +| N6 首笔真实还款及会计交接 | 确认本次金额/分配/日期,财务申请,马经理关联方批准,财务导出、在网银实际提交、留存提交详情与正式付款回单,核验/必要人工确认后核销,交代账留存 | 依赖 N5 及本笔真实付款批准。实际支付仍由有权人员在网银操作;只核销本次确实付款的本金。首笔成功后才能记录真实银行闭环验收;不把软件预验写成真实还款已通过 | 到办理时确认本次还多少及付款安排;不从剩余本金推定全额付款。保留银行指令详情、正式回单和代账交接结果;异常未知不重付 | + +N1–N2 已按授权完成本地实施和验证;后续进入 N3 的独立代码复核、本地提交与新候选验证,再按 N4–N6 办理发布和真实业务。复核交接范围已列在本轮完成回执中;当前 inline 主会话不能代替独立复核。提交、发布及真实付款不包含在本轮实施授权里;已明确批准的同范围工作执行时不再逐步询问。 + +### 验收与取样纪律 + +- 原件类型、OCR 字段、交易归属、最终核销分别记录。现有实测止于匹配函数,不宣称已复现生产错误核销。正式回单没有字面成功状态的问题须按实际记账证据设计,不能以人工补写状态通过。 +- N1 先跑相关回单/OCR/人工重建单测及借款隔离 HTTP/数据库资金反例;涉及页面再跑相应电脑/手机操作。覆盖共享报销路径,重点反例配有效变异。新候选形成后再运行完整正式门禁,本轮定向结果不代替该门禁。 +- 真样本不足的异常路径继续标未验,保持银行事实、占用及冻结;不得用异常“人工说没付”绕过。若上线前仍不能验证一条正常最终付款路径,N4 不进入真实启用。 +- 不再要求补本年完整银行流水、重新核实已确认借款是否还本、S002 保证金退款材料或重复下载同格式回单。确需补材料时明确具体交易、材料名称、缺失字段、取得方法和用途。 +- 仅在现有材料无法回答本次需要时再问银行资料问题;普通 OCR 不以前置咨询银行为条件。新增文件云 OCR 授权、生产业务访问及真实写入与原四份样本授权分开。 + +### 规划及实施检查 + +- [x] 已重新核对当前候选回执、四份样本实测、本地新增回单分类及用户确认;没有新增云 OCR 调用或读取生产配置。 +- [x] 规划阶段的任务校验、Trellis 治理(含敏感扫描)、流程合同 baseline 和 `git diff --check` 通过;文档注册检查零错误,保留原有“22 个 docs 未登记”的一条汇总告警。该时点仅三份既有任务文档新增内容,历史正文完整保留,尚未运行应用测试;实施后的验证见下方完成回执。 +- [x] 用户明确“开始执行,需要我手动操作的及时告诉我”,批准 N1–N2 本地实施与针对性验证。实施期间需要人工配合时说明具体步骤和用途;不重复索取已确认事实或原四份样本授权。 +- [x] N1:凭证识别及保存、自动/人工核验、旧记录本地重检、最终消费与页面接续。 +- [x] N2:现有材料的受控本地核对、隔离资金与电脑/手机回归、定向变异、主会话检查及真实验收边界记录。无提交、推送、部署或真实数据写入授权。 + +### N1–N2 本轮完成回执 + +本轮 31 项改动为 27 个已跟踪文件修改及 4 个新增文件,均未提交。新增受限本地 PDF 凭证解析服务/工作线程、其单测和带真实可提取文字的合成回单工具;服务端新增 `pdfjs-dist` 依赖,继续使用部署及 CI 既有 Node 22。没有新增迁移,隔离数据库仍验证 82 个迁移。 + +- 正式回单、非最终回执、未知分类随加密核验证据保留。构建核验和最终消费均拒绝非最终、入账方向、已知状态矛盾和缺少依据;失败/终态流程共用的低层字段匹配规则未被付款凭证门禁替换。 +- 正式回单没有字面成功状态时,可依据原件记账支出、实际记账日期和原件已核对声明提出人工核验。必须由该批次适用的确认人确认;关联模式仍为财务经办与马经理公司代表确认。保留原始状态为空,不生成虚假的“交易成功”。 +- 旧核验缺少凭证性质时,财务在原弹窗显式更新,校验原件摘要/大小及版本后新增核验与审计,不发送云 OCR、不改历史或沿用旧人工批准。重检和最终核销按同一批次锁串行;审计失败整体回滚,已消费证据不可重检。 +- 主会话检查补齐历史 OCR 零页结果的详情兼容:零页仍可进入本地重检,负数、超过 20 页或异常大页数拒绝;新增 4 项路由反例。此项仅改善详情入口,不放宽最终资金判断。 +- 合成 PDF 票面金额、账户及银行编号与对应的模拟 OCR/人工录入一致。测试沿真实解析、归一化、加密核验、人工重建及核销,仅替换云端识别返回;不是只向匹配函数塞入“成功”结果。 + +验证证据统一位于 `artifacts/qa-flow/loan-receipt-evidence-20261008/`;各行是不同范围,不累计为独立测试总数: + +| 验证 | 结果与准确范围 | 回执 | +|---|---|---| +| 完整借款 HTTP 与数据库 | 116 组 HTTP、9 组数据库约束、82 个迁移通过;含 21 组关联/本轮回单场景、拒绝后本金占用不变、旧自动/人工记录重检和两种确定性交错 | `loan-http-final.log` | +| 关联模式电脑/手机实操 | 各 9 组,共 18 组通过;实际填写记账依据/日期、必须勾选原件核对、公司代表确认、最终核销,非最终回执无手动覆盖或核销动作;本轮运行明确未跑 HTTP | `loan-ui-final.log`;截图索引在该日志 | +| 服务端全量 | 228 文件、4554 项通过,1 项既有可选跳过。随后仅改进合成 PDF 数据一致性和零页详情兼容,对应以下定向/HTTP/浏览器回归;不将新增 4 项虚报为已经重跑的全量数字 | `server-unit-final.log` | +| 最新回单及报销路由定向 | 7 文件、292 项通过,覆盖上述最后改动 | `receipt-targeted-latest.log` | +| 共享回单数据库及真实认证链 | 38 组通过,使用合成账号经真实密码/JWT;审核、读取、重复交易、事务及并发受约束 | `receipt-db.log` | +| 共享报销页面限定重验 | 24 项通过,受管环境 PASS,电脑/手机回单交互及实际报销付款链通过;不是整站正式门禁 | `web-shared-ready.log`、`web-shared-ready/results/` | +| 资金检查变异 | 基线通过,9 个不同守卫全部被具体断言捕获;对最终合成数据又复验同一组 9 个,不累计为 18 个守卫。临时副本测试,当前源码摘要未变、副本已删除 | `mutation-final/mutations.json`;首轮 `mutations.json` 保留 | +| 静态及合同 | Prisma 校验、前端类型检查/构建、错误码合同、UI 规则、敏感扫描、任务和流程/文档检查通过;最终文档检查回执见同目录 `checks-final.json` | 同目录检查日志 | + +早期失败证据保留:`server-unit.log` 的 6 项制单测试因未指向现有 Python 依赖环境失败,指定既有环境后全量通过;`web-shared.log` 为缺 WebKit 导致的环境无效,使用本轮专用浏览器后受管重验通过;`loan-full.log` 已通过 HTTP 后在浏览器错误期待“禁用按钮”,产品实际隐藏该动作,改成验证不存在后相关电脑/手机 18 组通过。不得将这些早期日志记作整轮 PASS,也未无原因重跑已通过的无关测试。 + +### 实样预验、复核交接及待人工事项 + +- 六份已有材料均本地读取且原件摘要未变:S001/S003 为非最终回执,S002-A 为正式支出回单,S002-B 凭证性质未知,两份借入回单为正式入账回单。S002 仍仅用于格式预验,不作为借款或还本事实,专项 OCR 兼容继续后置。回执 `real-document-check.json`。 +- S002-A 按原件实际金额、币种、双方账号、编号及日期,在内存中构造已记账支出的人工核验提案成功,要求后续业务确认,未编造成功状态。没有建立真实 OA 批次、真实批准或核销;未验证其银行提交对应或真实借款还款。原始字段没有写入报告,回执 `local-manual-proof-check.json`。 +- 本轮新增云 OCR 调用为 0,未读取 OCR 配置或生产数据、未写真实台账、未付款。原四份样本既有 OCR 授权不扩大到新增借入文件;本地预验无需外发这些文件。 +- 主会话按本任务验收检查了全部改动,包括 4 个未跟踪新文件,未发现尚未处理的本轮阻断;当前 inline 模式不能派 implement/check 子代理,**独立代码复核仍未完成**。N3 需同时复核关联模式与本轮差异,重点为原件限制/人工依据、旧证据重建、权限与锁序、共享报销消费者、页面及 9 项变异证据;不能仅检查 `git diff` 而遗漏新增文件。 +- 当前无需用户补下载材料、询问银行或要求技术员工参与。获准上线和真实录入时,财务用已有凭据登记历史本金及公司授权,马经理在 OA 确认;首笔真实还款前再明确本次金额/分配,财务实际网银提交,保留该笔提交详情及正式付款回单。到对应步骤再给出操作指引,不从余额推定全额付款。 +- 本轮独立代码复核、明确授权的本地提交、新候选正式门禁、获准发布和首笔真实还款闭环均未完成;AC5、AC8、AC27 及整体真实业务验收保持开放。没有推送、部署、远端备份切换、生产查询、真实写入或付款。 + ## 2026-10-08 关联方模式本地提交与正式验证(已授权) 用户明确答复“是,授权”,同意将本轮 39 项改动作为一个完整的本地提交,并对干净的新候选运行正式验证。提交范围为 33 个修改文件和 6 个新增文件,含一个新增迁移;文件摘要已与上一轮收尾清单核对一致,本次只补授权及验证安排的记录。 diff --git a/.trellis/tasks/09-17-company-loan-principal-repayment/prd.md b/.trellis/tasks/09-17-company-loan-principal-repayment/prd.md index ec571ca7..726950b6 100644 --- a/.trellis/tasks/09-17-company-loan-principal-repayment/prd.md +++ b/.trellis/tasks/09-17-company-loan-principal-repayment/prd.md @@ -1,5 +1,32 @@ # 公司借款台账与本金归还 +## 2026-10-09 N3 本地提交及新候选验证接续 + +用户明确“继续下一步”,沿已列明的 N3 处理 N1–N2 的 31 项本地改动:核对上一轮验证清单、补任务记录、单独本地提交并对干净的新候选运行 `verify-candidate`。范围、命令及失败处置见 implement 顶部;本轮实际候选及结果以 `artifacts/release-gate/loan-receipt-n3-20261009/` 为准,不沿用 `ce11dea9` 的通过结论。 + +当前 inline 指令禁止派 implement/check 子代理;独立代码复核与真实还款验收继续待完成。提交和候选验证不代表生产启用,未授权推送、部署、生产访问或真实业务写入。下方 N1–N2“未提交”为上一轮历史时点;整体任务保持 `in_progress`,AC5、AC8、AC27 等开放条件不提前勾选。 + +## 2026-10-08 实际银行材料核对后的接续(N1–N2 本地完成,未提交) + +用户先要求重新梳理后续任务,随后明确“开始执行,需要我手动操作的及时告诉我”,批准 N1–N2 的本地实施、针对性验证及主会话检查。目标是形成可证明实际付款的回单处理路径,再完成独立代码复核、获准发布和首笔真实还款;执行顺序见 implement 顶部,凭证处理方案见 design 顶部。提交、推送、生产操作及真实付款不在本次范围。 + +N1–N2 已完成本地实现及可执行验证:正式/非最终/未知凭证区分、无字面成功状态的已记账支出人工核对、旧证据本地重检、最终核销及电脑/手机交互已贯通;原件限制与交易唯一性不能由人工覆盖。合成借款链和真实历史材料的内存预验分别留证,详细范围见 implement 顶部。新增 31 项本地改动尚未提交,无新增迁移;独立代码复核及真实银行闭环仍待完成。 + +- 实施基线为已提交候选 `ce11dea9`,关联方模式正式本地门禁已通过:82 个迁移、服务端 4519 项通过及 1 项既有可选跳过、浏览器 372 项通过且无失败/跳过/重试。证据为 `artifacts/release-gate/loan-ce11dea9-20261008/release-result.json` 及同目录 `local-followup.json`,不覆盖本轮未提交的凭证变化。本地 main 比本地 origin/main 多 13 个提交;未推送、未部署。 +- 两笔历史借入的正式银行回单已做本地文字核对;用户已确认均未归还本金、本年无新增借还、开卡小额往来已归还且不计借款,并确认沿用原个人收款账户。具体金额、账号及原件仅在受控材料中保留。上述事实不再重复提问;财务正式录入时仍须留存原凭据、已有会计记录引用及核对截止日,不把历史借入当成当天新增收款。 +- 财务经办、马经理代表公司作关联方确认;技术员工不参与业务复核、审批或见证。代表权不再待决,真实授权及范围尚未写入 OA。关联模式 Q6 继续关闭,不要求补足第三人或取得无结果查询 PDF 来办理正常还款。 +- 获准的四份 PDF 此前已完成真实 OCR 调用。其中 S001/S003 虽字段匹配,但原件注明不作为最终交易依据;此前实样只执行到本地匹配函数,不能称为已复现生产错误核销。本轮借款完整链只用合成数据;另用 S002-A 的正式支出原件做内存人工核验预验,证明该格式可以留存记账依据,不代表其属于借款或已完成真实还本。S002 专项 OCR 适配仍后置。新取得的两份正式借入回单证明的是历史入账,不能充当未来还本的支出回单;本轮没有新增云 OCR 调用。 +- 独立代码复核、正常付款凭据及提交归属实样验收、真实业务录入、发布与公网验收保持开放。此前银行流水及原件整理不等于这些项目已完成;2026-10-07 的生产空存量结论仅适用于该查询时点。 + +### R4/AC5 本次补充的凭证验收边界 + +本小节已随 N1–N2 获准实施,完成状态以 implement 的验证结果为准;其余 R1–R6 和原 AC 编号保持不变。 + +- 明确注明非最终依据、仅受理/处理中,或凭证性质不明的材料,可以留存,不能仅因金额、账户、状态字段匹配而核销。机器未提取出限制说明不等于原件没有该说明;普通人工录入或关联确认不能覆盖已知限制。 +- 正式银行回单是否证明实际付款,应根据已核验格式的记账方向、记账/交易事实和关键字段共同判断。不能仅凭标题推定成功,也不能要求经办补写原件不存在的“交易成功”。正式借入回单的公司入账方向不能当作公司还本的付款方向。 +- 原件足以证明付款而机器未能提全时,保留有依据的人工核对路径;经办录入原件实际信息、确认人按既定模式核对,记录原件及判断依据。原件不足、交易归属不明、字段冲突或重复交易仍不能放行。自动与人工结论分开记录。 +- 核验、人工证据重建及最终核销须消费同一份仍有效的凭证依据;旧记录缺少新判断依据时不得直接沿用为自动通过,不能回写已发生的付款历史。验收必须包含最终资金动作及失败后本金/占用未变化,不能止于匹配函数。 + ## 2026-10-08 两人办理关联方还款(本地实施与合成验证完成,已授权本地提交及正式验证) - 用户确认:实际经办为一名财务兼人事行政;马经理为股东、个人出借收款人,且有权代表公司批准还款。技术员工不参与复核、审批或见证,不增加财务权限,不作为候选、任务接收人或业务验收所需人员。 @@ -123,6 +150,12 @@ R3 的模式同时作用于付款审批、提交清单、成功/失败归属、 ## Current Status +N1–N2 在 `ce11dea9` 基线上完成本地实现和主会话检查,31 项改动未提交,无新增迁移。最新验证为 82 个迁移、116 组借款 HTTP、9 组数据库约束、18 组关联电脑/手机操作、38 组共享回单数据库/真实认证及 24 项共享报销页面限定重验通过;回单/报销最新定向 292 项通过,9 个资金守卫变异全部捕获。服务端全量为 4554 通过、1 项既有可选跳过,随后合成数据及零页详情的改动由上述最新定向覆盖,不虚报重新全量。Prisma、前端类型/构建及合同检查通过,准确时点和回执见 implement 顶部。 + +六份真实材料的本地分类及既有正式支出样本的内存人工录入预验已完成;未用真实借款执行核销,也未验证真实银行提交对应。当前不用用户补材料。独立代码复核、新候选正式门禁、获准发布/真实台账与授权登记、首笔真实还款闭环仍未完成;AC5、AC8、AC27 及整体业务验收不据本地结果勾选。原候选的正式门禁不覆盖当前差异,没有本轮提交、推送、部署、生产访问或真实写入。 + +### 关联方模式收尾记录(历史时点,最新状态见上文) + 本地已有 A/B/C、S1–S8、SR恢复及电脑/手机界面,本轮另完成关联方模式 RP1–RP4 的实现与可执行验证,已获准本地提交及新候选正式验证。最新借款专项为82迁移、HTTP 109、浏览器33、数据库约束9组通过;关联模式单列14组HTTP及电脑/手机各7组浏览器,13个有效守卫变异均捕获。报销付款、付款权限和导航35项通过,现行差旅冻结保护另2项通过;一次误选封存差旅整链的403失败保留,未恢复差旅权限。服务端全量4519通过、1项原有跳过,后续小改的77项定向验证通过,详细先后和证据见implement。AC20–AC26仅完成本地合成验收;AC5、AC8、AC27及其余整体业务验收保持开放。 较早候选 `4c37fcee` 的正式门禁 PASS 只证明旧独立模式;本轮提交后单独生成 `verify-candidate` 回执,不提前宣称通过,主会话检查也不等同独立代码复核。不得以测试数字推定真实银行接受性或生产已经应用。 diff --git a/client/src/api/errorReasons.ts b/client/src/api/errorReasons.ts index 621ff76e..89434a49 100644 --- a/client/src/api/errorReasons.ts +++ b/client/src/api/errorReasons.ts @@ -803,6 +803,8 @@ export const ERROR_REASON_MESSAGES: Record = { "payment_batch.receipt_transaction_reused": "回单交易已被使用或存在重复,请核对原回单;不能通过修改参考号重复登记。", "payment_batch.receipt_manual_state_changed": "回单或核验版本已变化,请刷新后重新查看当前状态。", "payment_batch.receipt_manual_verification_required": "回单信息尚不能确认与付款一致,请核对全部页、交易状态、金额、币种及收付款账号;不要直接登记付款。", + "payment_batch.receipt_document_check_required": "这份回单尚未检查凭证性质,请在核验详情中更新凭证核验后重新核对。", + "payment_batch.receipt_final_evidence_required": "当前材料不能证明本笔付款已完成。请核对正式回单的实际付款或记账依据;非最终回执、入账方向不符或证据不明时不能登记付款。", "payment_batch.receipt_key_unavailable": "回单核验密钥不可用,请联系管理员修复;不要重复识别或登记付款。", "payment_batch.receipt_key_rotation_required": "回单交易记录使用的密钥版本不一致,请管理员完成受控密钥迁移;人工核验不能绕过。", "payment_batch.receipt_snapshot_unsupported": "付款业务或银行快照不支持回单核验,请联系管理员核实;不要重复识别。", diff --git a/client/src/api/paymentBatches.ts b/client/src/api/paymentBatches.ts index 5758eb97..e4f12588 100644 --- a/client/src/api/paymentBatches.ts +++ b/client/src/api/paymentBatches.ts @@ -293,6 +293,23 @@ export async function retryPaymentBatchReceiptOcr(batchId: string, attachmentId: ); } +export type ReceiptPaymentProof = { + basis: "explicit_success" | "booked_debit"; + originalChecked: true; + note: string; + bookedOn?: string; +}; + +export type ReceiptDocumentEvidence = { + version: number; + kind: "bank_receipt" | "non_final" | "unknown"; + source: "pdf_text" | "image" | "unreadable" | "manual"; + direction: "debit" | "credit" | null; + explicitSuccess: boolean; + issues: string[]; + proof?: ReceiptPaymentProof; +}; + export type ReceiptVerificationPage = { pageNumber: number; amount: string | null; @@ -302,10 +319,12 @@ export type ReceiptVerificationPage = { transactionStatus: string | null; reference: string | null; itemId?: string; + document?: ReceiptDocumentEvidence | null; }; export type ReceiptVerificationDetail = { canReview?: boolean; + documentInspectionRequired?: boolean; version: number; state: string; pagesExpected: number; @@ -331,7 +350,7 @@ export function fetchReceiptReadiness(batchId: string, signal?: AbortSignal) { export function saveReceiptVerification(batchId: string, attachmentId: string, body: { expectedVersion: number; reason?: string; - pages: { pageNumber: number; itemId: string; fields?: { Name: string; Value: string }[] }[]; + pages: { pageNumber: number; itemId: string; fields?: { Name: string; Value: string }[]; paymentProof?: ReceiptPaymentProof }[]; }) { return apiRequestData<{ item: { id: string; status: string; version: number } }>( `/api/payment-batches/${batchId}/receipt-attachments/${attachmentId}/manual-verification`, @@ -339,6 +358,13 @@ export function saveReceiptVerification(batchId: string, attachmentId: string, b ); } +export function checkReceiptDocument(batchId: string, attachmentId: string, expectedVersion: number) { + return apiRequestData<{ item: { id: string; status: string; version: number } }>( + `/api/payment-batches/${batchId}/receipt-attachments/${attachmentId}/document-check`, + { method: "POST", body: JSON.stringify({ expectedVersion }) }, + ); +} + export function reviewReceiptVerification(batchId: string, attachmentId: string, proposalId: string, body: { expectedVersion: number; decision: "approve" | "reject"; reason: string; }) { diff --git a/client/src/components/common/PaymentReceiptVerificationDialog.tsx b/client/src/components/common/PaymentReceiptVerificationDialog.tsx index 18875ce8..d5101f25 100644 --- a/client/src/components/common/PaymentReceiptVerificationDialog.tsx +++ b/client/src/components/common/PaymentReceiptVerificationDialog.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { - fetchReceiptVerification, saveReceiptVerification, reviewReceiptVerification, + fetchReceiptVerification, saveReceiptVerification, reviewReceiptVerification, checkReceiptDocument, type ReceiptVerificationPage, } from "../../api/paymentBatches"; import { resolveErrorMessage } from "../../lib/resolveErrorMessage"; @@ -13,7 +13,7 @@ import { Textarea } from "../ui/textarea"; const FIELD_NAMES = ["小写金额", "币别", "付款人账号", "收款人账号", "凭证号", "单据状态"] as const; const CHANGE_LABELS: Record = { amount: "金额", currency: "币种", payerAccount: "付款账号", payeeAccount: "收款账号", - reference: "交易参考", transactionStatus: "交易状态", + reference: "交易参考", transactionStatus: "交易状态", paymentProof: "付款凭证依据", }; function PageFacts({ page }: { page?: ReceiptVerificationPage }) { @@ -22,7 +22,11 @@ function PageFacts({ page }: { page?: ReceiptVerificationPage }) {
付款账号:{page?.payerAccountMasked ?? "未识别"}
收款账号:{page?.payeeAccountMasked ?? "未识别"}
交易参考:{page?.reference ?? "未识别"}
-
状态:{{ success: "成功", failed: "失败", pending: "受理或处理中" }[page?.transactionStatus ?? ""] ?? "未确认"}
+
状态:{page?.document?.proof?.basis === "booked_debit" ? "已记账支出(人工核对)" : + ({ success: "成功", failed: "失败", pending: "受理或处理中" }[page?.transactionStatus ?? ""] ?? "未确认")}
+
凭证性质:{{ bank_receipt: "银行正式回单", non_final: "非最终回执", unknown: "需核对原件" }[page?.document?.kind ?? "unknown"]}
+ {page?.document?.proof ?
付款依据:{page.document.proof.basis === "booked_debit" ? + `正式回单记载已记账支出,记账日期 ${page.document.proof.bookedOn}` : "正式回单原件明确显示付款成功"}
: null} ; } @@ -40,6 +44,7 @@ export function PaymentReceiptVerificationDialog(props: Props) { const [editing, setEditing] = useState>({}); const [changes, setChanges] = useState>>({}); const [reason, setReason] = useState(""); + const [proofs, setProofs] = useState>({}); const query = useQuery({ queryKey: ["receipt-verification", props.actorId, props.batchId, props.attachmentId], queryFn: ({ signal }) => fetchReceiptVerification(props.batchId, props.attachmentId, signal), @@ -59,7 +64,14 @@ export function PaymentReceiptVerificationDialog(props: Props) { const pageNumber = index + 1; const fields = Object.entries(changes[pageNumber] ?? {}).filter(([, value]) => value.trim()) .map(([Name, Value]) => ({ Name, Value })); - return { pageNumber, itemId: selections[pageNumber] ?? "", ...(fields.length ? { fields } : {}) }; + const proof = proofs[pageNumber]; + if (proof?.basis === "booked_debit") { + const status = fields.find((field) => field.Name === "单据状态"); + if (status) status.Value = ""; else fields.push({ Name: "单据状态", Value: "" }); + } + return { pageNumber, itemId: selections[pageNumber] ?? "", ...(fields.length ? { fields } : {}), + ...(proof?.basis ? { paymentProof: { basis: proof.basis, originalChecked: true as const, note: reason.trim(), + ...(proof.basis === "booked_debit" ? { bookedOn: proof.bookedOn } : {}) } } : {}) }; }), }), onSuccess: changed, @@ -71,15 +83,26 @@ export function PaymentReceiptVerificationDialog(props: Props) { ), onSuccess: changed, }); - const busy = save.isPending || review.isPending; + const inspect = useMutation({ + mutationFn: () => checkReceiptDocument(props.batchId, props.attachmentId, detail!.version), + onSuccess: changed, + }); + const busy = save.isPending || review.isPending || inspect.isPending; const pageCount = detail && Number.isInteger(detail.pagesExpected) && detail.pagesExpected > 0 && detail.pagesExpected <= 20 ? detail.pagesExpected : 0; - const editable = props.canManage && detail && ["needs_review", "ocr_failed"].includes(detail.state) && !query.isError; + const editable = props.canManage && detail && !detail.documentInspectionRequired && ["needs_review", "ocr_failed"].includes(detail.state) && !query.isError; const reviewable = props.canReview && detail?.canReview === true && detail?.proposal?.status === "pending_review" && props.actorId !== detail.proposal.proposedBy && props.actorId !== props.makerId && !query.isError; - const complete = pageCount > 0 && Array.from({ length: pageCount }, (_unused, index) => selections[index + 1]).every(Boolean); + const blocked = (page?: ReceiptVerificationPage) => page?.document?.kind === "non_final" || + page?.document?.direction === "credit" || Boolean(page?.document?.issues.length); + const needsProof = (page?: ReceiptVerificationPage) => page?.document?.kind !== "bank_receipt" || !page.document.explicitSuccess; + const complete = pageCount > 0 && Array.from({ length: pageCount }, (_unused, index) => { + const n = index + 1; const page = detail?.pages.find((entry) => entry.pageNumber === n); const proof = proofs[n]; + return selections[n] && !blocked(page) && (!needsProof(page) || (proof?.basis && proof.checked && + reason.trim().length >= 5 && (proof.basis !== "booked_debit" || /^\d{4}-\d{2}-\d{2}$/.test(proof.bookedOn)))); + }).every(Boolean); const hasCorrections = Object.values(changes).some((fields) => Object.values(fields).some((value) => value.trim())); - const error = query.error ?? save.error ?? review.error; + const error = query.error ?? save.error ?? review.error ?? inspect.error; return { if (!open && !busy) props.onClose(); }}> @@ -90,10 +113,15 @@ export function PaymentReceiptVerificationDialog(props: Props) { {error ?

{resolveErrorMessage(error)}

: null} + {detail?.documentInspectionRequired ?
+

旧核验尚未记录凭证性质。更新后会保留原记录,并要求重新核对原件;不会重新发送文件识别。

+ {props.canManage && !query.isError ? : null} +
: null} {detail && !pageCount ?

尚无完整页数,请先重新识别或上传可读取的原回单。

: null} {Array.from({ length: pageCount }, (_unused, index) => index + 1).map((pageNumber) => { const original = detail?.pages.find((page) => page.pageNumber === pageNumber); @@ -101,13 +129,14 @@ export function PaymentReceiptVerificationDialog(props: Props) { return

第{pageNumber}页 · 原识别结果

+ {blocked(original) ?

这份材料不能证明本笔付款完成。请提供对应的正式付款回单;不能通过更正字段覆盖非最终说明、收付方向或交易矛盾。

: null} {proposed ?

人工核验结果

对应明细:{props.items.find((item) => item.id === proposed.itemId)?.label ?? "当前明细不可用"}

更正字段:{detail?.proposal?.changedFields.find((entry) => entry.pageNumber === pageNumber)?.fields.map((field) => CHANGE_LABELS[field] ?? "字段").join("、") || "未更正,仅关联"}

: null} - {editable ? <> + {editable && !blocked(original) ? <> + {needsProof(original) ?
+ + {proofs[pageNumber]?.basis === "booked_debit" ? : null} + +

记账支出不要求补填“交易成功”。凭证性质或资金方向不能确认时,请先补证。

+
: null} {editing[pageNumber] ?

只填写要更正的字段,其他字段保留原识别值。更正后需要{props.relatedParty ? "公司代表确认" : "独立复核"}。

- {FIELD_NAMES.map((name) =>