feat: 完成错误原因合同与付款就绪提示(09-05-error-explainability-payment-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-05 19:00:35 +08:00
父节点 ab82fe30c0
当前提交 a837912638
共修改 42 个文件,包含 2867 行新增和 876 行删除
@@ -23,6 +23,95 @@ QA script patterns over new abstractions.
- `cd server && ./scripts/regression-workflow-actions.sh`
- `cd server && ./scripts/regression-all.sh`
## Scenario: Business Error Reasons And Payment Creation Readiness
### 1. Scope / Trigger
- Adding or changing user-facing business validation, especially reimbursement,
invoice binding/unbinding, payment batches, leave and attendance correction.
- Root cause addressed by task `09-05-error-explainability-payment-readiness`:
server details and UI text evolved independently; successful static coverage
was incorrectly inferred from shared details keys or string counts.
### 2. Signatures
- Registry: `server/src/constants/errorReasons.js`, `ERROR_REASONS`.
- Builder: `buildBusinessValidationError(reasonCode, legacyDetails, statusCode)`.
- Client map: `client/src/api/errorReasons.ts`.
- Gate: `node scripts/qa/check-error-reason-contract.mjs --strict`; negative
subprocess fixtures: the same command with `--self-test`.
- Candidate API accepts `paymentChannel`; each candidate adds
`readiness: { ready, blockers }`.
- `POST /api/payment-batches/readiness` accepts only `entityType`, `entityIds`,
and optional `defaultPaymentChannel`; this is authenticated, manage-authorized
and read-only. Creation remains independently authoritative.
### 3. Contracts
- Keep HTTP status, top-level error code and legacy details. Add the registered
domain-scoped `details.reasonCode`; do not infer business identity from a
details key alone. The builder rejects unregistered reasons.
- **迁移一条遗留原因码 = 服务端注册 + 客户端映射 + 具体内容断言,三项缺一不可。**
Fixed expected text lives in `server/tests/unit/fixtures/error-reason-messages.json`
and is asserted against both registry and client rendering; do not replace
these assertions with broad message regexes or expectations derived at runtime.
- Legacy entries are versioned AST-shape SHA-256 fingerprints plus relative file/operation,
occurrence count, owner, explanation and expiry date (UTC). Whitespace, quotes,
indentation, trailing commas and parenthesization must not change identity.
A changed node is a new obligation; one deletion cannot offset one addition.
- The first-wave route files and invoice bind/unbind/shared item resolver are
zero-exception operations. Do not baseline their new or old generic errors.
- Candidate rows, combination preflight and the locked creation transaction all
consume `resolvePaymentBatchCreationReadiness` from `paymentBatchReadiness.js`.
`loadPaymentCreationFacts` loads sources, reservations, accounts and reviewers
in batches. Department flag reads are memoized only inside that fact load;
do not cache readiness across requests or write readiness to the database.
- Creation takes source locks, reloads facts, runs the resolver and only then
allocates/creates a batch, items, success audit and notifications. Blocked
preflight/create must not emit any of those success side effects.
- Public blockers contain only reason code and optional source association.
Neither decrypted account numbers nor account snapshots enter readiness DTOs.
Bank-channel constraints must not silently become constraints on cash or other
already-supported channels. No bank capability, role or maker-checker changes.
- Execution keeps persisted `failure_code/failure_message` and their API fields
compatible. Store only the registered reason code in the existing execution-risk
`payload_json`, with a safe detail; refresh that open risk's explanatory metadata
inside the same transaction without changing status, creator or notification
rules. This is not a readiness field or a new server logging mechanism.
### 4. Validation & Error Matrix
- Missing source -> `404 VALID_001` on create; invalid input -> `400 VALID_001`.
- Status, occupancy, department, missing company/payee/account, decryption,
cross-company and missing independent reviewer -> safe blockers; create `409`.
- Three synthetic missing fields return three distinct actionable blockers.
- Missing mapping/content fixture, typo, duplicate, changed/new/stale fingerprint,
expired exception or primary-operation exception -> gate non-zero exit.
### 5. Good/Base/Bad Cases
- Good: individually ready records from different companies produce a blocked
combination; changing source/account facts changes the next response.
- Base: complete bank fixture produces `{ ready: true, blockers: [] }` and can
create; a competing reservation after preflight is refused under the lock.
- Bad: hide invalid candidates, copy create conditions into an API/UI, approve
permanent path exemptions, or mark deployment acceptance from local fixtures.
### 6. Tests Required
- Gate self-test must launch failing subprocesses and assert their exit codes,
including formatting stability, one-added/one-removed and primary exceptions.
- Focused route tests assert exact reason, HTTP status and retained details.
- Shared readiness matrix plus route tests cover batch loading, repaired facts,
combination rules, post-lock changes and zero batch/item/audit/notification.
- Keep existing B2, invoice, leave, correction, receipt and maker-checker tests.
### 7. Wrong vs Correct
- Wrong: count generic error strings, or `details.status` as the cross-domain key.
- Correct: register `payment_batch.source_status_invalid`, map and assert its
concrete safe text, and consume the shared readiness result at every boundary.
## Scenario: Prisma Client Defaults In Raw SQL
### 1. Scope / Trigger
@@ -12,6 +12,71 @@ This spec keeps hyb-oa-specific React/Vite, shared-component, business-boundary
and verification contracts. Do not copy broad aesthetic guidance from
`frontend-design` into this file; use the skill directly for design judgment.
## Scenario: Actionable Validation And Selectable Payment Channels
### 1. Scope / Trigger
- New validation feedback, disabled selections, or payment creation controls.
### 2. Signatures
- `resolveErrorMessage(ApiError)` prefers the explicit domain-scoped reason map.
- `PaymentReadiness` is `{ ready, blockers: [{ reasonCode, entityId?, entityCode? }] }`.
- Candidate query identity includes payment channel; combination query identity
includes selected IDs and channel. Never reuse another selection's readiness.
### 3. Contracts
- Unknown `VALID_001` uses actionable safe fallback, with a bounded diagnostic
request ID only when present. Do not render raw message/details, IDs or paths.
- Keep reason registration, client map and fixed concrete-text tests together.
Structured invoice-count rendering remains narrowed and must tolerate malformed
fields without exposing payloads.
- Blocker text stays beside its row/action; use persistent text, `role=alert`
or `aria-live`, and `aria-describedby` on disabled actions. Toast supplements,
never replaces, the persistent explanation. Normal rows add no success noise.
- The payment channel selector is available before any selection, otherwise
bank-blocked rows make other supported channels unreachable.
- A not-ready unselected row cannot be selected; a previously selected row that
becomes invalid can still be deselected. Pending writes may disable edits.
- Pending/error/not-ready combination state disables creation, and the mutation
handler checks the same result. Server create rechecks independently.
- Persisted payment failure summaries are not UI copy. Use the failure projection
in `paymentBatchDisplay.ts`, associating risk reasons by batch ID and item ID;
historical/unknown failure text and execution-risk details use safe fallback.
Do not announce complete success when the execution result is `failed`.
- Only authorized maintainers may receive maintenance links; other roles receive
recovery-owner text. Do not instruct users to falsify their actual bank details.
### 4. Validation & Error Matrix
- Missing company/bank/card or unsupported bank -> exact local row explanation.
- Cross-company combination -> action-local explanation and disabled button.
- Concurrent create refusal -> remain on the page with action-local safe text.
- Unknown reason -> safe fallback, optional request ID, no empty identifier label.
### 5. Good/Base/Bad Cases
- Good: switch to an existing non-bank channel before selection; deselect a row
whose readiness changed without losing the rest of the selection.
- Base: normal bank fixture remains selectable and submits for independent review.
- Bad: hide the channel selector until a selectable bank row is chosen, or disable
an already-selected invalid checkbox permanently.
### 6. Tests Required
- Concrete message fixtures, unknown reasons with/without request IDs, and malformed
structured invoice details; no broad message regex substitutions.
- Local synthetic Playwright covers omissions, channel limits, combination block,
concurrent failure, keyboard selection, accessible descriptions, channel-before-
selection and deselection after invalidation. No real payment data is required.
### 7. Wrong vs Correct
- Wrong: `disabled={!candidateReady}` for every checkbox.
- Correct: block only new invalid selections while preserving deselection, and
share the reason mapping between row, combination and create errors.
## Scenario: PC Shell And Detail Breakpoint Coordination
### 1. Scope / Trigger