验证OCR关闭后的报销闭环(07-27-yang-jun-reimbursement-readiness)

这个提交包含在:
2026-07-27 20:54:19 +08:00
父节点 68600ec2d5
当前提交 9694179c25
共修改 7 个文件,包含 206 行新增和 14 行删除
@@ -15,6 +15,98 @@ duplicate detection or attachment behavior.
submission, approval, payment batch, receipt attachment and AP export when the
Trellis task scope touches those areas.
## Scenario: OCR Disabled Without Mock Data
### 1. Scope / Trigger
- Trigger: the production OCR provider is unavailable or intentionally disabled
while employees still need to upload PDF invoices and submit reimbursements.
- Purpose: keep real invoices out of the mock provider and avoid interpreting a
historical database default as evidence that Tencent OCR actually ran.
### 2. Signatures
- Runtime env: `OCR_PROVIDER=off`.
- Upload: `POST /api/invoice-pool/upload` with a PDF multipart `file`.
- Bind: `POST /api/invoice-pool/:id/bind` with `{ reimbursementId }`.
- Database: `InvoicePool.ocr_provider` is a non-null `OcrProvider` with schema
default `tencent`; `ocr_attempts`, `ocr_last_requested_at`,
`ocr_completed_at` and extracted invoice fields carry the execution evidence.
### 3. Contracts
- `off` means no OCR queue entry, no provider request and no invented invoice
code, number, date, buyer/seller or amount. A new PDF enters `available` and
may be bound to an existing reimbursement item.
- `mock` is restricted to local or explicitly isolated synthetic tests. It must
not process a real production invoice.
- Because `ocr_provider` is non-null with a database default, an off-mode record
may still store `tencent` when the create payload omits that field. Consumers
must not treat that value alone as proof of OCR execution.
- Until real OCR or mandatory manual metadata entry is separately approved,
finance checks the PDF original manually. SHA-256 duplicate detection remains
active; invoice-code/number duplicate detection is unavailable for blank
metadata.
- A config-only provider switch keeps `BUILD_SHA` unchanged. Recreate every
process that actually loads OCR config: server only when `JOBS_ENABLED=true`
and no worker exists; server plus worker when an independent worker already
exists. Never create a second job executor merely to satisfy a generic restart
instruction.
### 4. Validation & Error Matrix
- `OCR_PROVIDER=mock` in real operation -> block real invoice use and switch to
`off` or a verified live provider.
- `status != available` immediately after an off-mode upload -> stop and inspect
the effective runtime config; do not patch invoice fields to bypass it.
- Any extracted invoice business field, OCR attempt, request timestamp or
completion timestamp appears in off mode -> fail the acceptance run and
investigate provider/job topology.
- `ocr_provider=tencent` with attempts `0`, null request/completion timestamps
and null extracted fields -> historical schema-default finding, not evidence
of a Tencent request.
- Blank metadata cannot bind or submit -> keep the test draft, clean it through
normal APIs, then plan a code change; do not add an emergency production
database edit.
### 5. Good/Base/Bad Cases
- Good: off-mode PDF is `available`, all business fields are null, attempts are
`0`, it binds to a 0.01 synthetic reimbursement, submits, is returned, then
the reimbursement is deleted and the invoice archived.
- Base: live Tencent OCR is configured and verified with an authorized sample;
normal extracted-field and duplicate rules apply.
- Bad: leave production on mock, or decide that OCR ran merely because
`ocr_provider` contains the schema default `tencent`.
### 6. Tests Required
- Assert upload status is `available` under `OCR_PROVIDER=off`.
- Assert invoice code, number, date, buyer, seller and all amount fields are
null; assert attempts are `0` and request/completion timestamps are null.
- Bind to a reimbursement with one valid item, submit through the real workflow,
return instead of approve, delete the draft and archive the released invoice.
- Final read-only checks: test accounts archived, active sessions `0`, test
reimbursements `0`, pending tasks `0`, non-archived test invoices `0`, and
Enterprise WeChat writeback remains skipped.
### 7. Wrong vs Correct
#### Wrong
```js
const ocrRan = invoice.ocr_provider === "tencent";
```
#### Correct
```js
const ocrRan = invoice.ocr_attempts > 0
|| invoice.ocr_last_requested_at != null
|| invoice.ocr_completed_at != null
|| invoice.ocr_result_id != null;
```
## Scenario: Payment Batch Receipt Attention Count
### 1. Scope / Trigger