补充报销事实修正审计规约(07-27-yang-jun-reimbursement-readiness)

这个提交包含在:
2026-08-01 11:05:31 +08:00
父节点 de2edd660d
当前提交 4cf73ce0d2
@@ -691,6 +691,9 @@ await previewFileInWecom({ url: bundle.signedUrl, name: bundle.originalName, siz
- `PATCH /api/reimbursements/:id` accepts nullable camel-case counterparts.
- Detail response includes `travelSummary: { isTravel, purpose, location,
startDate, endDate }`.
- A privileged correction of an already submitted reimbursement writes an
`AuditLog` in the same transaction. Its `extra.changes` is an object keyed by
changed field name, with `{ before, after }` scalar snapshots.
### 3. Contracts
@@ -702,6 +705,17 @@ await previewFileInWecom({ url: bundle.signedUrl, name: bundle.originalName, siz
min/max travel-item dates; location remains `未填写` and is never guessed.
- Submit requires all four fields only for travel reimbursements; ordinary
reimbursements remain unaffected.
- An administrator correcting an already submitted amount, expense date,
travel-summary date or other core declared financial fact must record the
exact normalized `before` and `after` values in the restricted database audit
entry. Dates use `YYYY-MM-DD`; decimal amounts use canonical strings. The
audit also records a sanitized reason and operation/task reference.
- The repository-visible Trellis task, docs and generated evidence record only
changed field names, invariant results and counts. They must not copy the
real purpose, location, amount, ticket content or audit payload.
- Do not put attachment bytes, OCR/provider payloads or unrelated personal data
into `extra.changes`. Existing `system.audit.view` authorization remains the
access boundary for the database audit.
### 4. Validation & Error Matrix
@@ -710,18 +724,33 @@ await previewFileInWecom({ url: bundle.signedUrl, name: bundle.originalName, siz
- Travel submit with missing fields -> `400` with
`travelSummary: incomplete` and stable `missingFields`.
- Edit while not draft/rejected -> existing `409 invalid_edit_status`.
- Privileged correction changes a core declared fact but lacks a field-level
`before` or `after` snapshot -> abort the transaction; a field-name-only
audit is insufficient.
- A historical correction is later found without its original snapshot -> do
not invent or reconstruct an audit value from memory. Record the evidence
gap and enforce the complete audit contract on future corrections.
### 5. Good/Base/Bad Cases
- Good: travel item plus complete purpose/location/date range submits normally.
- Base: office-supply reimbursement has no travel summary and is not blocked.
- Good: an authorized date correction atomically updates the row and writes
`extra.changes.expenseDate = { before, after }` using synthetic or real
database values visible only to authorized auditors.
- Bad: infer a destination from invoice OCR, attachment name or free text.
- Bad: overwrite a submitted expense date and audit only
`changedFields: ["expenseDate"]`, leaving the original fact unrecoverable.
### 6. Tests Required
- Prisma validation and all migrations against an empty PostgreSQL database.
- Route tests for detail fallback, complete/incomplete travel submit, date order
and non-travel compatibility; frontend typecheck/build and PC/mobile UI tests.
- Privileged-correction tests use synthetic data and assert one transaction,
exact normalized `before/after`, sanitized reason, unchanged amount/material/
workflow invariants, rollback on audit failure and no real values in generated
repository evidence.
### 7. Wrong vs Correct
@@ -731,12 +760,26 @@ await previewFileInWecom({ url: bundle.signedUrl, name: bundle.originalName, siz
travelLocation = invoice.ocrResult.sellerName;
```
```js
extra = { changedFields: ["expenseDate"] }; // original declared fact is lost
```
#### Correct
```js
travelLocation = reimbursement.travel_location ?? null;
```
```js
extra = {
changes: {
expenseDate: { before: "2026-01-02", after: "2026-01-03" },
},
reason: "authorized_correction",
operationRef: "TASK-SYNTHETIC",
};
```
## Scenario: Payment Batch Receipt Attention Count
### 1. Scope / Trigger