收口付款批次创建与复核(08-01-reimbursement-notification-progress-clarity)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-02 00:29:47 +08:00
父节点 78b80f88d7
当前提交 7c39f8b0f9
共修改 17 个文件,包含 591 行新增和 227 行删除
+128 -9
查看文件
@@ -234,9 +234,11 @@ return [submittedStage, ...approvalStages, batchReviewStage, paymentStage];
### 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.
- Batch creation resolves exactly one active non-maker reviewer whose effective
role permissions include `payment.batch.review`: choose the first stable
`ceo` candidate, otherwise the first stable `manager` candidate. Candidate
ordering uses a deterministic business key plus `id`; never hard-code a
person's name or employee ID and never broadcast to every manager/CEO.
- 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
@@ -255,8 +257,10 @@ return [submittedStage, ...approvalStages, batchReviewStage, paymentStage];
### 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.
- No eligible non-maker reviewer -> `409 VALID_001` with
`details.review=no_eligible_reviewer` before batch, item, audit or notification
writes. Do not weaken maker-checker, notify the maker or leave an ownerless
batch for later diagnosis.
- Maker inactive or missing `payment.batch.manage` after review -> notify the
eligible finance fallback; no eligible fallback -> review remains committed
with zero execution notifications.
@@ -274,16 +278,21 @@ return [submittedStage, ...approvalStages, batchReviewStage, paymentStage];
- 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.
- Base: one active CEO and several managers qualify; only the CEO selected by
stable ordering receives the review notification.
- Bad: no eligible reviewer is configured but an ownerless batch is still
created, leaving finance with an action that nobody can complete.
- 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.
- Route tests assert CEO-first single-recipient resolution, manager fallback,
maker exclusion, active maker preference after review, inactive-maker finance
fallback and reviewer exclusion.
- The no-reviewer route test asserts `409` and zero batch, item, audit,
notification and dispatch side effects.
- 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
@@ -316,3 +325,113 @@ const notifications = await createNotificationsWithRecords(tx, {
// after commit
await flushNotificationDispatchTasks({ tasks: [buildDispatchTask(notifications)] });
```
## Scenario: Reimbursement Payment Batch Create-As-Submit Lifecycle
### 1. Scope / Trigger
- Trigger: changing reimbursement payment-batch creation, user-facing status or
actions, payment-date defaults, payment-channel compatibility, or receipt
prerequisites across server API and React clients.
- This lifecycle records an offline bank payment. OA never initiates a bank
transfer and `execute` means registering a completed payment.
### 2. Signatures
- Create: `POST /api/payment-batches` with
`{ entityType: "reimbursement", entityIds: UUID[], name?,
defaultPaymentDate?, defaultPaymentChannel?, voucherPrefix? }`.
- Review: `POST /api/payment-batches/:id/review`.
- Register completed payment: `POST /api/payment-batches/:id/execute`.
- Receipt API: `GET|POST /api/payment-batches/:id/receipt-attachments`.
- Internal status machine:
`draft -> reviewed -> executed|failed|cancelled`; there is no separate submit
endpoint or submitted database status.
- Shared client label API:
`paymentBatchStatusLabel(status)` from
`client/src/lib/paymentBatchDisplay.ts`.
### 3. Contracts
- For the normal reimbursement UI, creation sends only `entityType` and
selected `entityIds`. The server still accepts optional fields for controlled
compatibility and exceptional item overrides.
- Missing name becomes `<company-local YYYY-MM-DD> 付款批次`; the server creates
the unique batch number. Missing channel becomes `bank`. Missing actual
payment date is resolved to the company-local registration date only when
payment completion is registered, not when the batch is created.
- Creation is create-as-submit: the same transaction locks candidates, checks
reviewer eligibility, creates the batch/items/audit and station notification,
then the dispatcher runs after commit. Internal `draft` therefore means
`待付款复核`, never an editable unsubmitted draft.
- User-facing status labels are `draft=待付款复核`,
`reviewed=待财务付款`, `executed=已付款`,
`failed=付款登记失败`, `cancelled=已取消`. Payment-batch list/detail and AP
views import the shared label function instead of keeping local maps.
- The reviewer action is `复核通过`. The finance action is
`登记付款完成`; user-facing text must not say OA executes a transfer.
- For a reimbursement batch, a persisted null batch channel is interpreted as
effective `bank` in API projection, item channel resolution, receipt guard and
reimbursement payment record. This compatibility rule must not rewrite the
historical batch row.
- When an executable bank item exists, a successful receipt query with zero
active receipts exposes `上传银行回单` as the sole primary action. Only after
an active receipt exists may the UI expose `登记付款完成`. Loading or failed
receipt queries expose neither action rather than guessing that the
prerequisite is satisfied.
### 4. Validation & Error Matrix
- No selected reimbursement -> schema validation error; zero writes.
- Candidate already reserved or not payable -> existing deterministic `409`;
zero new batch.
- No eligible non-maker reviewer -> `409 VALID_001` with
`details.review=no_eligible_reviewer`; zero batch/item/audit/notification.
- Maker reviews own batch -> `409 VALID_001` with maker-checker conflict.
- Register bank payment without active receipt -> `409 VALID_001` with
`details.receiptAttachments=bank_receipt_required`; source reimbursement and
batch status remain unchanged.
- Receipt query loading/error -> no premature registration button; the error is
shown independently and the user may retry.
- Existing null-channel reimbursement batch -> project as bank and apply the
receipt guard without updating `default_payment_channel`.
### 5. Good/Base/Bad Cases
- Good: finance selects approved reimbursements and clicks
`创建并提交复核`; the generated batch immediately shows `待付款复核` and one
eligible non-maker receives the task notification.
- Base: after review, finance completes the bank transfer outside OA, uploads
the receipt, then registers payment completion using the company-local date.
- Bad: label `draft` as `草稿`, ask the user to find a nonexistent submit button,
show `登记付款完成` before receipt status is known, or silently rewrite a
legacy null channel in production.
### 6. Tests Required
- Route tests assert optional name, generated company-local title, bank default,
payment-date default, CEO-first reviewer, no-reviewer zero side effects and
null-channel compatibility without mutation.
- PostgreSQL concurrency regression asserts double create, double execute,
execute/cancel and direct-pay/batch races remain mutually exclusive.
- Client E2E asserts creation sends only `entityType+entityIds`, all consumers
use the shared labels, and receipt upload switches the sole primary action
from `上传银行回单` to `登记付款完成`.
- Build/typecheck, Prisma validation and focused payment-batch route/export tests
are required before commit.
### 7. Wrong vs Correct
#### Wrong
```tsx
const label = status === "draft" ? "草稿" : status;
<Button onClick={execute}>执行付款</Button>
```
#### Correct
```tsx
<StatusBadge label={paymentBatchStatusLabel(batch.status)} />
{needsBankReceipt ? <UploadReceipt /> : canRegister ? <RegisterPaid /> : null}
```