修复报销票据门禁提示与 OCR 降级(08-30-b2-submit-error-ocr-fallback)
这个提交包含在:
@@ -30,10 +30,12 @@ duplicate detection or attachment behavior.
|
||||
require the same own-or-`expense.view_all` guard as reimbursement detail reads
|
||||
before invoking the precheck service; an ordinary nonparticipant must receive
|
||||
403 without service-side evaluation.
|
||||
- 取消或放宽必填校验时,必须继续审计该校验之后的代码是否依赖它提供的
|
||||
非空、类型或状态保证,并为生产真实存在的空值路径补回归测试。不得只
|
||||
删除前置错误分支;可空字段的 `.trim()`、`.length`、字符串拼接和传给
|
||||
非空参数的调用都应逐处确认。
|
||||
- 增加、取消或放宽强制校验时,必须同时核对既有降级合同、兼容模式和该
|
||||
校验之后的代码假设。取消或放宽时审计下游是否依赖它提供的非空、类型
|
||||
或状态保证,并为生产真实存在的空值路径补回归测试;增加时确认不会把
|
||||
`off`、历史数据或其他已承诺可用的降级路径变成不可提交。不得只修改
|
||||
前置错误分支;可空字段的 `.trim()`、`.length`、字符串拼接和传给非空
|
||||
参数的调用都应逐处确认。
|
||||
|
||||
## Scenario: OCR Disabled Without Mock Data
|
||||
|
||||
@@ -994,6 +996,10 @@ await selectApprovalRule(reimbursement.total_amount);
|
||||
- Reimbursement detail returns `invoiceGuardEnabled` for the form department.
|
||||
- `POST /api/invoice-pool/:id/bind` requires `linkedItemId` while the guard is
|
||||
enabled; `POST /api/reimbursements/:id/submit` enforces the final gate.
|
||||
- `PATCH /api/reimbursements/:id/invoices/:invoiceId/item-link` atomically links
|
||||
one bound-but-unlinked invoice to an item in the same editable form.
|
||||
- Detail invoice projection adds `amountEvidenceStatus`; form detail and submit
|
||||
success add the redacted manual-review summary without exposing OCR payloads.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
@@ -1017,16 +1023,36 @@ await selectApprovalRule(reimbursement.total_amount);
|
||||
- With B2 on, binding requires an existing item in the same reimbursement and
|
||||
may not reach the legacy auto-create branch. If an upload waits for OCR, keep
|
||||
the user-selected item ID with the pending invoice and send it when OCR later
|
||||
makes the invoice bindable. Submit blocks any bound invoice with no item
|
||||
link, each required-invoice item with no amount-bearing bound invoice, and
|
||||
each declared amount greater than its linked invoice total.
|
||||
- Sum and compare money with `Prisma.Decimal`. Invoice-free items are excluded.
|
||||
A lower declared amount remains allowed for finance to reconcile.
|
||||
makes the invoice bindable. Submit separates PDF presence from amount
|
||||
evidence: a required item without a bound PDF blocks as missing evidence;
|
||||
a bound PDF with attempted OCR but no amount blocks as recognition failure.
|
||||
- Classify one invoice from the explicit minimum fields `total_amount`,
|
||||
`ocr_attempts`, request/completion timestamps and the bounded OCR error fact.
|
||||
Non-null amount is `verified`; null amount with no execution fact is
|
||||
`manual_review`; null amount with any execution fact is `ocr_failed`.
|
||||
Never infer execution from the historical non-null `ocr_provider` default.
|
||||
- Sum and compare money with `Prisma.Decimal` only when every bound invoice for
|
||||
the required item is `verified`. If any invoice is `manual_review`, allow
|
||||
submission and mark the item/form for finance review; a mixed verified plus
|
||||
manual-review set must not be compared using only the known partial sum.
|
||||
Invoice-free items are excluded. A lower declared amount remains allowed for
|
||||
finance to reconcile.
|
||||
- A bound invoice with `linked_item_id=null` remains visible and recoverable.
|
||||
The narrow item-link write locks invoice and form, reuses the owner/submitter
|
||||
participant check, accepts only `draft|rejected`, verifies both objects belong
|
||||
to the form, updates only `linked_item_id`, and writes audit in one
|
||||
transaction. Failure and concurrent state drift produce zero writes.
|
||||
- PC and mobile map structured guard details to a safe Chinese summary plus
|
||||
item/invoice-local recovery text. IDs locate current DTO objects only and are
|
||||
never rendered; dynamic summaries use alert semantics and toast is not the
|
||||
sole feedback. Finance read-only detail keeps manual-review markers visible.
|
||||
- With B2 off, retain legacy auto-match/create compatibility. Keep the legacy
|
||||
whole-form invoice gate only when at least one item requires an invoice;
|
||||
allowance-only forms remain invoice-free because that is an always-active B1
|
||||
policy. Turning the flag off never deletes or unbinds relationships already
|
||||
written while it was on.
|
||||
written while it was on. The manual-review projection and submit-success
|
||||
notice remain active for linked no-OCR PDFs even when B2 blocking is off;
|
||||
the flag controls blocking, not whether finance is told to inspect originals.
|
||||
- Form maintenance and invoice bind/unbind remain owner-or-submitter writes;
|
||||
`expense.view_all` and admin roles remain read-only for another employee's
|
||||
reimbursement.
|
||||
@@ -1040,11 +1066,23 @@ await selectApprovalRule(reimbursement.total_amount);
|
||||
`required_when_invoice_guard_enabled`; an item outside the form remains
|
||||
`item_not_in_reimbursement` with zero writes.
|
||||
- Guard-on submit with unlinked bound invoices -> `invoice_item_guard_failed`
|
||||
plus `unlinkedInvoiceCount`.
|
||||
- Required item without an amount-bearing bound invoice -> same error plus
|
||||
plus `unlinkedInvoiceCount` and locating `unlinkedInvoiceIds`.
|
||||
- Required item without any bound PDF -> same error plus
|
||||
`missingInvoiceItemIds`.
|
||||
- Bound PDF with an OCR execution fact but no amount -> same error plus
|
||||
`amountUnavailableItems`; tell the user to retry recognition or contact
|
||||
finance, not to upload a duplicate PDF.
|
||||
- Bound PDF with no OCR execution fact and no amount -> submit succeeds with
|
||||
`invoiceAmountManualReviewRequired=true`; approval detail keeps the same
|
||||
manual-review warning until comparable evidence exists.
|
||||
- Mixed verified and manual-review invoices -> submit succeeds for manual
|
||||
review without comparing the declared amount to the known partial sum.
|
||||
- Declared amount above linked total -> same error plus entries containing only
|
||||
`itemId`, canonical `declaredAmount` and `invoiceTotal`.
|
||||
`itemId`, canonical `declaredAmount` and `invoiceTotal`, but only when every
|
||||
linked invoice amount is verified.
|
||||
- Item-link by a nonparticipant, on a non-editable form, across forms, for an
|
||||
already-linked/non-bound invoice or after a concurrent state change -> reject
|
||||
with zero link/audit writes.
|
||||
- Invoice-free allowance-only form with no invoice -> may submit with the guard
|
||||
either on or off when all other existing checks pass.
|
||||
- Env hard-off or DB force-off -> B2 item-level blocks are skipped; the legacy
|
||||
@@ -1054,6 +1092,11 @@ await selectApprovalRule(reimbursement.total_amount);
|
||||
|
||||
- Good: one required item explicitly links several same-purpose invoices whose
|
||||
total covers the declaration; submit passes with the guard on.
|
||||
- Good: five required items each link one verified invoice and submit normally;
|
||||
one off-mode PDF with no OCR execution submits with an explicit finance
|
||||
manual-review marker.
|
||||
- Good: a CNY 1,000 item linked to one verified CNY 600 invoice and one
|
||||
manual-review invoice does not produce a false CNY 400 overage.
|
||||
- Good: a positive meal allowance-only form has no invoice and passes with the
|
||||
guard either on or off.
|
||||
- Base: guard off preserves an older draft and its auto-created link; later
|
||||
@@ -1064,6 +1107,9 @@ await selectApprovalRule(reimbursement.total_amount);
|
||||
item while explicit selection is required, use JavaScript floating-point
|
||||
sums for money, let repeated init erase finance account mappings, or 删除前置
|
||||
必填校验后继续对可空值直接调用 `.trim()`。
|
||||
- Bad: call a PDF with failed OCR “missing invoice”, use only a known partial
|
||||
amount in a mixed set, expose UUIDs/raw details, or implement recovery as
|
||||
client-side unbind followed by bind.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
@@ -1075,14 +1121,20 @@ await selectApprovalRule(reimbursement.total_amount);
|
||||
disabled legacy types, default-off flag and account-code preservation.
|
||||
- Item/submit route tests 覆盖 assets 创建 omitted→null、编辑 null+omitted→null、
|
||||
有值+omitted→保留、显式清空、切换非 assets 清空和无依据提交,并继续覆盖
|
||||
invoice-free zero invoice、missing link、unlinked bound invoice、
|
||||
declared-over-invoice 与 participant/nonparticipant side effects。
|
||||
invoice-free zero invoice、missing PDF、unlinked bound invoice、OCR failed、
|
||||
manual review、verified+manual mixed evidence、five-item/five-invoice normal
|
||||
submit、declared-over-invoice 与 participant/nonparticipant side effects。
|
||||
- Bind tests cover flag on/off, missing/foreign/successful explicit links and
|
||||
prove the guard-on path does not create a reimbursement item.
|
||||
- Atomic item-link tests cover success, participant ownership, editable status,
|
||||
invoice/form/item ownership, existing links, non-bound state and concurrent
|
||||
zero-write behavior including audit.
|
||||
- PC/mobile Playwright covers asset 登记导向说明、批准依据输入/展示均不存在且
|
||||
保存 payload omission、CNY 2,000 warning、allowance-only submit projection、
|
||||
disabled add action until every
|
||||
selected invoice has an item, and pending-OCR binding retaining that item.
|
||||
selected invoice has an item, pending-OCR binding retaining that item, four
|
||||
safe guard messages with local recovery actions, alert semantics, atomic
|
||||
unlinked-invoice recovery and finance manual-review visibility.
|
||||
- Run full backend Vitest with the pinned BIFF8 Python dependencies, Prisma
|
||||
validate/generate check, frontend typecheck/build, focused Playwright,
|
||||
process/docs/Trellis governance and `git diff --check`.
|
||||
@@ -1109,7 +1161,7 @@ asset_prior_approval_basis: targetApprovalBasis.trim();
|
||||
#### Correct
|
||||
|
||||
```js
|
||||
if (item.expense_type.requires_invoice) {
|
||||
if (item.expense_type.requires_invoice && linkedInvoices.every(hasVerifiedAmount)) {
|
||||
const total = linkedInvoices.reduce(
|
||||
(sum, invoice) => sum.plus(invoice.total_amount),
|
||||
new Prisma.Decimal(0),
|
||||
|
||||
在新工单中引用
屏蔽一个用户