补充报销事实修正审计规约(07-27-yang-jun-reimbursement-readiness)
这个提交包含在:
@@ -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
|
||||
|
||||
在新工单中引用
屏蔽一个用户