修复铁路OCR空金额回退(07-28-travel-reimbursement-guide)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-07-29 15:32:16 +08:00
父节点 5f2788ccac
当前提交 665cf4add5
共修改 4 个文件,包含 58 行新增和 2 行删除
@@ -165,6 +165,9 @@ const ocrRan = invoice.ocr_attempts > 0
`sellerName`.
- `electronic_train_ticket`: `invoiceNumber`, `invoiceDate`, `totalAmount`,
`buyerName`.
- Railway `totalAmount` uses the first individually parseable value from
Tencent `Total`, then `Fare`. Parse each field before applying fallback:
an empty or otherwise unparseable `Total` must not block a parseable `Fare`.
- Never invent an invoice code from an invoice number or vice versa. Known
no-code full-VAT and railway tickets use invoice-number-only duplicate
detection; other categories retain code-plus-number semantics.
@@ -225,6 +228,9 @@ const ocrRan = invoice.ocr_attempts > 0
- OCR service/job tests assert category-specific required fields,
number-only dedupe only for known no-code categories, and no retry on terminal
errors.
- Railway adapter tests must cover an empty `Total` with a parseable `Fare` and
assert that normalized `totalAmount` uses `Fare` without emitting the source
amount in diagnostics.
- Cache tests seed a legacy Tencent result containing `raw` and sensitive
fields, then assert both normal and race cache paths return and persist only
the allow-listed shape.
@@ -251,6 +257,16 @@ const persisted = sanitizeOcrResultForPersistence("tencent", normalized);
await prisma.ocrResult.create({ data: { result_json: persisted } });
```
For railway amount fallback, do not choose the raw source before parsing:
```js
// Wrong: an empty string is not nullish, so it blocks Fare.
toNumberString(info?.Total ?? info?.Fare);
// Correct: only a successfully parsed Total blocks Fare.
toNumberString(info?.Total) ?? toNumberString(info?.Fare);
```
## Scenario: Live OCR Sample Stdin Probe
### 1. Scope / Trigger