feat(reimbursement-small-team-form-validation): 完善小团队报销填报校验
这个提交包含在:
@@ -780,6 +780,78 @@ extra = {
|
||||
};
|
||||
```
|
||||
|
||||
## Scenario: Small-Team Reimbursement Form And Submit Validation
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: changing ordinary reimbursement item editing, total display, submit validation, expense-date warnings, evidence summary or asset-category hints.
|
||||
- This is an internal 5–10 person workflow. Do not turn these rules into a configurable policy center or automatic accounting/asset platform.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Runtime env: `REIMBURSEMENT_MAX_AGE_DAYS=180|365`, default `365`.
|
||||
- Header edit: `PATCH /api/reimbursements/:id` accepts `reimbursementDate` and nullable `description` for participant-owned `draft|rejected` forms.
|
||||
- Item edit: `POST /api/reimbursements/:id/items` and `PATCH /api/reimbursements/:id/items/:itemId` require decimal `amount > 0`.
|
||||
- Submit: `POST /api/reimbursements/:id/submit` locks the reimbursement, reads every item, validates amounts and persists the recomputed `total_amount` before budget and workflow resolution.
|
||||
- Reimbursement precheck adds `evidenceSummary`, `timelinessChecks` and `assetHint`; these operational fields remain active when the budget-precheck feature flag is off.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- PC, mobile and print display the server `totalAmount`; clients never calculate or submit an editable form total.
|
||||
- A template or travel conversion may keep zero-value placeholders in a draft, but submission requires at least one item and every item amount strictly greater than zero.
|
||||
- Date age uses `APP_TIMEZONE` business date plus date-only UTC arithmetic. Exactly 180/365 days is within policy; the first later day is overdue.
|
||||
- Overdue and cross-calendar-year are independent non-blocking warnings. One item may carry both flags and both user messages.
|
||||
- Evidence summary counts bound invoices, whole-form supporting attachments and bound invoices per item. An unlinked item warns but does not add a new block because allowances and whole-form evidence remain valid cases.
|
||||
- `expense_type.category=assets` returns only candidate count and manual-registration guidance. It never creates an asset card; the `/assets` link remains permission-gated.
|
||||
- Do not add a “历史费用补报” field, badge or process. Age and calendar-year facts provide the traceable signal.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Unsupported `REIMBURSEMENT_MAX_AGE_DAYS` -> application startup fails with `Invalid REIMBURSEMENT_MAX_AGE_DAYS`.
|
||||
- Manual item amount `<= 0` -> `400 VALID_001 / amount=must_be_positive`.
|
||||
- Submit with zero items -> `400 VALID_001 / items=at_least_one_required`.
|
||||
- Submit with any item amount `<= 0` -> `400 VALID_001 / items=positive_amount_required` plus affected item IDs.
|
||||
- Expense date older than policy or in another calendar year -> precheck `decision=warn`; submission remains possible after confirmation.
|
||||
- Asset candidate without asset permission -> show manual next step only, never the asset-module link or asset data.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: three positive items display one server total; submit repairs a stale stored total before selecting the approval rule.
|
||||
- Good: a 366-day-old asset item under the 365-day policy shows overdue, cross-year, evidence and manual-asset warnings without creating an asset.
|
||||
- Base: a current office-supply item with one linked invoice passes existing gates and displays its linked count.
|
||||
- Bad: sum item amounts in React, trust a stale `ReimbursementForm.total_amount` during submit, block all old invoices, or auto-create asset cards.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Config tests cover default 365, allowed 180/365 and unsupported values.
|
||||
- Service tests freeze the business time and assert threshold-day, first-overdue-day, cross-year, per-item invoice count, attachment count and asset candidate behavior.
|
||||
- Route tests assert empty/non-positive submit errors and that the recomputed amount reaches approval-rule selection.
|
||||
- Frontend typecheck/build plus PC/mobile Playwright assert header editing, item count, server total, evidence wording and absence of internal terms.
|
||||
- Run Prisma validation, reimbursement focused regression, process/docs/Trellis gates and `git diff --check`.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```js
|
||||
const total = items.reduce((sum, item) => sum + Number(item.amount), 0);
|
||||
await selectApprovalRule(reimbursement.total_amount); // may be stale
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```js
|
||||
const items = await tx.reimbursementItem.findMany({
|
||||
where: { reimbursement_id: reimbursementId },
|
||||
select: { id: true, amount: true },
|
||||
});
|
||||
if (!items.length || items.some((item) => Number(item.amount) <= 0)) {
|
||||
throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400 });
|
||||
}
|
||||
reimbursement.total_amount = await recomputeTotalAmount(tx, reimbursementId, actorId);
|
||||
await selectApprovalRule(reimbursement.total_amount);
|
||||
```
|
||||
|
||||
## Scenario: Payment Batch Receipt Attention Count
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
在新工单中引用
屏蔽一个用户