feat(detail-page-structure-progress-actions): 统一详情处理进度与操作区
这个提交包含在:
@@ -489,3 +489,74 @@ const label = status === "draft" ? "草稿" : status;
|
||||
{["reviewed", "failed"].includes(batch.status) ? <PaymentRegistrationFields /> : null}
|
||||
{needsBankReceipt ? <UploadReceipt /> : canRegister ? <RegisterPaid /> : null}
|
||||
```
|
||||
|
||||
## Scenario: Shared Payment Processing Projection
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: adding or changing payment-stage labels, status projection, receipt/order guidance, reimbursement progress or payment-batch detail progress.
|
||||
- This is a read-only cross-layer projection. It does not add workflow tasks, alter the payment write state machine or relax receipt/maker-checker gates.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Shared service: `server/src/services/paymentProcessingProgress.js`.
|
||||
- Builder: `buildPaymentProcessingSemantics({ status, createdAt, reviewedAt, executedAt, cancelledAt, cancelledReason, requiresBankReceipt, receiptCount, paymentOrderExportCount })`.
|
||||
- Channel helpers: `resolveItemPaymentChannel(batch, item)` and `requiresBankReceipt(batch)`.
|
||||
- Payment detail: `GET /api/payment-batches/:id` returns `processingProgress.stages[]`; list responses do not return this field.
|
||||
- Stage states: `completed | current | pending | exception | terminated`.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- The shared service is the only runtime source for payment stage keys, labels and `draft|reviewed|executed|failed|cancelled` state projection. Reimbursement and payment-batch routes only assemble their own DTO view, resolve actor names and add permission-gated links.
|
||||
- Reimbursement keeps its single-item `paymentBatchItem` view; payment detail keeps the whole-batch view. Do not replace these with one database-aware DTO builder, and do not add route-local status branches after calling the shared builder.
|
||||
- `requiresBankReceipt` is shared with the execute guard and resolves the effective item channel, including the legacy null-channel reimbursement fallback to `bank`. Receipt count alone must never decide whether a receipt is required.
|
||||
- A reviewed non-bank batch is current at `付款完成` with guidance to register completion. It must not mention uploading a bank receipt or generating a bank order.
|
||||
- A reviewed bank batch with no active receipt distinguishes no order vs order generated; an active receipt changes guidance to registration. Payment-order count affects copy only when `requiresBankReceipt=true`.
|
||||
- `failed` uses `exception`. A cancellation before review makes the review stage `exception` and payment stage `terminated`; a cancellation after review completes review and makes payment `exception`. `terminated` is a neutral stopped state, not pending, completed or failed.
|
||||
- `actorSource=creator|reviewer|executor|canceller` is an internal semantic reference. The shared service never queries employees; each response assembler resolves only a nullable actor name and never exposes actor IDs inside progress.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Unknown/missing status -> created remains completed and downstream stages remain pending; do not guess a current or completed business fact.
|
||||
- `reviewed + requiresBankReceipt=false` -> current payment guidance is registration only, regardless of receipt/order counts.
|
||||
- `reviewed + requiresBankReceipt=true + receiptCount=0 + exportCount=0` -> guide offline payment and receipt upload.
|
||||
- Same with `exportCount>0` -> say the bank order was generated and wait for payment/receipt; do not imply money moved.
|
||||
- `reviewed + requiresBankReceipt=true + receiptCount>0` -> guide registration completion.
|
||||
- `cancelled + reviewedAt=null` -> review `exception`, payment `terminated`; `cancelled + reviewedAt!=null` -> review `completed`, payment `exception`.
|
||||
- Missing actor row or timestamp -> corresponding name/time is null; the detail response still succeeds.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: both pages import the same semantic builder, while the reimbursement route selects two payment stages and the payment route returns all three.
|
||||
- Base: a cash batch without a receipt shows `等待财务登记付款完成` and exposes the permitted registration action.
|
||||
- Bad: a route adds `if (batch.status === "reviewed")` to rewrite stage state/detail, or a client infers receipt requirements from `receiptCount===0`.
|
||||
- Bad: a cancelled batch shows a later `待处理` node, or a cash batch tells finance to upload a bank receipt.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Pure table-driven unit tests cover all five statuses, both cancellation positions, bank/non-bank channels, order count, receipt count and actorSource.
|
||||
- A structural test extracts both route assembler functions and rejects route-local payment status matrices or duplicate `付款复核` / `付款完成` label definitions.
|
||||
- Route tests assert actor name/time projection, list/detail field separation and absence of snapshot, bank ciphertext and actor IDs inside progress.
|
||||
- PC/mobile E2E uses synthetic responses for all five states, verifies `terminated`, bank/non-bank copy, one visible primary action per viewport and upload-to-register in-place switching.
|
||||
- Reimbursement E2E asserts the four-block order and that submit, print, remind, withdraw and delete retain their state/permission, reason/confirmation, request and refresh contracts after action-panel migration.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```js
|
||||
if (batch.status === "reviewed" && receiptCount === 0) {
|
||||
paymentStage.detail = "请上传银行回单";
|
||||
}
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```js
|
||||
const semantics = buildPaymentProcessingSemantics({
|
||||
status: batch.status,
|
||||
requiresBankReceipt: requiresBankReceipt(batch),
|
||||
receiptCount: batch._count?.receipt_attachments ?? 0,
|
||||
paymentOrderExportCount: batch.payment_order_export_count ?? 0,
|
||||
});
|
||||
```
|
||||
|
||||
在新工单中引用
屏蔽一个用户