收口付款批次创建与复核(08-01-reimbursement-notification-progress-clarity)
这个提交包含在:
@@ -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}
|
||||
```
|
||||
|
||||
在新工单中引用
屏蔽一个用户