收紧付款复核资料与阶段编辑(08-01-reimbursement-notification-progress-clarity)
这个提交包含在:
@@ -343,7 +343,11 @@ await flushNotificationDispatchTasks({ tasks: [buildDispatchTask(notifications)]
|
||||
defaultPaymentDate?, defaultPaymentChannel?, voucherPrefix? }`.
|
||||
- Review: `POST /api/payment-batches/:id/review`.
|
||||
- Register completed payment: `POST /api/payment-batches/:id/execute`.
|
||||
- Payment metadata: `PATCH /api/payment-batches/:id/items/:itemId` accepts
|
||||
only `paymentVoucherNoOverride?` and `paymentDateOverride?`.
|
||||
- Receipt API: `GET|POST /api/payment-batches/:id/receipt-attachments`.
|
||||
- Receipt void API:
|
||||
`POST /api/payment-batches/:id/receipt-attachments/:attachmentId/void`.
|
||||
- Internal status machine:
|
||||
`draft -> reviewed -> executed|failed|cancelled`; there is no separate submit
|
||||
endpoint or submitted database status.
|
||||
@@ -353,9 +357,11 @@ await flushNotificationDispatchTasks({ tasks: [buildDispatchTask(notifications)]
|
||||
|
||||
### 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.
|
||||
- The normal reimbursement UI sends `entityType`, selected `entityIds` and
|
||||
`defaultPaymentChannel`; the channel defaults to `bank` and becomes an
|
||||
immutable review fact after creation. Item PATCH must not accept a channel
|
||||
override. A persisted legacy item override remains readable/executable for
|
||||
compatibility but is never exposed as a new write path.
|
||||
- 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
|
||||
@@ -379,6 +385,23 @@ await flushNotificationDispatchTasks({ tasks: [buildDispatchTask(notifications)]
|
||||
an active receipt exists may the UI expose `登记付款完成`. Loading or failed
|
||||
receipt queries expose neither action rather than guessing that the
|
||||
prerequisite is satisfied.
|
||||
- Stage editing is fixed: `draft` is read-only apart from cancel/review;
|
||||
`reviewed|failed` permits voucher/date and receipt upload/void;
|
||||
`executed|cancelled` is entirely read-only. Authorized receipt list/download
|
||||
remains available in every state.
|
||||
- Payment-batch responses never expose stored `snapshot_json` or a
|
||||
`snapshotJson` DTO field. Detail responses rebuild an allowlisted review DTO
|
||||
with only `payeeName`, `bankName` and a format-validated
|
||||
`bankAccountMasked`; list responses keep those detail-only fields null and do
|
||||
not decrypt owner accounts. New items persist the same low-sensitivity
|
||||
`payee` object in `snapshot_json`; full bank accounts and encrypted source
|
||||
values never enter new batch snapshots or any response.
|
||||
- A legacy or malformed stored payee is not trusted merely because its field is
|
||||
named `bankAccountMasked`. Invalid text, ciphertext-looking values and account
|
||||
values without a valid mask cause a read-only fallback to the current
|
||||
reimbursement owner. The fallback must not backfill history, and an existing
|
||||
but unreadable encrypted account fails closed rather than returning the raw
|
||||
stored value.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
@@ -395,6 +418,20 @@ await flushNotificationDispatchTasks({ tasks: [buildDispatchTask(notifications)]
|
||||
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`.
|
||||
- Item PATCH in `draft|executed|cancelled` -> `409 VALID_001` with
|
||||
`details.status=payment_metadata_not_editable`; zero item/audit writes.
|
||||
- Item PATCH contains `paymentChannelOverride` -> strict schema rejection;
|
||||
zero writes.
|
||||
- Receipt upload/void outside `reviewed|failed` -> `409 VALID_001` with
|
||||
`details.status=receipt_attachment_not_editable`; zero durable storage,
|
||||
attachment or audit side effects.
|
||||
- Receipt upload starts in `reviewed|failed` but the locked batch becomes
|
||||
`executed|cancelled` before DB write -> the transaction rejects and every
|
||||
object already written to storage is deleted by compensation; no attachment
|
||||
or audit row remains.
|
||||
- Missing legacy payee source -> return null display fields. Encryption-key or
|
||||
ciphertext authentication failure -> fail closed; never substitute a full
|
||||
account, ciphertext or unrelated employee.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
@@ -403,9 +440,14 @@ await flushNotificationDispatchTasks({ tasks: [buildDispatchTask(notifications)]
|
||||
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.
|
||||
- Good: reviewer sees the payee name, bank and masked account while every
|
||||
payment-registration control and receipt mutation is absent in `draft`.
|
||||
- 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.
|
||||
- Bad: permit voucher/channel/receipt changes while a reviewer is checking the
|
||||
batch, trust a persisted field name as proof of masking, or return
|
||||
`snapshotJson`, `bank_account_encrypted`, ciphertext or a full account.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
@@ -414,9 +456,18 @@ await flushNotificationDispatchTasks({ tasks: [buildDispatchTask(notifications)]
|
||||
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 `登记付款完成`.
|
||||
- Route tests assert the full state matrix, channel strict rejection, zero
|
||||
mutation side effects, masked new snapshots, legacy fallback and malicious
|
||||
stored snapshots without historical writes or full-account/ciphertext
|
||||
response values. They also assert list requests do not perform detail-level
|
||||
payee lookup/decryption.
|
||||
- Receipt tests cover both `reviewed` and `failed` positive paths, all three
|
||||
read-only states, zero stored files on immediate rejection, and storage
|
||||
compensation when the locked status changes after upload parsing.
|
||||
- Client E2E asserts creation sends the selected `defaultPaymentChannel`, all
|
||||
consumers use the shared labels, `draft` has no payment/receipt editors,
|
||||
`executed|cancelled` remains read-only, 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.
|
||||
|
||||
@@ -426,6 +477,7 @@ await flushNotificationDispatchTasks({ tasks: [buildDispatchTask(notifications)]
|
||||
|
||||
```tsx
|
||||
const label = status === "draft" ? "草稿" : status;
|
||||
<ChannelSelect onChange={(value) => patchItem({ paymentChannelOverride: value })} />
|
||||
<Button onClick={execute}>执行付款</Button>
|
||||
```
|
||||
|
||||
@@ -433,5 +485,7 @@ const label = status === "draft" ? "草稿" : status;
|
||||
|
||||
```tsx
|
||||
<StatusBadge label={paymentBatchStatusLabel(batch.status)} />
|
||||
{batch.status === "draft" ? <PayeeReviewFacts item={item} /> : null}
|
||||
{["reviewed", "failed"].includes(batch.status) ? <PaymentRegistrationFields /> : null}
|
||||
{needsBankReceipt ? <UploadReceipt /> : canRegister ? <RegisterPaid /> : null}
|
||||
```
|
||||
|
||||
在新工单中引用
屏蔽一个用户