完善报销通知与付款进度(08-01-reimbursement-notification-progress-clarity)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-01 21:06:31 +08:00
父节点 ca24479a44
当前提交 aa6b9bc373
共修改 23 个文件,包含 730 行新增和 88 行删除
+111 -5
查看文件
@@ -138,9 +138,12 @@ if (!hasPermission(actor, "payment.batch.review")
workflow stages use the actual pending assignee when available. Future stages
keep `actorName=null`; never guess a person from today's role holders.
- A reimbursement approved without a batch makes `payment_batch_review` the
current stage with `detail="待财务创建付款批次"`. A draft batch says
`所在付款批次待非制单人复核`; shared-batch wording must not imply that this
reimbursement alone is being reviewed again.
current stage with `detail="待财务安排付款"`. A draft batch says
`待非制单人复核付款`; employee-facing wording does not expose the internal
batch object or imply that this reimbursement alone is being reviewed again.
- The final stage label is `付款完成`, and its pending/current/failed detail
explicitly says it is a finance registration step rather than an approval.
Internal finance pages may continue to use the precise `付款批次` term.
- Missing workflow, batch or receipt facts degrade only their stages. Earlier
known stages remain visible; missing downstream objects are normal, not 5xx.
- `targetHref` is returned only when the viewer already has one of the existing
@@ -163,7 +166,7 @@ if (!hasPermission(actor, "payment.batch.review")
- Batch `draft|reviewed|executed|failed|cancelled` -> map to explicit current,
completed or exception states without changing the batch.
- Executed payment + zero active receipts -> completed payment with
`付款已完成,回单待留存`; positive count -> `付款与回单已留存`.
`付款已完成,回单待留存`; positive count -> `付款和回单已留存`.
- Viewer lacks batch-read permission -> `targetHref=null`; do not return 403 for
the reimbursement detail itself.
- Database/query failure -> existing 5xx behavior and server logging; do not
@@ -171,7 +174,7 @@ if (!hasPermission(actor, "payment.batch.review")
### 5. Good/Base/Bad Cases
- Good: `财务复核=通过` followed by current `待财务创建付款批次`, so the owner
- Good: `财务复核=通过` followed by current `待财务安排付款`, so the owner
can diagnose the exact responsibility without asking three people.
- Base: a newly submitted reimbursement shows known submission, every real
configured approval task, and the two future payment stages with no guessed
@@ -210,3 +213,106 @@ const approvalStages = workflowTasks.map((task) => buildApprovalStage(task));
approvalStages.push(...buildFutureSnapshotStages(workflowSnapshot, workflowTasks));
return [submittedStage, ...approvalStages, batchReviewStage, paymentStage];
```
## Scenario: Payment Responsibility Notifications
### 1. Scope / Trigger
- Trigger: creating a reimbursement payment batch, completing its maker-checker
review, or extending notifications to another payment responsibility handoff.
- These are action-required notifications, not new workflow approval tasks and
not public business-domain events.
### 2. Signatures
- Notification types: `payment_batch.review_requested` and
`payment_batch.execution_requested`.
- Both use `targetType=payment_batch`, `targetId=<batch id>` and
`targetPath=/payment-batches?id=<batch id>`.
- Both resolve to the existing `reimbursement` IM domain while payment batches
contain only reimbursement entities.
### 3. Contracts
- Batch creation resolves active employees whose roles include `manager|ceo`
and whose effective role permissions include `payment.batch.review`; exclude
the maker and never hard-code a person's name or employee ID.
- Review completion first targets the active maker when that employee still has
`payment.batch.manage`. If unavailable, fall back to other active
`finance|finance_mgr` employees with that permission; always exclude the
reviewer.
- Create station notification records inside the same transaction as the batch
creation/review state change. Dispatch through the shared IM dispatcher only
after commit. A dispatch failure is logged and, once a channel attempt starts,
retained as delivery evidence; it never rolls back the committed payment
action.
- Payment execution keeps the existing `reimbursement.paid` result notification
to the owner/submitter. `付款完成` remains a finance action and must not be
represented as another approval.
- PC and mobile approval histories use the shared
`client/src/lib/approvalDisplay.ts#workflowActionLabel`; `start|submit` both
render as `提交`, and pages must not keep local label maps.
### 4. Validation & Error Matrix
- No eligible non-maker reviewer -> create the batch with zero review
notifications; do not weaken maker-checker or notify the maker.
- Maker inactive or missing `payment.batch.manage` after review -> notify the
eligible finance fallback; no eligible fallback -> review remains committed
with zero execution notifications.
- Maker attempts review or a reviewed batch is reviewed again -> existing
deterministic `409 VALID_001`; zero responsibility notifications and zero IM
dispatch.
- IM disabled, recipient push disabled/unbound, or no matching channel -> station
notification remains; dispatcher returns a fixed skipped reason without
rolling back the payment action.
- Notification record creation fails inside the transaction -> the protected
payment state change fails atomically; no partial handoff is reported.
### 5. Good/Base/Bad Cases
- Good: finance creates one weekly batch, the eligible non-maker reviewer gets
one notification, then successful review returns the action to the active
maker for payment and receipt registration.
- Base: no eligible reviewer is configured; the draft batch remains visible for
diagnosis and no unqualified management broadcast is fabricated.
- Bad: notify every finance/manager account, let the maker review their own
batch, send before transaction commit, or label payment completion as an
approval.
### 6. Tests Required
- Route tests assert qualified reviewer resolution, maker exclusion, active
maker preference, inactive-maker finance fallback and reviewer exclusion.
- Negative tests assert maker conflict, repeated review and duplicate batch
creation produce no notification/dispatch.
- Dispatcher tests assert both types are IM-supported and resolve to the
reimbursement domain; dispatch failure must leave the committed batch state.
- PC/mobile E2E asserts `start` renders `提交` from the shared label map and the
five-stage employee projection ends with `付款复核` / `付款完成(非审批)`.
### 7. Wrong vs Correct
#### Wrong
```js
notifyEmployeesByRole(["manager", "finance"]);
await tx.paymentBatch.update({ data: { status: "reviewed" } });
```
#### Correct
```js
const recipients = await resolvePaymentExecutionRecipientIds(tx, {
makerId: batch.created_by,
reviewerId: actor.id,
});
const notifications = await createNotificationsWithRecords(tx, {
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.paymentBatchExecutionRequested,
targetType: "payment_batch",
targetId: batch.id,
});
// after commit
await flushNotificationDispatchTasks({ tasks: [buildDispatchTask(notifications)] });
```