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
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,126 @@
# Design:错误可解释性与付款就绪治理
## 1. 总体边界
本任务不把全部 `VALID_001` 重构成新异常体系。保留 HTTP status、顶层 code 和既有 details,在已迁移错误中增加稳定 `details.reasonCode`,用注册表、遗留指纹基线和 focused 测试形成只增不退的 ratchet。
```text
服务端业务事实
-> 原因码注册表 / domain validator
-> AppError { code, details: { reasonCode, ...legacyDetails } }
-> ApiError(保留 requestId)
-> 原因码中文映射 / 页面专用投影
-> 就近提示 + 动作禁用 + 安全兜底
```
## 2. 原因码注册表
服务端建立唯一原因码注册表模块,例如 `server/src/constants/errorReasons.js`。每项至少包含:
```js
{
code: "payment_batch.legal_entity_required",
domain: "payment_batch",
audience: "user",
defaultMessage: "报销人尚未维护所属公司,请联系管理员补充后再安排付款。",
recovery: "contact_admin",
}
```
客户端维护显式、可测试的 reasonCode 映射。静态提示可使用注册表默认中文;涉及数量、页面定位或权限差异的提示由窄化 renderer 处理,但必须有默认安全提示。服务端使用常量或经注册表校验的 builder,不散落裸原因码。
兼容响应继续使用 `VALID_001`,并同时保留 legacy details:
```json
{
"error": {
"code": "VALID_001",
"message": "参数校验失败",
"details": {
"reasonCode": "payment_batch.legal_entity_required",
"legalEntity": "single_legal_entity_required"
}
},
"requestId": "..."
}
```
新 UI 不使用该 message 作为用户文案来源。未知 reasonCode 走安全兜底,并由现有 `ApiError.requestId` 附加排障编号。
## 3. Parity 与遗留基线
新增 `scripts/qa/check-error-reason-contract.mjs`,使用仓库已有 parser 解析 JS/TS,而不是按 key 或行号做字符串计数:
1. 注册表唯一、命名和元数据完整;`audience=user` 必须有客户端映射。
2. reason-aware builder 的原因均已注册;客户端无孤儿映射。
3. 泛化 `buildValidationError` / `new AppError(...VALID_001...)` 调用若没有 reasonCode,必须命中版本化遗留调用指纹。
遗留基线放在 QA 合同附近,例如 `scripts/qa/error-reason-legacy-baseline.json`。按业务域/文件组记录 owner、reason、expiresOn 和规范化 AST 节点形状指纹;指纹必须忽略空白、缩进、换行和纯格式化差异,源码文本与行号仅用于报告,不参与身份计算。首批主链路 operation 单列为 zero-exception,命中遗留也失败。self-test 用临时 fixture 验证新增、删除、拼写、结构性指纹变化、纯格式化不变、过期和同 key 跨域。
这避免两类假门禁:只比数量可被“一增一删”绕过;只比 details key 会把不同业务语义误判为同一覆盖。
## 4. 首批迁移边界
首批原因域:
- `reimbursement.*`:提交、草稿编辑、票据关联/恢复。
- `payment_batch.*`:候选/组合 readiness、创建、复核、回单和登记。
- `leave.*`:员工提交路径。
- `attendance_correction.*`:员工/授权应急提交路径。
同一业务后果跨入口复用同一码;不同业务后果即使 legacy key 相同也使用不同码。20 处旧字符串先形成 task-local 处置表:客户端 mock、主链路服务测试、内部/持久化摘要、延期域分别处理。迁移测试断言 exact reasonCode、HTTP status、兼容字段和准确中文,不使用模糊 message 正则。
## 5. 付款 readiness 服务
抽取无 HTTP 展示依赖的 `resolvePaymentBatchCreationReadiness`。输入包括 entity type、entity IDs、付款渠道、maker ID 和当前配置;输出只含安全原因:
```ts
type PaymentReadiness = {
ready: boolean;
blockers: Array<{
reasonCode: PaymentBatchReasonCode;
entityId?: string; // 只用于关联,不直接渲染
entityCode?: string;
}>;
};
```
解析器复用快照、部门 gate、活动占用、付款账户和 reviewer 事实。敏感账户只做存在性、银行机构和可解密判断,响应不带密文、完整账号或原始 employee。
```text
GET candidates
-> 单条 resolver
-> readiness/blockers 决定可选性和提示
POST readiness(只读)
-> 当前多选集合的组合 resolver
-> 跨公司/占用/复核人等组合提示
POST create
-> 事务 + 锁后复用同一 resolver
-> ready=false 抛相同 reasonCode
-> ready=true 才创建 batch/items/audit/notification
```
候选查询必须批量载入必要关系、占用、付款账户和复核人事实,避免在候选循环中产生 N+1。readiness 不落库、不进入长期缓存;资料补齐后刷新查询即可得到新事实。
## 6. UI 投影
依据 `ui-ux-pro-max` 的表单反馈规则:错误就近、提供恢复路径、toast 不作唯一反馈。
- 正常候选保持紧凑,不显示“资料已齐全”。
- 异常候选仍出现在列表中,避免再次形成不可见状态空隙;复选框 disabled,行内展示必要缺项。
- 缺员工资料时说明需管理员维护;仅有维护权限者看到对应入口。
- 非建行时说明当前银行付款渠道限制,不引导伪填。
- 组合 readiness 紧贴主动作,使用 `role=alert`/`aria-live`,按钮以 `aria-describedby` 关联原因。
- create 并发失败复用相同映射留在动作附近;toast 只补充“未创建付款安排”。
- 未知错误显示可行动兜底及可用 request ID,不回显原始 details。
## 7. 兼容、隐私与回滚
- API 是加法变更;旧 code/details 保留,旧客户端继续工作。
- 不记录银行卡、密文、完整账号或真实 payload。原因码是否进入脱敏服务端日志延期评估。
- 不改变付款状态机、maker-checker、角色、通知对象、回单门禁或 B1/B2。
- 无 migration/schema/seed/flag/routing。生产资料补录不属于部署步骤。
- 建议按“原因码/parity、首批迁移、付款 readiness/UI”形成逻辑提交;付款提交可独立回滚且无数据库补偿。
- 部署另行批准;公网只用合成 fixture 验证。不得自动创建、复核或付款 0014。
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,150 @@
# Implementation Plan:错误可解释性与付款就绪治理
## 0. 开工门
- [x] 用户复核并批准 PRD、design、implement。
- [x] 批准后运行 `task.py start`;inline 模式实施前加载 `trellis-before-dev`,不派 implement/check sub-agent。
- [x] 记录开工基线、工作区状态和当前 20 处旧字符串测试命中清单。
## 1. 原因码合同与 parity
1. [x] 新增原因码注册表和 reason-aware validation helper;保留顶层 `VALID_001` 与 legacy details。
2. [x] 新增客户端 reasonCode 映射与未知错误安全兜底,接入 `ApiError.requestId`。
3. [x] 建立 AST/结构化 parity 检查和版本化遗留指纹:基于规范化 AST 节点形状而非源码文本,纯格式化不得改变指纹;注册唯一性、服务端使用、客户端覆盖、孤儿映射、新增遗留、过期例外全部 fail closed。
4. [x] self-test 覆盖同 key 跨域、未注册、拼写、缺映射、一增一删、结构性指纹变化、纯格式化不变、到期和主链路试图加例外。
5. [x] 接入既有治理入口;报告只输出相对文件、原因码/域、计数和稳定 finding。
## 2. 首批主链路迁移
1. [x] 迁移报销提交/编辑/票据关联的用户可达校验。
2. [x] 迁移付款创建/复核/回单/登记的用户可达校验。
3. [x] 迁移请假提交、考勤修正提交的用户可达校验。
4. [x] 主链路达到零例外;其他存量按域记录 owner/reason/expiresOn,不使用永久目录豁免。
5. [x] 逐项处置 20 处旧字符串:
- 客户端 4 个 mock 改为 reasonCode,并断言具体提示/安全兜底;
- 主链路服务测试断言 exact reasonCode、status 和兼容 details;
- workflow utility、ERP procurement、budget 等逐项迁移或登记有效遗留;
- `failure_message`、批处理 reason 等持久化/内部摘要单独验证兼容,不与 UI 文案混同。
## 3. 付款 readiness
1. [x] 抽取共享 resolver,复用快照、部门 gate、账户、占用和 reviewer 事实;搬移 create 条件而非复制。
2. [x] 候选批量计算单条 readiness,避免 N+1;异常候选仍返回但不可选择。
3. [x] 增加多选组合只读 readiness API;严格输入,响应仅含安全 blocker。
4. [x] create 在锁和事务内复用 resolver;失败零 batch/item/成功 audit/notification 副作用。
5. [x] 覆盖资料或账户变化后的即时重算,不新增持久字段或长期缓存。
## 4. 付款页面
1. [x] 扩展 API types;候选行显示就近 blocker,正常行无解释噪音。
2. [x] 异常候选不可选择并具备可访问原因;有维护权限才显示维护入口。
3. [x] 多选变化查询组合 readiness;提示紧贴主按钮,按钮禁用、handler 和 create 二次拦截消费同一原因映射。
4. [x] 并发失败留在动作附近;toast 为补充;未知原因使用 request ID 安全兜底。
5. [x] PC 覆盖主路径;移动端保持现有能力边界且不显示技术错误。
## 5. 验证
- [x] `node scripts/qa/check-error-reason-contract.mjs --self-test` 与 strict 正常扫描。
- [x] Server:registry/helper、付款 readiness/route、报销/B2、请假、考勤 focused Vitest;涉及 workflow/状态机时做只读 review。
- [x] Client:error mapping 测试;付款 Playwright 覆盖正常、三类 0014 缺项、非建行、跨公司、占用、无 reviewer、并发、未知 request ID fallback、键盘/ARIA。
- [x] `cd server && npm run prisma:validate`;触达 JS 运行 `node --check`。
- [x] `cd client && npm run typecheck && npm run build`。
- [x] UI strict/self-test、Trellis governance、敏感信息扫描、`git diff --check`。
- [x] 核对 schema/migration/seed/flag/routing 和生产数据修改为零。
## 6. Spec、提交与部署
- [x] 实现验证稳定后,把“原因码 + 遗留指纹 + 主链路零例外”和“共享 readiness 多消费者”沉淀到现有 spec,不新建泛化设计文档。
- [x] spec 明确迁移一条遗留原因码的完整合同:服务端注册、客户端映射、具体内容断言三件缺一不可。
- [ ] 提交建议:A 原因码/parity;B 首批迁移;C 付款 readiness/UI;task 元数据另行回填。
- [ ] commit message 使用中文并引用 `[09-05-error-explainability-payment-readiness]`。
- [ ] commit、push、provenance、部署分别批准;切换前再次运行 UI strict。
- [ ] 部署保持 `SYNC_ENV=0`、existing 数据合同,无 migration/schema/seed/flag/routing/资料补录。
- [ ] 部署后核版本、公网文案、合成 readiness/错误投影、B2、maker-checker 和真人单据状态。
- [ ] 不自动创建 0014 批次、不复核、不登记付款;真实业务由授权人员完成。
- [x] 回滚无需数据库补偿;新客户端遇旧服务端未知码仍须显示可行动兜底。
## 7. 开工基线与 20 处旧断言处置
2026-09-05 恢复实施;开工 HEAD 为 `ab82fe3`,仅当前 task 目录未跟踪并已保留。
依据交接中的明确批准幂等执行 `task.py start`;本会话主会话实施/检查,没有派 implement/check agent。
原生 `update_plan` 未在工具集中提供,未创建替代 plan;继续以原已批准清单执行。
下表行号是开工前定位,不以当前字符串总数作为完成证据。兼容响应的原始 message
仍可以出现在测试 fixture;用户展示断言必须具体,不能用宽泛正则掩盖。
延期域均由 `shunli` 负责,遗留 AST 基线到期日为 **2026-10-05(UTC)**;未增添主链路例外。
| 编号 | 开工前测试定位 | 逐项处置 |
| --- | --- | --- |
| T01 | `client/tests/e2e/r103-cross-page-ui-consistency.spec.ts:544` | 采购附件 mock 增加 `purchase_attachment.unsupported_type`;对应服务端分支、固定中文断言和安全上传浏览器断言一并保留。 |
| T02 | `client/tests/e2e/r111-invoice-reimbursement-guard.spec.ts:448` | 提交 mock 改为 `reimbursement.items_invoice_item_guard_failed`;保留四类精确数量解释、异常字段安全兜底与双端就近提示断言。 |
| T03 | `client/tests/e2e/r111-invoice-reimbursement-guard.spec.ts:481` | 明细关联 mock 改为 `reimbursement.invoice_invalid_unlinked_item_state`;保留准确中文和原子关联断言。 |
| T04 | `client/tests/e2e/pc-finance-admin-visual-wave4.spec.ts:83` | 核验 mock 分别登记频控/次数上限原因码;服务端注册、客户端映射、固定内容和 route details 精确断言齐全;旧 `VALID_001:` UI 前缀断言改为无技术前缀的准确中文,不用正则。 |
| T05 | `server/tests/unit/utils/workflow.test.js:64` | workflow 配置工具延期;保留原准确消息断言,对应 `utils/workflow.js` 到期指纹。 |
| T06 | `server/tests/unit/utils/workflow.test.js:65` | workflow 空步骤配置延期;保留原断言,对应同文件独立调用指纹。 |
| T07 | `server/tests/unit/utils/workflow.test.js:87` | approval-rule 配置工具延期;保留原断言及到期指纹。 |
| T08 | `server/tests/unit/utils/workflow.test.js:94` | approval-rule 无效配置分支延期;保留原断言及到期指纹。 |
| T09 | `server/tests/unit/utils/workflow.test.js:112` | approval-rule 结构配置分支延期;保留原断言及到期指纹。 |
| T10 | `server/tests/unit/utils/workflow.test.js:331` | 缺少 assignee rule 配置延期;保留原断言及到期指纹。 |
| T11 | `server/tests/unit/utils/workflow.test.js:332` | 未知 assignee rule 类型延期;保留原断言及到期指纹。 |
| T12 | `server/tests/unit/services/leave.test.js:50` | 改为精确 `VALID_001`、422、`leave.leave_invalid_range` 与兼容 `leave: invalid_range`。 |
| T13 | `server/tests/unit/services/leave.test.js:98` | 改为精确 `VALID_001`、422、`leave.leave_non_working_range` 与兼容 `leave: non_working_range`。 |
| T14 | `server/tests/unit/routes/integrations.erp.procurement.test.js:49` | ERP procurement 非首批域延期;保留原消息断言与 integrations 模块到期指纹。 |
| T15 | `server/tests/unit/routes/integrations.erp.procurement.test.js:57` | ERP procurement 另一资格分支延期;保留原断言,不扩展集成能力。 |
| T16 | `server/tests/unit/routes/paymentBatches.reimbursement.test.js:2341` | 内部持久化 `failure_code/failure_message` 及 API 字段兼容断言保留;执行风险既有 JSON 仅附加注册原因码、刷新安全说明,不改风险状态;明细/风险卡经专用 helper 展示,历史与未知原因安全兜底,不直接展示原摘要。 |
| T17 | `server/tests/unit/routes/workflow.batch.test.js:377` | 内部批处理 reason 兼容断言原样保留;不把批处理摘要冒充用户恢复文案。 |
| T18 | `server/tests/unit/routes/workflow.permissions.test.js:172` | workflow 管理查询域延期;保留准确消息与 `routes/workflow.js` 到期指纹。 |
| T19 | `server/tests/unit/budgetPool.service.test.js:28` | budget scope 配置域延期;保留原断言与 `services/budgetPool.js` 到期指纹。 |
| T20 | `server/tests/unit/budgetPool.service.test.js:75` | budget pool 候选配置域延期;保留原断言及到期指纹,不变更预算规则。 |
## 8. 实施与只读复核记录
- 原因码合同接入既有 `run-release-gate.sh` static 阶段;主链路包含 invoice bind/unbind
与共享 item resolver,不借整目录永久豁免绕过。
- 候选、组合预检和创建事务复用 `loadPaymentCreationFacts` 与
`resolvePaymentBatchCreationReadiness`;候选关系、占用、公司账户、复核人批量读取。
部门 gate 仅在单次事实加载内复用底层 flag 查询,仍调用原判定实现;没有修改 flag。
- 只读 reviewer 首轮发现两项 P2:未选中时无法切换付款渠道、失效已选项无法取消。
已分别移出渠道选择器的选中条件、允许失效已选项取消,并补浏览器回归。
第二轮只读 review 确认这两项闭环及请求内 gate 查询复用;无新增高置信问题。
- 系统 Python 缺 `xlwt` 导致既有银行模板导出测试失败;使用现有隔离测试环境提供的
Python、通过 `PAYMENT_ORDER_PYTHON_BIN` 运行后恢复通过。未改依赖清单或生产配置。
- 首轮扩大浏览器运行另包含一个合同只读用例超时,未完成该项归因,未改合同产品逻辑或该断言;
付款、B2、采购与本轮核验反馈改为明确 focused 集重跑,不把无关失败标成通过。
- 已更新现有 backend/frontend spec;根因归类为跨层合同和传播缺失,而非真实资料补录。
- 本轮不提交、不 push、不部署、不归档;真实资料和 0014 的任何业务写入均未执行。
部署后 AC12 仍须独立批准与公网证据,不能用本地合成结果替代。
- 最终检查进一步发现两处付款明细直出旧失败摘要,已按 T16 补齐安全展示转换;
新原因只保存在既有执行风险 JSON 元数据中,未新增 schema、readiness 持久字段或日志机制。
追加只读 review 确认历史兼容、事务和风险状态边界,无新增高置信问题。
## 9. 本地验证结果(2026-09-05)
| 验证 | 结果与边界 |
| --- | --- |
| AST strict | PASS;142 注册原因、199 已迁移使用点、695 遗留调用、主链路例外 0、finding 0。遗留身份同时包含相对文件、operation 和规范化 AST 形状,不能靠移动操作或一增一删抵消。 |
| AST self-test | 20 项 PASS;正常跨域与纯格式化退出 0,其余拒绝场景退出 1,包括六类必查失败、主链路加例外、同数替换、别名、具体内容断言缺失、operation 移动、条件式通用码和不安全文案。 |
| 扩展服务端 focused | 23 文件 / 407 用例 PASS:报销、票据、请假、考勤、付款、readiness、registry/helper 和客户端投影。 |
| 后续兼容/新增边界 focused | 8 文件 / 309 用例 PASS:包含当轮 75 个付款 route 用例、149 个具体内容/安全投影用例、采购附件和延期域/内部摘要兼容测试;与上一行有交集,不合并虚报数量。 |
| 最终摘要投影 focused | 4 文件 / 244 用例 PASS:76 付款 route、150 内容/投影、16 readiness、2 helper;新增既有风险元数据更新与历史摘要保护,和前述测试有交集。 |
| Playwright focused | 18 个不同用例 PASS(原 17 项 + 最后追加的失败摘要保护 1 项):银行正常提交、现金渠道、三类缺项、非建行、跨公司、占用、无 reviewer、创建并发、移动未知原因排障编号、键盘/ARIA、渠道可达与失效项取消、B2 双端、采购附件、核验错误、PC/移动视口历史失败明细及风险保护。仅本地全合成 API mock。 |
| 客户端工程 | `npm run typecheck`、`npm run build` PASS;未部署构建产物。 |
| 服务端工程 | Prisma validate、触达 JS/MJS `node --check` PASS;没有执行 migration 或数据库重置。 |
| 治理 | UI strict/self-test、process baseline、docs registry、Trellis validate/governance、敏感扫描、`git diff --check` PASS。 |
| 只读 review | 首轮两项 P2 已修复;第二轮确认闭环;追加摘要投影与风险元数据复核无新增高置信问题。Reviewer 未写文件、未操作真实数据。 |
| 边界核对 | schema/migration/seed/flag 实现、前端 routing、真实资料与真实付款写入均为 0;0014 未操作。 |
复跑入口:`node scripts/qa/check-error-reason-contract.mjs --strict` 与 `--self-test`;
服务端测试中的银行模板导出需把 `PAYMENT_ORDER_PYTHON_BIN` 指向满足现有锁定依赖的隔离测试 Python。
浏览器 focused 文件为 `payment-reviewer-permissions.spec.ts`、`r111-invoice-reimbursement-guard.spec.ts`、
`r103-cross-page-ui-consistency.spec.ts` 和 `pc-finance-admin-visual-wave4.spec.ts`,筛选本表相关用例;
不得对公网或真实账户套用这些 mock 场景。
## 10. 独立复核、遗留验证事项与发布授权(2026-09-05)
- 用户独立复核 PASS:真实调用点注入未注册原因码时 strict 退出 1;向主链路新增例外时 strict 退出 1;确认 readiness 三处共用、20 项 self-test 及格式化指纹稳定。用户确认探针全部还原,基线校验与备份一致。
- 两个 task 已按完整文件物理分离;本目录仅保留错误可解释性与付款 readiness,Trellis 升级在独立 worktree 待单独审查。原治理文件均与开工 HEAD 一致,分离后原因码及治理门禁通过。
- 未解决验证事项:首轮扩大浏览器运行的合同只读用例超时,**未归因、不计入通过结果**,不是已确认的产品缺陷或可永久忽略的 flaky。后续合同域验证前必须单独复现,补充精确用例、失败步骤及环境证据后归类;本任务不得通过放宽断言、重试至绿或混入 focused PASS 关闭它。
- 用户明确批准本任务提交、push、部署;部署后核验 `/ready` 与 bundle marker、文案扫描零 findings、0014 候选禁用及缺项原因、创建接口 fail-closed、B2 保持 force_on + global。
- 生产核验只使用最小脱敏结果,不读取或修改真实银行卡资料;不补录 0014,不执行真实付款。若前置条件变化可能导致创建成功,停止强行创建探针。
- Task 保持 `in_progress`;独立复核通过不等于公网 AC12 完成。发布和公网证据待实际执行后回填,不提前标记 PASS,不归档 Trellis 升级任务。
@@ -0,0 +1,94 @@
# 错误可解释性与付款就绪治理
## Goal
把“系统知道为什么不能继续、真人只看到参数校验失败”的缺陷从逐页补文案改成可持续的错误合同,并以付款批次为第一个完整样板:财务在点击前就知道付款资料是否齐全、缺什么、由谁补;点击后的服务端二次校验与页面提示必须来自同一份实时判断。
## Background
- 2026-09-05,真实单据 `0014` 在生产付款页已能进入待处理队列,但财务选择银行转账并点击“创建并提交复核”后只看到“参数校验失败”。只读诊断确认请求未创建付款批次;报销人缺所属公司、开户银行和银行卡号,公司侧已有有效建行付款账户,经理/CEO 复核人存在。
- 服务端创建前已检查来源状态、公司归属、收款账户、公司付款账户、活动批次占用和复核人;客户端未覆盖付款原因,因此回退到通用技术文案。
- 这是十一天内第三次“参数校验失败”阻断真人主流程。此前 B2 已验证“一份判断同时驱动展示、点击拦截和 mutation 二次拦截”能防漂移;本任务把该模式推广到付款就绪。
- 当前文本基线:服务端同一通用校验文案 365 处,`buildValidationError(` 518 次,测试目录中该字符串 20 次。数量用于确定渐进迁移边界,不代表独立业务原因数量。
- `details key` 不能作为原因合同:同一个 `legalEntity`、`status` 或 `review` 在不同业务域含义不同。稳定合同必须是带业务域的原因码,例如 `payment_batch.legal_entity_required`。
- 生产资料补录和真实付款继续由授权人员操作;本任务不得读取、记录或修改真实银行卡号,不把伪填建行资料作为恢复办法。
## Scope
本 task 包含三项互相支撑但可独立回滚的交付:稳定业务原因码及增量 parity 门禁、首批真人主链路错误迁移、付款批次实时 readiness 与就近反馈。它不建设统一待办,也不扩展银行付款能力。
## Requirements
### R1 稳定原因码与渐进 parity 门禁
1. 新增服务端业务错误原因注册表。每个原因至少记录稳定 `reasonCode`、业务域、普通用户是否可达、默认安全中文提示/恢复方向;原因码采用 `<domain>.<reason>`,不得复用任意 `details key` 冒充合同。
2. 原有 HTTP status、顶层 `error.code` 和兼容 `details` 保持;已迁移错误在 `details.reasonCode` 增加稳定原因码。旧客户端可继续依赖既有字段,新客户端优先消费原因码。
3. 新增 parity 检查:已注册用户原因必须有客户端安全映射;服务端使用的原因必须已注册;新增或修改通用校验不得绕过原因码;客户端不得有孤儿/拼写漂移;未知原因走安全兜底且不展示原始 details、内部路径或 UUID。
4. 存量通用错误使用版本化遗留基线渐进收敛。例外按业务域/文件组记录原因、负责人、到期日和行号无关的调用指纹;新增指纹默认失败。不得以整目录永久忽略、只比较数量或模糊正则代替指纹门禁。
5. 首批真人主链路不得进入例外:报销提交/编辑/票据关联,付款创建/复核/登记,请假提交和考勤修正提交。范围内每个用户可达校验分支必须有注册原因码、准确中文提示和测试。
6. 遗留例外是迁移债,不是管理页永久展示技术文案的许可。到期后门禁必须失败;续期需要新理由和明确期限。
### R2 用户可行动的错误反馈
1. `VALID_001` 未映射时不得再把服务端的“参数校验失败”直接展示给普通用户。兜底必须说明操作未完成、建议核对页面提示或联系管理员,并附现有 request ID;没有 request ID 时不展示空编号。
2. 具体提示必须回答“哪里不满足、下一步怎么恢复”,并位于相关字段、条目或动作附近。页面顶部错误和 toast 只作补充。
3. 错误不得只靠红色表达;动态错误使用可访问告警语义。禁用动作的原因必须在按钮附近持续可见,并通过 `aria-describedby` 或等价语义关联。
4. 20 处现有测试字符串逐处处置,不用宽泛正则消除失败:客户端 4 处模拟响应改为原因码 fixture;首批主链路改断言 reasonCode、兼容 details 和 HTTP status;非本批路径明确对应遗留例外和到期日;持久化失败摘要/批处理 reason 单独判断兼容性。
### R3 付款批次实时 readiness
1. 抽取唯一服务端付款就绪解析器,返回 `{ ready, blockers }`。blocker 使用 `payment_batch.*` 原因码和最小安全定位信息,不返回完整银行卡、密文或原始员工对象。
2. readiness 至少覆盖:来源状态/已付款、付款能力部门门禁、活动批次占用、单一公司归属、收款人银行资料、公司付款账户及解密可用性、非制单人的合格经理/CEO复核人。
3. 候选列表对单条候选实时计算 readiness;缺资料的记录仍可被发现,但不可选择,并就近显示缺项与恢复责任。财务无员工维护权限时只提示联系管理员,不暴露无权入口。
4. 多选付款必须用同一解析器计算组合 readiness,覆盖跨公司、并发占用和复核人等组合条件;不能因为每条单独就绪就推断组合一定就绪。
5. 创建接口在事务、必要锁和最新数据下再次调用同一解析器并 fail closed。候选、组合预检和创建不得各写一套判断;并发变化返回同一原因码。
6. readiness 每次按当前事实派生,不新增数据库字段、不落库、不长期缓存。资料或账户变化后,下次查询自然反映最新结果。
7. 银行转账只按真实资料判断。开户行不是建行时不得建议伪填;是否支持跨行付款属于独立产品能力。
### R4 付款页面反馈
1. 正常候选不增加解释性噪音;有 blocker 时在候选行展示精确缺项,复选框不可用且原因可被键盘和读屏取得。
2. 已选组合 readiness 位于主动作紧邻上方;阻断时主动作禁用,提示与服务端二次拦截来自同一组 reasonCode 投影。
3. 正常银行付款 fixture 保持可选择、可创建并提交复核;新增门禁不得误伤资料与复核人完整的单据。
4. PC 是批量付款主路径;移动端不新增复杂组批能力,但已有移动付款路径不得显示原始技术错误或回归。
## Acceptance Criteria
- [x] AC1:存在可机器读取的原因码注册表;同一 details key 在两个业务域使用不同 reasonCode,parity 分别验证,证明不是按 key 虚假覆盖。
- [x] AC2:未注册原因、缺客户端映射、拼错 reasonCode、新增无原因码调用、遗留指纹变化、例外过期六类 self-test 均非零退出;正常注册与有效遗留通过。
- [x] AC3:报销提交/编辑/票据、付款创建/复核/登记、请假提交、考勤修正提交的用户可达错误为零例外;报告按域输出注册、已迁移、遗留和过期数量。
- [x] AC4:未知 `VALID_001` 在 PC/移动显示安全可行动兜底;有 request ID 时展示编号,无编号时无空占位;“参数校验失败”、原始 details、UUID、接口路径不出现在 UI。
- [x] AC5:20 处既有测试字符串有逐项处置清单;主链路断言具体原因码/中文,非本批项对应有效例外,无宽泛正则掩盖差异。
- [x] AC6:付款单条与组合 readiness 覆盖正常、状态异常、活动批次、缺公司、缺/非建行收款资料、缺/不可解密公司账户、跨公司、无复核人;准确返回原因码,正常 fixture `ready=true`。
- [x] AC7:候选、组合预检和创建事务调用同一 readiness 实现;并发变化时创建再次阻断且不生成 batch、item、成功 audit 或通知。
- [x] AC8:正常候选无 readiness 噪音;异常候选不可选择且缺项就近可读;组合原因紧贴禁用主动作,toast 非唯一反馈,键盘/读屏语义通过。
- [x] AC9:员工资料或公司账户变化后下一次 readiness 立即变化;schema、migration 和持久化 readiness 字段为零。
- [x] AC10:用受控 fixture 复现 0014 三类缺项,分别提示所属公司、开户银行/银行卡和渠道限制;不使用或修改真实银行卡资料。
- [x] AC11:相关服务端、客户端单元/Playwright、typecheck/build、Prisma validate、UI strict/self-test、Trellis governance、敏感扫描和 `git diff --check` 全部通过。
- [ ] AC12(部署后):`/ready`、bundle marker、公网文案扫描通过;合成 API/页面证明 readiness 与原因码解析生效。真实资料补录和 0014 重试由授权人员完成,未取得真实证据时不得冒充 PASS。
## Test Plan
- 原因码合同:运行 parity strict 检查及 self-test,覆盖注册表唯一性、同一 details key 跨域、未注册原因、缺客户端映射、孤儿/拼写漂移、新增无原因码调用、遗留指纹变化、例外过期,以及首批主链路试图加入例外时 fail closed。
- 首批主链路:对报销提交/编辑/票据关联、付款创建/复核/登记、请假提交、考勤修正提交运行 focused 服务端测试;逐分支断言 HTTP status、稳定 `reasonCode`、兼容 details 和准确中文映射,并验证主链路遗留例外为零。
- 付款 readiness:用受控 fixture 覆盖正常、状态异常、活动批次占用、缺所属公司、缺开户银行/银行卡、非建行、缺或不可解密公司账户、跨公司、无合格复核人;分别验证单条和组合投影,并以正常 fixture 证明可选择、可创建和提交复核不被误伤。
- 事务与实时性:模拟候选读取后事实变化,确认创建事务重新校验、失败时不产生 batch/item/成功 audit/notification;修改受控员工资料或公司账户后重新查询,确认 readiness 即时变化且无持久化字段或缓存依赖。
- 客户端与可访问性:逐项处置现有 20 处“参数校验失败”测试命中;断言具体原因就近显示、禁用按钮关联 `aria-describedby` 或等价语义、toast 非唯一反馈、未知原因不泄露 details/UUID/路径,有 request ID 时显示排障编号、无编号时不留空占位;不得以宽泛正则替代精确断言。
- 工程门禁:运行相关 server/client 单元测试与 Playwright、`npm run prisma:validate`、前端 typecheck/build、UI strict/self-test、Trellis governance、敏感信息扫描和 `git diff --check`,并确认 schema/migration/seed/flag/routing 与生产数据修改均为零。
- 部署后验收:在另行批准部署后核对 `/ready`、bundle marker 和公网文案扫描;仅用合成 API/页面样本验证三类 0014 缺项、正常付款路径及 B2/maker-checker 回归。真实银行卡资料、0014 建批/复核/登记必须由授权人员操作;缺少真实链路证据的项目明确记为 BLOCKED 或待自然样本验证,不以合成测试冒充 PASS。
## Out of Scope
- 统一首页待办、通知推导和角色行动项模型;等待真实付款链路记录后另行规划。
- 一次性迁移全部 365 处通用文案或 518 个调用文本;非首批路径只建立可到期遗留基线。
- 支持跨行付款、银行直连、自动付款,或改变 maker-checker、付款角色、复核规则。
- 自动补录或读取生产员工公司、开户银行、银行卡号和公司账户。
- 修改业务状态机,新增 schema/migration/seed/flag/routing。
- 把完整 details、payload、UUID、账号或内部路径写入日志、任务材料或普通页面。
## Risks and Deferred Items
- 静态扫描只证明合同登记;focused 分支测试证明真实返回,二者缺一不可。
- 单条与组合 readiness 语义不同;只做前者仍留下跨公司等点击后失败路径,因此组合预检是必交项。
- request ID 可帮助定位,但当前 AppError details 不进服务端日志。是否增加脱敏 reasonCode 日志另行评估,本任务不记录原始 details。
- 0014 只证明资料未维护,不证明员工没有建行卡;真实开户行由本人和授权管理员确认。
@@ -0,0 +1,33 @@
{
"id": "error-explainability-payment-readiness",
"name": "error-explainability-payment-readiness",
"title": "错误可解释性与付款就绪治理",
"description": "建立稳定业务错误原因码与渐进 parity 门禁,并以付款批次实时 readiness 消除点击后才暴露的资料阻断。",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P0",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-09-05",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [
"server/src/routes/paymentBatches.js",
"server/src/utils/AppError.js",
"client/src/api/errorMessages.ts",
"client/src/lib/resolveErrorMessage.ts",
"client/src/pages/PaymentBatchesPage.tsx",
"scripts/qa"
],
"notes": "来源:真实单据 0014 创建付款批次时第三次出现‘参数校验失败’;生产只读诊断确认未创建批次,员工付款资料缺失且错误未被客户端解释。2026-09-05 最终规划已获用户批准实施;交接复核重点:门禁六类 self-test 真失败、主链路零例外且禁止新增例外、付款候选与创建共享同一 readiness resolver、20 处旧断言逐项处置且不得使用宽泛正则。0014 真实资料补录与付款不属于本 task 自动操作范围。",
"meta": {}
}
+9 -2
查看文件
@@ -1,4 +1,5 @@
import { formatInvoiceOcrFieldLabels } from "../lib/invoiceOcr";
import { resolveErrorReasonMessage, UNKNOWN_VALIDATION_MESSAGE } from "./errorReasons";
function readStringList(value: unknown): string[] {
if (!Array.isArray(value)) {
@@ -71,7 +72,13 @@ export function mapApiErrorMessage(
code: string,
details?: Record<string, unknown>,
fallbackMessage?: string,
) {
): string {
if (code === "VALID_001" && details?.reasonCode === "reimbursement.items_invoice_item_guard_failed") {
return mapApiErrorMessage(code, { ...details, reasonCode: undefined, items: "invoice_item_guard_failed" });
}
if (code === "VALID_001" && details?.reasonCode !== undefined) {
return resolveErrorReasonMessage(details.reasonCode) ?? UNKNOWN_VALIDATION_MESSAGE;
}
if (code === "AUTH_001") {
return "账号或密码错误,请检查后重试。";
}
@@ -512,5 +519,5 @@ export function mapApiErrorMessage(
}
}
return fallbackMessage || "请求失败,请稍后重试。";
return code === "VALID_001" ? UNKNOWN_VALIDATION_MESSAGE : fallbackMessage || "请求失败,请稍后重试。";
}
+152
查看文件
@@ -0,0 +1,152 @@
export const ERROR_REASON_MESSAGES: Record<string, string> = {
"reimbursement.material_preview_limit": "报销材料较多,暂时无法连续预览,请分批查看材料或联系管理员协助。",
"purchase_attachment.unsupported_type": "仅支持 PDF、JPG、PNG、WEBP 文件,请重新选择。",
"reimbursement.invoice_amount_required": "票据金额尚未确认,无法自动生成明细,请先核对票据金额或手动选择明细。",
"reimbursement.expense_type_required": "当前没有可用费用类型,请联系管理员维护后再添加票据。",
"reimbursement.invoice_not_bound": "该票据已不再关联业务单据,请刷新票据列表后确认。",
"reimbursement.invoice_unbind_status_invalid": "报销单当前不能移出票据,请先核实审批和付款状态。",
"reimbursement.invoice_bind_status_invalid": "报销单当前不能添加票据,请刷新并确认单据状态。",
"reimbursement.invoice_owner_mismatch": "票据所属人与报销人不一致,请选择本人对应的票据。",
"reimbursement.invoice_item_required": "请为该票据选择对应的报销明细后再添加。",
"reimbursement.travel_invoice_bind_status_invalid": "差旅订单当前不支持添加票据,请核实订单状态。",
"invoice_verification.too_frequent": "验真触发过于频繁,请稍后重试。",
"invoice_verification.attempts_exceeded": "验真重试次数已达上限,请联系财务人工核验。",
"reimbursement.invalid_input": "报销内容不完整或格式不正确,请核对页面必填项和日期后重试。",
"reimbursement.invalid_uuid": "记录标识已失效,请刷新页面后重新选择。",
"reimbursement.body_empty": "尚未填写修改内容,请补充后再保存。",
"reimbursement.date_to_must_be_after_date_from": "结束日期不能早于开始日期,请调整查询日期范围。",
"reimbursement.amount_must_be_positive": "明细金额必须大于 0。",
"reimbursement.template": "模板中的明细格式已不适用,请联系管理员修正模板后重试。",
"reimbursement.owner_employee_id_invalid_owner": "所选报销人不可用,请重新选择有效员工。",
"reimbursement.total_amount_invalid": "报销合计金额不正确,请核对各项明细金额。",
"reimbursement.total_amount_negative_not_allowed": "报销合计不能为负数,请核对各项明细金额。",
"reimbursement.workflow_missing_approval_rule": "尚未配置适用的报销审批规则,请联系管理员维护后重试。",
"reimbursement.precheck": "报销提交检查未通过,请按页面检查清单逐项处理后重试。",
"reimbursement.template_id_invalid_template": "所选报销模板已失效,请重新选择有效模板。",
"reimbursement.template_id_contains_disabled_expense_type": "模板包含已停用的费用类型,请联系管理员修正模板后重试。",
"reimbursement.linked_travel_request_id_not_found": "所选出差申请已不存在,请刷新后重新选择。",
"reimbursement.linked_travel_request_id_not_approved": "所选出差申请尚未获批,请先完成出差审批。",
"reimbursement.linked_travel_request_id_owner_mismatch": "出差申请与报销人不一致,请选择本人对应的出差申请。",
"reimbursement.linked_travel_request_id_already_linked": "该出差申请已关联其他报销单,请核对已有报销记录。",
"reimbursement.linked_travel_request_id_missing_expense_type_mapping": "该出差项目尚未配置对应费用类型,请联系管理员维护后重试。",
"reimbursement.status_invalid_edit_status": "当前报销单状态不允许编辑,请刷新并确认审批进度。",
"reimbursement.travel_end_date_invalid_date_range": "出差结束日期不能早于开始日期,请调整日期。",
"reimbursement.status_invalid_delete_status": "当前报销单状态不允许删除,请核实审批和付款进度。",
"reimbursement.status_invalid_attachment_edit_status": "当前报销单不能修改附件,请刷新并确认单据状态。",
"reimbursement.file_unsupported_mime_type": "附件类型不支持,请选择允许的图片或文档格式。",
"reimbursement.file_file_too_large": "附件超过大小限制,请压缩或更换文件后重试。",
"reimbursement.file_empty_file": "所选附件为空,请重新选择有效文件。",
"reimbursement.file_too_many_files": "一次最多上传 5 个附件,请减少数量后重试。",
"reimbursement.file_missing": "请先选择需要上传的附件。",
"reimbursement.reason_required": "读取该材料需要填写用途,请补充访问原因。",
"reimbursement.status_invalid_item_edit_status": "当前报销单不能修改明细,请刷新并确认单据状态。",
"reimbursement.expense_type_id_invalid_or_disabled": "所选费用类型已失效或停用,请重新选择。",
"reimbursement.reimbursement_invalid_status_for_invoice_item_link": "当前报销单状态已变化,不能再关联发票,请刷新页面。",
"reimbursement.invoice_invalid_unlinked_item_state": "这张发票的关联状态已变化,请刷新后重试。",
"reimbursement.linked_item_id_item_not_in_reimbursement": "所选明细已不存在或不属于本单,请刷新后重新选择。",
"reimbursement.status_invalid_submit_status": "当前报销单状态不允许提交,请刷新并确认是否已提交。",
"reimbursement.items_at_least_one_required": "请先添加至少 1 条报销明细。",
"reimbursement.items_positive_amount_required": "所有报销明细金额都必须大于 0,请修改后再提交。",
"reimbursement.items_invoice_item_guard_failed": "票据检查未通过,请核对每条明细与发票的关联和金额后重试。",
"reimbursement.travel_summary_incomplete": "出差汇总信息尚未填写完整,请补充页面提示的出差信息后提交。",
"reimbursement.submit_already_in_review": "该报销单已经在审批中,请勿重复提交,可查看审批进度。",
"reimbursement.invoices_bound_invoice_required": "报销单需要有效发票,请先添加并关联发票后提交。",
"reimbursement.budget_exceeded_block_mode": "本次报销超出可用预算,请调整金额或联系预算管理员处理。",
"reimbursement.workflow_missing_definition": "尚未配置可用报销审批流程,请联系管理员维护后重试。",
"reimbursement.workflow_no_actionable_approver": "当前没有可处理该报销的审批人,请联系管理员核实审批配置。",
"reimbursement.status_invalid_withdraw_status": "当前报销单不能撤回,请刷新并核实审批进度。",
"reimbursement.withdraw_no_pending_instance": "当前报销单没有可撤回的审批,请刷新并核实进度。",
"reimbursement.withdraw_already_processed_by_approver": "审批人已处理该报销单,不能撤回,请联系审批人确认后续处理。",
"reimbursement.feature_flag_project_linking_v1_disabled": "项目关联暂未开放,请联系管理员确认处理方式。",
"reimbursement.reimbursement_invalid_status_for_project_link": "当前报销单不能修改关联项目,请刷新并确认单据状态。",
"reimbursement.payment_payment_batch_v1_required": "该报销单须通过付款批次安排付款,请前往付款批次查看或创建付款安排。",
"reimbursement.status_invalid_pay_status": "当前报销单尚不满足付款条件,请核实审批和已有付款记录。",
"reimbursement.payment_already_paid_conflict": "该报销单已有不同的付款记录,请核实原付款,勿重复登记。",
"payment_batch.invalid_identifier": "付款记录标识已失效,请刷新后重新选择。",
"payment_batch.invalid_input": "付款操作内容不完整或格式不正确,请核对页面必填项后重试。",
"payment_batch.bank_receipt_required": "银行付款登记前必须上传有效银行回单,请先补充回单。",
"payment_batch.department_unavailable": "当前单据所属部门暂不能使用付款批次,请联系管理员确认处理方式。",
"payment_batch.source_not_found": "部分来源单据已不存在或不可用,请刷新后重新选择。",
"payment_batch.daily_sequence_exhausted": "今日付款批次编号已用完,请联系管理员处理,勿重复提交。",
"payment_batch.payee_account_unavailable": "收款账户资料暂不可用,请联系管理员检查后重试。",
"payment_batch.source_status_invalid": "来源单据尚未满足付款条件或已经付款,请刷新并核实审批与付款状态。",
"payment_batch.already_paid_conflict": "来源单据已有不同的付款记录,请核实已有记录,勿重复付款。",
"payment_batch.source_amount_changed": "来源单据金额已变化,请核对单据后重新安排付款。",
"payment_batch.settled_transaction_required": "尚未找到已结算的交易,请确认结算记录后再登记付款。",
"payment_batch.receipt_security_failed": "付款回单未通过安全检查,请联系管理员核实并重新上传有效回单。",
"payment_batch.receipt_field_invalid": "请通过回单上传入口重新选择文件。",
"payment_batch.receipt_too_large": "回单文件超过大小限制,请压缩后重试。",
"payment_batch.receipt_too_many": "上传回单数量超过限制,请减少文件数量后重试。",
"payment_batch.receipt_upload_limit": "回单上传超过请求限制,请减少文件数量或大小后重试。",
"payment_batch.receipt_file_invalid": "回单文件不符合上传要求,请核对格式、大小和文件内容后重新上传。",
"payment_batch.receipt_required": "请先选择有效的付款回单文件。",
"payment_batch.travel_payment_frozen": "差旅付款组批已暂停,请联系财务确认处理方式。",
"payment_batch.account_reveal_unavailable": "当前批次不支持查看收款账户,请核实批次类型和状态。",
"payment_batch.payee_snapshot_missing": "历史批次缺少收款资料快照,请联系管理员核实,不要据此重复付款。",
"payment_batch.receipt_not_editable": "当前付款状态不允许修改回单,请刷新并确认批次进度。",
"payment_batch.receipt_voided": "该回单已作废,请选择有效回单。",
"payment_batch.download_reason_required": "下载该回单需要填写用途,请补充下载原因。",
"payment_batch.receipt_void_required": "回单不能直接删除,请使用作废操作并说明原因。",
"payment_batch.receipt_already_voided": "该回单已经作废,请刷新列表,勿重复操作。",
"payment_batch.metadata_not_editable": "当前批次状态不允许修改付款信息,请刷新并核实进度。",
"payment_batch.item_not_editable": "只能修改尚未完成的付款明细,请核实该明细状态。",
"payment_batch.batch_not_reviewable": "当前批次状态不允许复核,请刷新并确认是否已处理。",
"payment_batch.items_not_pending": "部分明细已处理,不能复核该批次,请刷新并核实明细状态。",
"payment_batch.maker_required": "该批次缺少制单人信息,请联系管理员核实后再复核。",
"payment_batch.maker_checker_conflict": "制单人不能复核自己的付款批次,请由另一位有权限的经理或负责人复核。",
"payment_batch.risk_resolution_required": "还有未处理的风险提示,请逐项确认并处理后再复核。",
"payment_batch.batch_not_executable": "只有已复核或待重试的批次才能登记付款,请先确认批次状态。",
"payment_batch.review_required": "该批次尚未完成有效复核,请先由独立复核人完成复核。",
"payment_batch.batch_not_cancellable": "该批次当前不能取消,请核实是否已有付款结果并联系财务处理。",
"leave.invalid_uuid": "记录标识已失效,请刷新页面后重新选择。",
"leave.invalid_input": "提交内容不完整或格式不正确,请核对必填项和日期后重试。",
"leave.date_from_invalid": "开始日期不正确,请重新选择。",
"leave.date_to_invalid": "结束日期不正确,请重新选择。",
"leave.date_to_must_be_after_date_from": "结束日期不能早于开始日期,请调整日期范围。",
"leave.department_id_scope_all_only": "当前查询范围不支持指定部门,请调整查询范围。",
"leave.date_invalid": "日期格式不正确,请重新选择日期。",
"leave.file_only_single_file_supported": "每次只能上传一个证明文件,请重新选择。",
"leave.file_file_too_large": "文件超过上传大小限制,请压缩或更换文件后重试。",
"leave.leave_type_id_missing_active_policy": "当前假种尚未配置有效策略,请联系人事维护后重试。",
"leave.file_required": "该假种需要证明材料,请上传后再提交。",
"leave.file_unsupported_mime_type": "文件类型不支持,请选择允许的证明材料格式。",
"leave.status_invalid_edit_status": "当前单据状态不允许编辑,请刷新并确认审批进度。",
"leave.status_invalid_submit_status": "当前单据状态不允许提交,请刷新并确认是否已提交。",
"leave.submit_already_in_review": "该申请已经在审批中,请勿重复提交,可查看审批进度。",
"leave.leave_conflict_range": "该时段已有请假申请,请调整日期或先处理已有申请。",
"leave.leave_insufficient_balance": "可用假期余额不足,请调整天数或联系人事核实。",
"leave.workflow_missing_definition": "尚未配置可用审批流程,请联系管理员维护后重试。",
"leave.workflow_no_actionable_approver": "当前没有可处理该申请的审批人,请联系管理员核实审批配置。",
"leave.status_invalid_withdraw_status": "当前单据状态不允许撤回,请刷新并确认审批进度。",
"leave.withdraw_no_pending_instance": "当前没有可撤回的审批,请刷新并确认审批进度。",
"leave.withdraw_already_processed_by_approver": "审批人已处理该申请,不能再撤回,请联系审批人确认后续处理。",
"leave.session_invalid": "请选择有效的上午或下午时段。",
"leave.leave_invalid_range": "请假结束时间早于开始时间,请调整请假时段。",
"leave.leave_non_working_range": "所选时段没有可请假的工作日,请调整请假日期。",
"leave.leave_missing_reserve_ledger": "请假额度预留记录缺失,请联系人事核实后处理。",
"leave.file_empty": "所选文件为空,请重新选择有效文件。",
"attendance_correction.invalid_input": "提交内容不完整或格式不正确,请核对必填项和日期后重试。",
"attendance_correction.invalid_uuid": "记录标识已失效,请刷新页面后重新选择。",
"attendance_correction.datetime_invalid": "时间格式不正确,请重新选择发生时间。",
"attendance_correction.feature_flag_attendance_unified_v1_disabled": "考勤修正暂未开放,请联系管理员确认处理方式。",
"attendance_correction.workflow_missing_definition": "尚未配置可用审批流程,请联系管理员维护后重试。",
"attendance_correction.workflow_no_actionable_approver": "当前没有可处理该申请的审批人,请联系管理员核实审批配置。",
"attendance_correction.status_invalid_cancel_status": "当前申请已不能取消,请刷新并确认审批结果。",
"payment_batch.legal_entity_required": "报销人尚未维护所属公司,请联系管理员补充后再安排付款。",
"payment_batch.multiple_legal_entities": "所选单据属于不同公司,请按公司分别创建付款安排。",
"payment_batch.payee_bank_required": "收款人尚未维护开户银行,请联系管理员核实真实开户行后补充。",
"payment_batch.payee_account_required": "收款人尚未维护银行卡,请联系管理员补充真实收款资料。",
"payment_batch.payee_bank_unsupported": "当前银行付款仅支持建行收款账户,请停止本次银行付款并联系财务确认处理方式,不要修改真实开户行。",
"payment_batch.payment_account_required": "所属公司尚无可用的建行付款账户,请联系账户管理员维护后重试。",
"payment_batch.payment_account_unavailable": "公司付款账户暂不可用,请联系管理员检查账户配置后重试。",
"payment_batch.active_batch_exists": "所选单据已进入其他付款安排,请刷新列表并查看已有批次。",
"payment_batch.reviewer_required": "当前没有可复核的经理或负责人,制单人不能复核自己的批次,请联系管理员安排独立复核人。",
};
export const UNKNOWN_VALIDATION_MESSAGE = "操作未完成,请核对页面提示;如仍无法继续,请联系管理员协助处理。";
export function resolveErrorReasonMessage(reasonCode: unknown): string | undefined {
return typeof reasonCode === "string" && Object.hasOwn(ERROR_REASON_MESSAGES, reasonCode)
? ERROR_REASON_MESSAGES[reasonCode]
: undefined;
}
+19 -1
查看文件
@@ -89,7 +89,13 @@ export type PaymentBatchDetail = PaymentBatch & {
processingProgress: ProcessingProgress;
};
export type PaymentReadiness = {
ready: boolean;
blockers: Array<{ reasonCode: string; entityId?: string; entityCode?: string }>;
};
export type PaymentBatchCandidate = {
readiness: PaymentReadiness;
entityId: string;
entityType: "reimbursement";
businessNo: string;
@@ -140,15 +146,27 @@ export async function fetchPaymentBatchDetail(batchId: string) {
return apiRequestData<{ item: PaymentBatchDetail }>(`/api/payment-batches/${batchId}`);
}
export async function fetchPaymentBatchCandidates(query: { q?: string; page?: number; pageSize?: number } = {}) {
export async function fetchPaymentBatchCandidates(query: { q?: string; page?: number; pageSize?: number; paymentChannel?: PaymentChannel } = {}) {
const params = new URLSearchParams();
if (query.q?.trim()) params.set("q", query.q.trim());
if (query.page) params.set("page", String(query.page));
if (query.pageSize) params.set("pageSize", String(query.pageSize));
if (query.paymentChannel) params.set("paymentChannel", query.paymentChannel);
const suffix = params.toString() ? `?${params.toString()}` : "";
return apiRequestData<PaymentBatchCandidatesResponse>(`/api/payment-batches/candidates${suffix}`);
}
export async function fetchPaymentBatchReadiness(payload: {
entityType: PaymentBatchEntityType;
entityIds: string[];
defaultPaymentChannel: PaymentChannel;
}) {
return apiRequestData<PaymentReadiness>("/api/payment-batches/readiness", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function createPaymentBatch(payload: {
entityType: PaymentBatchEntityType;
name?: string;
+16 -1
查看文件
@@ -1,4 +1,19 @@
import type { PaymentBatchStatus } from "../api/paymentBatches";
import type { PaymentBatchItem, PaymentBatchStatus } from "../api/paymentBatches";
import type { RiskFlag } from "../api/feeControl";
import { resolveErrorReasonMessage, UNKNOWN_VALIDATION_MESSAGE } from "../api/errorReasons";
export function paymentItemFailureMessage(item: Pick<PaymentBatchItem, "id" | "status" | "failureCode" | "failureMessage" | "openRiskFlags">, batchId: string) {
if (item.status !== "failed" && !item.failureCode && !item.failureMessage) return null;
const flag = item.openRiskFlags.find((risk) => risk.riskCode === "payment_batch_execute_failed"
&& risk.payloadJson?.batchId === batchId && risk.payloadJson?.itemId === item.id);
return resolveErrorReasonMessage(flag?.payloadJson?.reasonCode) ?? UNKNOWN_VALIDATION_MESSAGE;
}
export function paymentRiskFlagsForDisplay(flags: RiskFlag[]) {
return flags.map((flag) => flag.riskCode === "payment_batch_execute_failed" ? {
...flag, detail: resolveErrorReasonMessage(flag.payloadJson?.reasonCode) ?? UNKNOWN_VALIDATION_MESSAGE,
} : flag);
}
const PAYMENT_BATCH_STATUS_LABELS: Record<PaymentBatchStatus, string> = {
draft: "待付款复核",
+9 -1
查看文件
@@ -1,5 +1,6 @@
import { isApiError } from "../api/types";
import { mapApiErrorMessage } from "../api/errorMessages";
import { UNKNOWN_VALIDATION_MESSAGE } from "../api/errorReasons";
function toUserSafeMessage(message: string | undefined, fallback: string) {
const normalized = (message ?? "")
@@ -8,9 +9,11 @@ function toUserSafeMessage(message: string | undefined, fallback: string) {
if (!normalized) {
return fallback;
}
if (normalized === "参数校验失败") return UNKNOWN_VALIDATION_MESSAGE;
const exposesTechnicalDetail =
!/[\u3400-\u9fff]/.test(normalized) ||
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i.test(normalized) ||
/https?:\/\/|\/api\/|(?:^|\s)\/[A-Za-z0-9._-]+(?:\/[^\s]*)?|[A-Za-z]:\\|(?:^|\n)\s*at\s|\b(?:GET|POST|PUT|PATCH|DELETE)\s+\/|^(?:\{|\[)/.test(normalized);
return exposesTechnicalDetail ? fallback : normalized;
@@ -21,8 +24,13 @@ export function resolveErrorMessage(
fallback = "操作失败,请稍后重试",
): string {
if (isApiError(error)) {
const message = mapApiErrorMessage(error.code, error.details, error.message);
if (message === UNKNOWN_VALIDATION_MESSAGE) {
const requestId = /^[A-Za-z0-9_-]{1,80}$/.test(error.requestId) ? error.requestId : "";
return requestId ? `${message} 排障编号:${requestId}` : message;
}
return toUserSafeMessage(
mapApiErrorMessage(error.code, error.details, error.message),
message,
fallback,
);
}
+51 -17
查看文件
@@ -1,4 +1,5 @@
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
import { resolveErrorReasonMessage, UNKNOWN_VALIDATION_MESSAGE } from "../api/errorReasons";
import { useEffect, useMemo, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Link, useSearchParams } from "react-router-dom";
@@ -9,6 +10,7 @@ import {
downloadPaymentOrder,
executePaymentBatch,
fetchPaymentBatchCandidates,
fetchPaymentBatchReadiness,
fetchPaymentBatchDetail,
fetchPaymentBatchReceiptAttachments,
fetchPaymentBatches,
@@ -62,7 +64,7 @@ import { StatusBadge } from "../components/common/StatusBadge";
import { DateTextInput } from "../components/common/DateTextInput";
import { downloadBlob } from "../lib/download";
import { formatDisplayDate, formatDisplayDateTime, formatMoney } from "../lib/displayFormat";
import { paymentBatchStatusLabel } from "../lib/paymentBatchDisplay";
import { paymentBatchStatusLabel, paymentItemFailureMessage, paymentRiskFlagsForDisplay } from "../lib/paymentBatchDisplay";
import { toast } from "sonner";
const PAYMENT_CHANNEL_OPTIONS: PaymentChannel[] = ["bank", "alipay", "wechat", "cash", "other"];
@@ -149,6 +151,7 @@ export function PaymentBatchesPage() {
Boolean(me?.roleCodes.some((roleCode) => ["finance", "finance_mgr"].includes(roleCode)));
const [selectedCandidateIds, setSelectedCandidateIds] = useState<string[]>([]);
const [selectedPaymentChannel, setSelectedPaymentChannel] = useState<PaymentChannel>("bank");
const [creationError, setCreationError] = useState("");
const receiptInputRef = useRef<HTMLInputElement | null>(null);
const [error, setError] = useState("");
const [confirmPaymentAction, setConfirmPaymentAction] = useState<{
@@ -211,15 +214,33 @@ export function PaymentBatchesPage() {
queryFn: () => fetchPaymentBatches(),
});
const candidatesQuery = useQuery({
queryKey: ["payment-batch-candidates", candidateSearch, candidatePage, candidatePageSize],
queryKey: ["payment-batch-candidates", candidateSearch, candidatePage, candidatePageSize, selectedPaymentChannel],
enabled: canManageBatch,
queryFn: () => fetchPaymentBatchCandidates({
q: candidateSearch,
page: candidatePage,
pageSize: candidatePageSize,
paymentChannel: selectedPaymentChannel,
}),
});
const creationReadinessQuery = useQuery({
queryKey: ["payment-batch-readiness", [...selectedCandidateIds].sort(), selectedPaymentChannel],
enabled: canManageBatch && selectedCandidateIds.length > 0 && activeView === "pending",
queryFn: () => fetchPaymentBatchReadiness({
entityType: "reimbursement", entityIds: selectedCandidateIds, defaultPaymentChannel: selectedPaymentChannel,
}),
staleTime: 0,
});
const creationBlocker = creationReadinessQuery.isFetching || creationReadinessQuery.isPending
? "正在核对付款条件,请稍候。"
: creationReadinessQuery.isError
? resolveUserErrorMessage(creationReadinessQuery.error)
: creationReadinessQuery.data?.ready !== true
? [...new Set((creationReadinessQuery.data?.blockers ?? []).map((blocker) => resolveErrorReasonMessage(blocker.reasonCode) ?? UNKNOWN_VALIDATION_MESSAGE))].join(" ") || UNKNOWN_VALIDATION_MESSAGE
: "";
useEffect(() => { setCreationError(""); }, [selectedCandidateIds, selectedPaymentChannel]);
const batches = useMemo(() => listQuery.data?.items ?? [], [listQuery.data?.items]);
const candidateTotal = candidatesQuery.data?.total ?? 0;
const candidateTotalPages = Math.max(1, Math.ceil(candidateTotal / candidatePageSize));
@@ -344,12 +365,15 @@ export function PaymentBatchesPage() {
};
const createMutation = useMutation({
mutationFn: async () =>
createPaymentBatch({
mutationFn: async () => {
if (!selectedCandidateIds.length || creationBlocker) throw new Error(creationBlocker || "请先选择可付款的单据。");
return createPaymentBatch({
entityType: "reimbursement",
entityIds: selectedCandidateIds,
defaultPaymentChannel: selectedPaymentChannel,
}),
});
},
onMutate: () => { setCreationError(""); },
onSuccess: async ({ item }) => {
setError("");
setSelectedCandidateIds([]);
@@ -365,8 +389,10 @@ export function PaymentBatchesPage() {
},
onError: (err) => {
const message = resolveError(err);
setError(message);
toast.error(message);
setCreationError(message);
void candidatesQuery.refetch();
void creationReadinessQuery.refetch();
toast.error("未创建付款安排,请查看动作旁的提示。");
},
});
@@ -392,10 +418,11 @@ export function PaymentBatchesPage() {
const executeMutation = useMutation({
mutationFn: executePaymentBatch,
onSuccess: async () => {
onSuccess: async ({ item }) => {
setError("");
await refresh();
toast.success("付款已登记完成");
if (item.status === "failed") toast.error("部分付款登记未完成,请查看明细中的原因后重试。");
else toast.success("付款已登记完成");
},
onError: (err) => {
const message = resolveError(err);
@@ -697,6 +724,9 @@ export function PaymentBatchesPage() {
) : null}
{(candidatesQuery.data?.items ?? []).map((candidate) => {
const selected = selectedCandidateIds.includes(candidate.entityId);
const candidateReady = candidate.readiness?.ready === true;
const blockerText = [...new Set((candidate.readiness?.blockers ?? []).map((blocker) => resolveErrorReasonMessage(blocker.reasonCode) ?? UNKNOWN_VALIDATION_MESSAGE))].join(" ") || UNKNOWN_VALIDATION_MESSAGE;
const reasonId = `payment-candidate-reason-${candidate.entityId}`;
return (
<label
key={candidate.entityId}
@@ -707,6 +737,8 @@ export function PaymentBatchesPage() {
type="checkbox"
className="mt-1 h-5 w-5"
checked={selected}
disabled={(!candidateReady && !selected) || candidatesQuery.isFetching || createMutation.isPending}
aria-describedby={!candidateReady ? reasonId : undefined}
onChange={(event) => setSelectedCandidateIds((current) =>
event.target.checked
? Array.from(new Set([...current, candidate.entityId]))
@@ -722,6 +754,7 @@ export function PaymentBatchesPage() {
{formatDisplayDate(candidate.businessDate)}
{candidate.summary ? ` · ${candidate.summary}` : ""}
</span>
{!candidateReady ? <span id={reasonId} className="mt-2 block text-sm text-red-700">{blockerText}</span> : null}
</span>
</label>
);
@@ -739,8 +772,6 @@ export function PaymentBatchesPage() {
</div>
</div>
) : null}
{selectedCandidateIds.length ? (
<>
<div className="max-w-sm space-y-2">
<Label htmlFor="payment-batch-default-channel">付款渠道</Label>
<select
@@ -748,6 +779,7 @@ export function PaymentBatchesPage() {
data-testid="payment-batch-default-channel"
className="select-std w-full"
value={selectedPaymentChannel}
disabled={createMutation.isPending}
onChange={(event) => setSelectedPaymentChannel(event.target.value as PaymentChannel)}
>
{PAYMENT_CHANNEL_OPTIONS.map((option) => (
@@ -758,13 +790,15 @@ export function PaymentBatchesPage() {
<div className="text-xs text-zinc-500">
已选择 {selectedCandidateIds.length} 项;系统自动生成编号和标题,按{paymentChannelLabel(selectedPaymentChannel)}提交负责人复核。
</div>
</>
) : null}
</div>
{selectedCandidateIds.length ? (
<Button type="button" disabled={createMutation.isPending} onClick={() => createMutation.mutate()}>
<div className="min-w-0 space-y-2">
{creationError || creationBlocker ? <div id="payment-creation-reason" role="alert" aria-live="polite" className="break-words text-sm text-red-700">{creationError || creationBlocker}</div> : null}
{creationReadinessQuery.isError ? <Button type="button" variant="outline" onClick={() => void creationReadinessQuery.refetch()}>重新检查付款条件</Button> : null}
<Button type="button" disabled={createMutation.isPending || Boolean(creationBlocker)} aria-describedby={creationError || creationBlocker ? "payment-creation-reason" : undefined} onClick={() => createMutation.mutate()}>
{createMutation.isPending ? "提交中..." : "创建并提交复核"}
</Button>
</div>
) : null}
</div>
) : null}
@@ -904,21 +938,21 @@ export function PaymentBatchesPage() {
onBlur={(event) => updateItemMutation.mutate({ batchId: selectedBatch.id, itemId: item.id, paymentDateOverride: event.target.value || null })}
/>
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
{item.failureMessage ? `失败原因:${item.failureMessage}` : "付款异常:无"}
{paymentItemFailureMessage(item, selectedBatch.id) ? `失败原因:${paymentItemFailureMessage(item, selectedBatch.id)}` : "付款异常:无"}
</div>
</div>
) : ["executed", "cancelled"].includes(selectedBatch.status) ? (
<div className="mt-3 detail-fact-grid text-sm md:grid-cols-3" data-testid="payment-batch-item-payment-readonly">
<div className="detail-fact-card"><div className="detail-fact-card__label">付款凭证号</div><div className="detail-fact-card__value">{item.paymentVoucherNoOverride || "-"}</div></div>
<div className="detail-fact-card"><div className="detail-fact-card__label">实际付款日期</div><div className="detail-fact-card__value">{formatDisplayDate(item.paymentDateOverride ?? selectedBatch.defaultPaymentDate)}</div></div>
<div className="detail-fact-card"><div className="detail-fact-card__label">付款结果</div><div className="detail-fact-card__value">{item.failureMessage || paymentItemStatusLabel(item.status)}</div></div>
<div className="detail-fact-card"><div className="detail-fact-card__label">付款结果</div><div className="detail-fact-card__value">{paymentItemFailureMessage(item, selectedBatch.id) || paymentItemStatusLabel(item.status)}</div></div>
</div>
) : (
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
付款复核通过后,由财务登记付款凭证号和实际付款日期。
</div>
)}
<RiskFlagsCard items={item.openRiskFlags} title="关联开放风险" />
<RiskFlagsCard items={paymentRiskFlagsForDisplay(item.openRiskFlags)} title="关联开放风险" />
</div>
))}
</div>
@@ -35,6 +35,8 @@ type SyntheticContract = {
};
type MockScenario = {
readiness?: { ready: boolean; blockers: Array<{ reasonCode: string }> };
createFailure?: string;
paymentCandidates?: Array<{
entityId: string;
entityType: "reimbursement";
@@ -44,6 +46,7 @@ type MockScenario = {
currencyCode: "CNY";
businessDate: string | null;
summary: string | null;
readiness?: { ready: boolean; blockers: Array<{ reasonCode: string }> };
}>;
paymentCandidateTotal?: number;
onCandidateQuery?: (query: URLSearchParams) => void;
@@ -267,7 +270,7 @@ async function mockSyntheticApis(page: Page, actor: SyntheticActor, scenario: Mo
return;
}
if (pathName === "/api/payment-batches/candidates") {
const items = scenario.paymentCandidates ?? [];
const items = (scenario.paymentCandidates ?? []).map((item) => ({ ...item, readiness: item.readiness ?? { ready: true, blockers: [] } }));
scenario.onCandidateQuery?.(requestUrl.searchParams);
await fulfillJson(route, {
items,
@@ -277,8 +280,16 @@ async function mockSyntheticApis(page: Page, actor: SyntheticActor, scenario: Mo
});
return;
}
if (pathName === "/api/payment-batches/readiness") {
await fulfillJson(route, scenario.readiness ?? { ready: true, blockers: [] });
return;
}
if (pathName === "/api/payment-batches" && request.method() === "POST") {
scenario.onCreatePaymentBatch?.(request.postDataJSON());
if (scenario.createFailure) {
await route.fulfill({ status: 409, contentType: "application/json", body: JSON.stringify({ requestId: "synthetic-readiness-race", error: { code: "VALID_001", message: "internal payload", details: { reasonCode: scenario.createFailure, private: "/private/hidden" } } }) });
return;
}
await fulfillJson(route, { item: { id: CREATED_PAYMENT_BATCH_ID } });
return;
}
@@ -404,6 +415,140 @@ test("无票据上传权限的兼容角色直达票据页时失败关闭", async
expectNoPageErrors(pageErrors);
});
for (const [reasonCode, message] of [
["payment_batch.legal_entity_required", "报销人尚未维护所属公司,请联系管理员补充后再安排付款。"],
["payment_batch.payee_bank_required", "收款人尚未维护开户银行,请联系管理员核实真实开户行后补充。"],
["payment_batch.payee_account_required", "收款人尚未维护银行卡,请联系管理员补充真实收款资料。"],
["payment_batch.payee_bank_unsupported", "当前银行付款仅支持建行收款账户,请停止本次银行付款并联系财务确认处理方式,不要修改真实开户行。"],
["payment_batch.active_batch_exists", "所选单据已进入其他付款安排,请刷新列表并查看已有批次。"],
["payment_batch.reviewer_required", "当前没有可复核的经理或负责人,制单人不能复核自己的批次,请联系管理员安排独立复核人。"],
]) {
test(`付款候选就近解释并禁用 ${reasonCode}`, async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成数据");
await mockSyntheticApis(page, FINANCE_ACTOR, { paymentCandidates: [{ ...PAYMENT_CANDIDATE, readiness: { ready: false, blockers: [{ reasonCode }] } }] });
await login(page, FINANCE_ACTOR);
await page.goto("/payment-batches");
const row = page.getByTestId("payment-batch-candidate-row");
await expect(row.getByText(message, { exact: true })).toBeVisible();
await expect(row.getByRole("checkbox")).toBeDisabled();
await expect(row.getByRole("checkbox")).toHaveAccessibleDescription(message);
await expect(row.getByRole("link")).toHaveCount(0);
});
}
test("跨公司组合原因紧邻禁用主动作并可被读屏读取", async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成数据");
let creates = 0;
await mockSyntheticApis(page, FINANCE_ACTOR, { paymentCandidates: [PAYMENT_CANDIDATE], readiness: { ready: false, blockers: [{ reasonCode: "payment_batch.multiple_legal_entities" }] }, onCreatePaymentBatch: () => { creates += 1; } });
await login(page, FINANCE_ACTOR);
await page.goto("/payment-batches");
const checkbox = page.getByTestId("payment-batch-candidate-row").getByRole("checkbox");
await checkbox.focus();
await page.keyboard.press("Space");
await expect(checkbox).toBeChecked();
const button = page.getByRole("button", { name: "创建并提交复核", exact: true });
await expect(button).toBeDisabled();
await expect(button).toHaveAccessibleDescription("所选单据属于不同公司,请按公司分别创建付款安排。");
await expect(page.locator("#payment-creation-reason")).toHaveAttribute("role", "alert");
expect(creates).toBe(0);
});
test("可先换渠道再选择,已选项失效后仍可取消", async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成数据");
const scenario: MockScenario = { paymentCandidates: [{ ...PAYMENT_CANDIDATE, readiness: { ready: false, blockers: [{ reasonCode: "payment_batch.payee_bank_required" }] } }] };
await mockSyntheticApis(page, FINANCE_ACTOR, scenario);
await login(page, FINANCE_ACTOR);
await page.goto("/payment-batches");
const checkbox = page.getByTestId("payment-batch-candidate-row").getByRole("checkbox");
await expect(checkbox).toBeDisabled();
const channel = page.getByTestId("payment-batch-default-channel");
await expect(channel).toBeVisible();
scenario.paymentCandidates = [{ ...PAYMENT_CANDIDATE, readiness: { ready: true, blockers: [] } }];
await channel.selectOption("cash");
await expect(checkbox).toBeEnabled();
await checkbox.check();
scenario.paymentCandidates = [{ ...PAYMENT_CANDIDATE, readiness: { ready: false, blockers: [{ reasonCode: "payment_batch.payee_bank_required" }] } }];
scenario.readiness = { ready: false, blockers: [{ reasonCode: "payment_batch.payee_bank_required" }] };
await channel.selectOption("bank");
await expect(page.getByTestId("payment-batch-candidate-row")).toContainText("收款人尚未维护开户银行,请联系管理员核实真实开户行后补充。");
await expect(checkbox).toBeChecked();
await expect(checkbox).toBeEnabled();
await checkbox.uncheck();
await expect(checkbox).not.toBeChecked();
await expect(page.getByRole("button", { name: "创建并提交复核", exact: true })).toHaveCount(0);
});
for (const [reasonCode, message] of [
["payment_batch.active_batch_exists", "所选单据已进入其他付款安排,请刷新列表并查看已有批次。"],
["unknown.synthetic", "操作未完成,请核对页面提示;如仍无法继续,请联系管理员协助处理。 排障编号:synthetic-readiness-race"],
]) {
test(`创建并发失败留在动作旁 ${reasonCode}`, async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成数据");
await mockSyntheticApis(page, FINANCE_ACTOR, { paymentCandidates: [PAYMENT_CANDIDATE], createFailure: reasonCode });
await login(page, FINANCE_ACTOR);
await page.goto("/payment-batches");
await page.getByTestId("payment-batch-candidate-row").getByRole("checkbox").check();
if (reasonCode === "unknown.synthetic") await page.setViewportSize({ width: 375, height: 812 });
await page.getByRole("button", { name: "创建并提交复核", exact: true }).click();
await expect(page.locator("#payment-creation-reason")).toHaveText(message);
await expect(page.getByRole("button", { name: "创建并提交复核", exact: true })).toHaveAccessibleDescription(message);
await expect(page.getByText("internal payload", { exact: false })).toHaveCount(0);
await expect(page.getByText("/private/hidden", { exact: false })).toHaveCount(0);
});
}
test("完整银行候选没有解释噪音并可提交复核", async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成数据");
let created: unknown = null;
await mockSyntheticApis(page, FINANCE_ACTOR, { paymentCandidates: [PAYMENT_CANDIDATE], onCreatePaymentBatch: (payload) => { created = payload; } });
await login(page, FINANCE_ACTOR);
await page.goto("/payment-batches");
const row = page.getByTestId("payment-batch-candidate-row");
await expect(row.locator('[id^="payment-candidate-reason-"]')).toHaveCount(0);
await row.getByRole("checkbox").check();
const button = page.getByRole("button", { name: "创建并提交复核", exact: true });
await expect(button).toBeEnabled();
await expect(page.locator("#payment-creation-reason")).toHaveCount(0);
await button.click();
await expect(page).toHaveURL(new RegExp(`id=${CREATED_PAYMENT_BATCH_ID}`));
expect(created).toEqual({ entityType: "reimbursement", entityIds: [PAYMENT_CANDIDATE_ENTITY_ID], defaultPaymentChannel: "bank" });
});
test("付款失败摘要与历史风险不向 PC 或移动视口暴露技术文案", async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成数据");
await mockSyntheticApis(page, FINANCE_ACTOR);
const payload: Record<string, unknown> = { batchId: CREATED_PAYMENT_BATCH_ID, itemId: "failed-item", reasonCode: "payment_batch.source_status_invalid" };
const detail = {
id: CREATED_PAYMENT_BATCH_ID, batchNo: "PB-SYN-FAIL", name: "合成失败批次", status: "failed",
entityType: "reimbursement", totalAmount: "1280.50", highRisk: false,
defaultPaymentDate: null, defaultPaymentChannel: "cash", createdBy: FINANCE_ACTOR.id,
paymentAccount: null, paymentOrderExportCount: 0, canExportPaymentOrder: false,
paymentOrderExportBlockers: [], processingProgress: { stages: [] },
reviewRequirement: { completedReviewCount: 1, requiredReviewCount: 1, nextStep: "ready_for_execute" },
items: [{
id: "failed-item", entityId: PAYMENT_CANDIDATE_ENTITY_ID, entityCode: PAYMENT_CANDIDATE.businessNo,
entityTitle: "合成失败明细", amount: "1280.50", currencyCode: "CNY", status: "failed",
paymentChannelOverride: null, paymentDateOverride: null, paymentVoucherNoOverride: null,
failureCode: "VALID_001", failureMessage: "参数校验失败 /private/hidden", expenseItems: [], expenseItemsTotal: "0.00",
openRiskFlags: [{ id: "failure-risk", riskCode: "payment_batch_execute_failed", title: "付款登记失败", level: "high", detail: "参数校验失败 /private/hidden", source: "payment_batch", status: "open", payloadJson: payload }],
}],
};
await page.route(`**/api/payment-batches/${CREATED_PAYMENT_BATCH_ID}`, (route) => fulfillJson(route, { item: detail }));
await page.route(`**/api/payment-batches/${CREATED_PAYMENT_BATCH_ID}/receipt-attachments`, (route) => fulfillJson(route, { items: [] }));
await login(page, FINANCE_ACTOR);
await page.goto(`/payment-batches?id=${CREATED_PAYMENT_BATCH_ID}`);
await expect(page.getByTestId("payment-batch-items-panel")).toContainText("失败原因:来源单据尚未满足付款条件或已经付款,请刷新并核实审批与付款状态。");
await expect(page.getByText("参数校验失败", { exact: false })).toHaveCount(0);
await expect(page.getByText("/private/hidden", { exact: false })).toHaveCount(0);
delete payload.reasonCode;
await page.reload();
await expect(page.getByTestId("payment-batch-items-panel")).toContainText("失败原因:操作未完成,请核对页面提示;如仍无法继续,请联系管理员协助处理。");
await page.setViewportSize({ width: 375, height: 812 });
await expect(page.getByTestId("payment-batch-items-panel")).toBeVisible();
await expect(page.getByText("参数校验失败", { exact: false })).toHaveCount(0);
await expect(page.getByText("/private/hidden", { exact: false })).toHaveCount(0);
});
test("财务从业务候选选择报销并使用所选实体创建付款批次", async ({ page }) => {
test.skip(process.env.E2E_MODE === "live", "只使用本地合成角色,不访问公网");
const pageErrors = capturePageErrors(page);
@@ -81,7 +81,7 @@ async function fulfillApiError(route: Route, status: number, details: Record<str
error: {
code: "VALID_001",
message: "参数校验失败",
details,
details: { ...details, reasonCode: details.verification === "too_frequent" ? "invoice_verification.too_frequent" : "invoice_verification.attempts_exceeded" },
},
}),
});
@@ -1227,8 +1227,9 @@ test("PC 票据辅助核验错误提示可理解", async ({ page }) => {
await expect(page.getByTestId("invoice-detail-panel")).toBeVisible();
await page.getByTestId("invoice-trigger-verification").click();
await expect(page.getByRole("main").getByText("VALID_001: 验真触发过于频繁,请稍后重试。")).toBeVisible();
await expect(page.getByRole("main").getByText("验真触发过于频繁,请稍后重试。", { exact: true })).toBeVisible();
await page.getByTestId("invoice-retry-verification").click();
await expect(page.getByRole("main").getByText("VALID_001: 验真重试次数已达上限,请联系财务人工核验。")).toBeVisible();
await expect(page.getByRole("main").getByText("验真重试次数已达上限,请联系财务人工核验。", { exact: true })).toBeVisible();
await expect(page.getByRole("main").getByText("VALID_001:", { exact: false })).toHaveCount(0);
});
@@ -542,7 +542,7 @@ async function mockApis(page: Page, options: { roleCodes?: string[] } = {}) {
error: {
code: "VALID_001",
message: "参数校验失败",
details: { file: "unsupported_type" },
details: { file: "unsupported_type", reasonCode: "purchase_attachment.unsupported_type" },
},
}),
});
@@ -446,7 +446,7 @@ async function mockApis(page: Page, state: ReturnType<typeof buildReimbursementS
error: {
code: "VALID_001",
message: "参数校验失败",
details: state.submitGuardDetails,
details: { ...state.submitGuardDetails, reasonCode: "reimbursement.items_invoice_item_guard_failed" },
},
}),
});
@@ -479,7 +479,7 @@ async function mockApis(page: Page, state: ReturnType<typeof buildReimbursementS
error: {
code: "VALID_001",
message: "参数校验失败",
details: state.invoiceItemLinkError,
details: { ...state.invoiceItemLinkError, reasonCode: "reimbursement.invoice_invalid_unlinked_item_state" },
},
}),
});
+255
查看文件
@@ -0,0 +1,255 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import crypto from "node:crypto";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
const require = createRequire(import.meta.url);
const ts = require("../../client/node_modules/typescript");
const root = fileURLToPath(new URL("../../", import.meta.url));
const primaryFiles = new Set([
"server/src/routes/paymentBatches.js", "server/src/routes/reimbursements.js",
"server/src/routes/leave.js", "server/src/services/leave.js",
"server/src/routes/attendanceCorrections.js",
]);
const primaryInvoiceOperations = new Set(["/invoice-pool/:id/bind", "/invoice-pool/:id/unbind", "function:resolveInvoiceLinkedItem"]);
export function operationFor(node) {
for (let parent = node.parent; parent; parent = parent.parent) {
if (ts.isCallExpression(parent) && ts.isPropertyAccessExpression(parent.expression) && ["get", "post", "patch", "delete"].includes(parent.expression.name.text) && literal(parent.arguments[0])?.startsWith("/")) return literal(parent.arguments[0]);
if (ts.isFunctionDeclaration(parent)) return `function:${parent.name?.text ?? "anonymous"}`;
}
return null;
}
function isPrimary(entry) {
return primaryFiles.has(entry.file) || (entry.file === "server/src/routes/invoicePool.js" && primaryInvoiceOperations.has(entry.operation));
}
function legacyKey(entry) {
return `${entry.file}:${entry.operation ?? "module"}:${entry.fingerprint}`;
}
function parse(file, source) {
const tree = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
if (tree.parseDiagnostics.length) throw new Error("source_parse_failed");
return tree;
}
function visit(node, callback) {
callback(node);
ts.forEachChild(node, (child) => visit(child, callback));
}
function shape(node) {
if (ts.isParenthesizedExpression(node)) return shape(node.expression);
const children = [];
ts.forEachChild(node, (child) => { children.push(shape(child)); });
return [node.kind, typeof node.text === "string" ? node.text : null, children];
}
export function fingerprint(node) {
return crypto.createHash("sha256").update(JSON.stringify(shape(node))).digest("hex");
}
function literal(node) {
return node && ts.isStringLiteralLike(node) ? node.text : null;
}
function properties(node) {
if (!node || !ts.isObjectLiteralExpression(node)) return {};
return Object.fromEntries(node.properties.filter(ts.isPropertyAssignment).map((property) => [property.name.text, literal(property.initializer)]));
}
function isSafeMessage(value) {
return typeof value === "string" && /[\u3400-\u9fff]/.test(value)
&& !/参数校验失败|https?:\/\/|\/api\/|(?:^|\s)\/[A-Za-z0-9._-]+|[A-Za-z]:\\|^\s*[\[{]|\d{12,}|\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i.test(value);
}
function declarations(source, name) {
let value;
visit(parse("contract.ts", source), (node) => {
if (ts.isVariableDeclaration(node) && node.name.text === name) {
if (value) throw new Error("duplicate_contract_declaration");
value = node.initializer;
}
});
if (!value) throw new Error("missing_contract_declaration");
return value;
}
export function inventory(sources) {
const legacy = [];
const used = [];
for (const [file, source] of Object.entries(sources)) {
const tree = parse(file, source);
const aliases = new Map();
visit(tree, (node) => {
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isIdentifier(node.initializer)) aliases.set(node.name.text, node.initializer.text);
if (ts.isBindingElement(node) && node.propertyName && ts.isIdentifier(node.name)) aliases.set(node.name.text, node.propertyName.text);
if (ts.isImportSpecifier(node) && node.propertyName) aliases.set(node.name.text, node.propertyName.text);
});
visit(tree, (node) => {
if (!ts.isCallExpression(node) && !ts.isNewExpression(node)) return;
let name = ts.isIdentifier(node.expression) ? node.expression.text : node.expression.name?.text;
const visitedNames = new Set();
while (aliases.has(name) && !visitedNames.has(name)) { visitedNames.add(name); name = aliases.get(name); }
if (name === "buildBusinessValidationError" || name === "addPaymentReadinessBlocker") {
const argument = node.arguments?.[0];
const registryValidated = argument && ts.isPropertyAccessExpression(argument) && argument.name.text === "code"
&& ts.isCallExpression(argument.expression) && argument.expression.expression.getText() === "getErrorReason";
if (!registryValidated) used.push({ file, code: literal(argument) });
}
const errorProperties = properties(node.arguments?.[0]);
let containsValidationCode = false;
const codeExpression = node.arguments?.[0]?.properties?.find((property) => property.name?.text === "code")?.initializer;
if (codeExpression) visit(codeExpression, (child) => { if (literal(child) === "VALID_001") containsValidationCode = true; });
const appError = name === "AppError" && (containsValidationCode || errorProperties.message === "参数校验失败");
if (!/^build(?!Business).*ValidationError$/.test(name ?? "") && !appError) return;
const errorDetails = node.arguments?.[0]?.properties?.find((property) => property.name?.text === "details")?.initializer;
const reasonProperty = errorDetails?.properties?.find((property) => property.name?.text === "reasonCode");
if (file === "server/src/utils/businessValidationError.js" && appError && operationFor(node) === "function:buildBusinessValidationError"
&& reasonProperty?.initializer?.getText(tree) === "reason.code") return;
legacy.push({ file, fingerprint: fingerprint(node), operation: operationFor(node) });
});
}
return { legacy, used };
}
function sourceFiles(directory) {
return fs.readdirSync(path.join(root, directory), { withFileTypes: true }).flatMap((entry) => {
const file = `${directory}/${entry.name}`;
return entry.isDirectory() ? sourceFiles(file) : /\.[cm]?[jt]sx?$/.test(file) ? [file] : [];
});
}
export function repositoryInput() {
return {
registry: fs.readFileSync(path.join(root, "server/src/constants/errorReasons.js"), "utf8"),
mapping: fs.readFileSync(path.join(root, "client/src/api/errorReasons.ts"), "utf8"),
expectedMessages: JSON.parse(fs.readFileSync(path.join(root, "server/tests/unit/fixtures/error-reason-messages.json"), "utf8")),
sources: Object.fromEntries(sourceFiles("server/src").map((file) => [file, fs.readFileSync(path.join(root, file), "utf8")])),
baseline: JSON.parse(fs.readFileSync(path.join(root, "scripts/qa/error-reason-legacy-baseline.json"), "utf8")),
};
}
export function check(input) {
const findings = [];
const add = (kind, extra = {}) => findings.push({ kind, ...extra });
const registered = new Set();
const rows = [...declarations(input.registry, "ERROR_REASONS").elements].map(properties);
if (!rows.length) add("empty_registry");
for (const row of rows) {
if (registered.has(row.code)) add("duplicate_reason", { code: row.code });
registered.add(row.code);
if (input.expectedMessages?.[row.code] !== row.defaultMessage) add("missing_content_assertion", { code: row.code });
if (!/^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/.test(row.code ?? "") || row.domain !== row.code?.split(".")[0]
|| !["user", "internal"].includes(row.audience) || !row.recovery || !isSafeMessage(row.defaultMessage)) add("invalid_registry_metadata");
}
const mapped = new Set();
for (const entry of declarations(input.mapping, "ERROR_REASON_MESSAGES").properties) {
const code = entry.name?.text;
if (mapped.has(code)) add("duplicate_mapping", { code });
mapped.add(code);
if (!registered.has(code)) add("orphan_mapping", { code });
if (!isSafeMessage(literal(entry.initializer))) add("unsafe_mapping", { code });
}
for (const row of rows) if (row.audience === "user" && !mapped.has(row.code)) add("missing_mapping", { code: row.code });
for (const code of Object.keys(input.expectedMessages ?? {})) if (!registered.has(code)) add("orphan_content_assertion", { code });
const { legacy, used } = inventory(input.sources);
for (const use of used) if (!registered.has(use.code)) add("unregistered_reason", use);
if (input.baseline.version !== 1) add("invalid_baseline_version");
const remaining = new Map();
const today = new Date().toISOString().slice(0, 10);
for (const entry of input.baseline.entries ?? []) {
const key = legacyKey(entry);
if (isPrimary(entry)) add("primary_exception_forbidden", { file: entry.file });
const expiresAt = Date.parse(`${entry.expiresOn}T00:00:00Z`);
if (!entry.owner || !entry.reason || !/^\d{4}-\d{2}-\d{2}$/.test(entry.expiresOn ?? "") || !Number.isFinite(expiresAt)
|| new Date(expiresAt).toISOString().slice(0, 10) !== entry.expiresOn || !Number.isSafeInteger(entry.count) || entry.count < 1) add("invalid_exception", { file: entry.file });
if (entry.expiresOn <= today) add("expired_exception", { file: entry.file });
if (remaining.has(key)) add("duplicate_exception", { file: entry.file });
remaining.set(key, entry.count);
}
for (const entry of legacy) {
if (isPrimary(entry)) add("primary_legacy_forbidden", { file: entry.file });
const key = legacyKey(entry);
const count = remaining.get(key) ?? 0;
if (count < 1) add("new_legacy", { file: entry.file });
else remaining.set(key, count - 1);
}
for (const [key, count] of remaining) if (count > 0) add("stale_exception", { file: key.split(":")[0] });
const domains = {};
const domain = (name) => domains[name] ??= { registered: 0, migrated: 0, legacy: 0, expired: 0 };
for (const row of rows) domain(row.domain).registered += 1;
for (const use of used) domain(use.code?.split(".")[0] ?? "unresolved").migrated += 1;
for (const entry of legacy) domain(path.basename(entry.file, path.extname(entry.file))).legacy += 1;
for (const entry of input.baseline.entries ?? []) if (entry.expiresOn <= today) domain(path.basename(entry.file, path.extname(entry.file))).expired += entry.count;
return { pass: findings.length === 0, registered: rows.length, migrated: used.length, legacy: legacy.length, primaryExceptions: (input.baseline.entries ?? []).filter(isPrimary).length, domains, findings };
}
function selfTest() {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "error-reason-contract-"));
const registry = 'const ERROR_REASONS = [{code:"first.required",domain:"first",audience:"user",defaultMessage:"请补充资料。",recovery:"edit"},{code:"second.required",domain:"second",audience:"user",defaultMessage:"请确认审核人。",recovery:"edit"}];';
const mapping = 'const ERROR_REASON_MESSAGES = {"first.required":"请补充资料。","second.required":"请确认审核人。"};';
const legacySource = 'throw buildValidationError({status:"old"}, 409);';
const sourceFile = "server/src/routes/legacy.js";
const sample = inventory({ [sourceFile]: legacySource }).legacy[0];
const base = { registry, mapping, sources: { [sourceFile]: legacySource, "server/src/routes/example.js": 'buildBusinessValidationError("first.required", {status:"same"}); buildBusinessValidationError("second.required", {status:"same"});' }, baseline: { version: 1, entries: [{ ...sample, count: 1, owner: "task-owner", reason: "deferred domain", expiresOn: "2099-01-01" }] } };
base.expectedMessages = { "first.required": "请补充资料。", "second.required": "请确认审核人。" };
const cases = [
["valid-cross-domain", () => {}, true],
["unregistered", (input) => { input.sources[sourceFile] += 'buildBusinessValidationError("missing.required");'; }, false],
["missing-mapping", (input) => { input.mapping = mapping.replace('"second.required":"请确认审核人。"', ''); }, false],
["typo", (input) => { input.mapping = mapping.replace("second.required", "second.requird"); }, false],
["new-legacy", (input) => { input.sources[sourceFile] += legacySource; }, false],
["structural-change", (input) => { input.sources[sourceFile] = legacySource.replace('"old"', '"new"'); }, false],
["expired", (input) => { input.baseline.entries[0].expiresOn = "2000-01-01"; }, false],
["one-added-one-removed", (input) => { input.sources[sourceFile] = 'buildValidationError({status:"replacement"}, 409);'; }, false],
["formatting-stable", (input) => { input.sources[sourceFile] = "throw buildValidationError(\n { status: 'old', },\n 409,\n);"; }, true],
["primary-exception", (input) => { input.sources["server/src/routes/paymentBatches.js"] = legacySource; input.baseline.entries.push({ ...input.baseline.entries[0], file: "server/src/routes/paymentBatches.js" }); }, false],
["duplicate-registry", (input) => { input.registry = registry.replace('code:"second.required",domain:"second"', 'code:"first.required",domain:"first"'); }, false],
["dynamic-reason", (input) => { input.sources[sourceFile] += 'buildBusinessValidationError(variable);'; }, false],
["missing-content-assertion", (input) => { delete input.expectedMessages["first.required"]; }, false],
["aliased-legacy", (input) => { input.sources[sourceFile] += 'const reject = buildValidationError; reject({status:"new"});'; }, false],
["aliased-unregistered", (input) => { input.sources[sourceFile] += 'const reject = buildBusinessValidationError; reject("first.typo");'; }, false],
["invalid-expiry", (input) => { input.baseline.entries[0].expiresOn = "2099-02-31"; }, false],
["moved-operation", (input) => { input.sources[sourceFile] = `function moved() { ${legacySource} }`; }, false],
["conditional-validation-code", (input) => { input.sources[sourceFile] += 'new AppError({code: condition ? "VALID_001" : "OTHER", message:"参数校验失败"});'; }, false],
["unsafe-client-message", (input) => { input.mapping = mapping.replace("请补充资料。", "请补充资料 /api/private"); }, false],
["unsafe-registry-message", (input) => { input.registry = registry.replace("请补充资料。", "请补充资料 /api/private"); input.expectedMessages["first.required"] = "请补充资料 /api/private"; }, false],
];
try {
for (const [name, mutate, expectedPass] of cases) {
const input = structuredClone(base);
mutate(input);
const file = path.join(directory, "fixture.json");
fs.writeFileSync(file, JSON.stringify(input));
const result = spawnSync(process.execPath, [fileURLToPath(import.meta.url), "--fixture", file], { encoding: "utf8" });
assert.equal(result.status, expectedPass ? 0 : 1, name);
assert.equal(JSON.parse(result.stdout).pass, expectedPass, name);
console.log(`${name}: PASS (exit=${result.status})`);
}
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
try {
if (process.argv.includes("--self-test")) selfTest();
else {
const fixtureIndex = process.argv.indexOf("--fixture");
const input = fixtureIndex >= 0 ? JSON.parse(fs.readFileSync(process.argv[fixtureIndex + 1], "utf8")) : repositoryInput();
const report = check(input);
console.log(JSON.stringify(report, null, 2));
process.exitCode = report.pass ? 0 : 1;
}
} catch {
console.log(JSON.stringify({ pass: false, findings: [{ kind: "contract_scan_failed" }] }));
process.exitCode = 1;
}
}
@@ -0,0 +1,678 @@
{
"version": 1,
"entries": [
{"file":"server/src/plugins/auth.js","fingerprint":"736318b046fb0f03c3c1e1db2630c7d84a63d0bb47a9ba15d56444a75cbc3bba","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:authPlugin"},
{"file":"server/src/routes/ai.js","fingerprint":"08c44e703511941756749328a4035ccb43674cc82d91dee795211ef829fe01bc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUuid"},
{"file":"server/src/routes/announcements.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/announcements.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseId"},
{"file":"server/src/routes/announcements.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/announcements.js","fingerprint":"9fcc268e8e61e61364ca617c982d1cff4a1b76290ff19822ed363383be4c3cda","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/announcements.js","fingerprint":"f2b59eab3ecb251e0fc247952125ed33c0c87173c26052b01af7eb6c7fed2595","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:normalizeAnnouncementPolicyFields"},
{"file":"server/src/routes/announcements.js","fingerprint":"b7bbbb68fab3f1f34753f2cd1dd5d3834cc220df4be0dc915c252db56125ddd9","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/announcements"},
{"file":"server/src/routes/announcements.js","fingerprint":"16e3a77398374a9da0ba54d34380f16866000cd809d649d13c68a262437cecb8","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/announcements/:id"},
{"file":"server/src/routes/announcements.js","fingerprint":"b489caa88f9c4b0f9cbdd4ddaa317a550973dc746fe11c44f556d6b0be5c5c58","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/announcements/:id/acknowledge"},
{"file":"server/src/routes/announcements.js","fingerprint":"6f2ecb599e0390bc141c7c6a8874a4a0b04b55c3f9c9f249432829ef2aa4c21f","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/announcements/:id/acknowledge"},
{"file":"server/src/routes/apOperations.js","fingerprint":"48951ae2ad49593089e10d00afd95a5f18e10c2cbcb87f7956eb47a59d60cfa0","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/apOperations.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/ap-operations/reimbursement-ledger"},
{"file":"server/src/routes/approvalRules.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/approvalRules.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseApprovalRuleId"},
{"file":"server/src/routes/approvalRules.js","fingerprint":"cd6c77086d44835903ad40dc099ee6640c1ddfb1323f346af4ba86ed8a0ea323","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureAmountRangeValid"},
{"file":"server/src/routes/approvalRules.js","fingerprint":"993d7526060943199799457112b59cf24e63a46341d3dc1080ddb6c8d0af317c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureAmountRangeValid"},
{"file":"server/src/routes/approvalRules.js","fingerprint":"b8698fe0fd39540fb0a1d21bf610947927564bdf7150e35b98ee0077317cc84b","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureOwnerSelfFallbackRoleValid"},
{"file":"server/src/routes/approvalRules.js","fingerprint":"996fd561bfa6933b8a36273a25fecfecc94d0b366da830685336ddc7f1212eca","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/approval-rules/:id"},
{"file":"server/src/routes/assetMovements.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/assetMovements.js","fingerprint":"6c58fb0c0b2fb699a6a013d4d40a2b87c072068b06e766e74b0e092cc7a335d5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUuid"},
{"file":"server/src/routes/assetMovements.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/assetMovements.js","fingerprint":"62bc45cfdb1f68c9dd134641c77cdfdfb653555f2e498e397e39a40e41e166e8","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureMovementAllowed"},
{"file":"server/src/routes/assetMovements.js","fingerprint":"d5b13762c5f0318690bb04b0e4d006e066524b06b185f9615a335396e6f0c93c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureAssetExists"},
{"file":"server/src/routes/assetMovements.js","fingerprint":"22b2935f24450ede35fa143304840ecaa3496a12c034f5ad284c2eef01c50cba","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureActiveEmployee"},
{"file":"server/src/routes/assetMovements.js","fingerprint":"186bfa660e6ea4282e9f63afccc7348f2096c8402e8e901439fb15627837565a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureActiveEmployee"},
{"file":"server/src/routes/assetMovements.js","fingerprint":"9c55abdac47794fe22e2d5663060d2db6753aa236adba4be735827e00b11cbdb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/assets/:id/assign"},
{"file":"server/src/routes/assetMovements.js","fingerprint":"6a2623801e3905473f5af5a6aeb45e5ad43aa309026772ccdd536c6ffbad2e8f","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/assets/:id/return"},
{"file":"server/src/routes/assetMovements.js","fingerprint":"d5a1ffc7fafbd0cab7a4ba98d3a9da8817c9166e452d4e1997bb6dbc93ca052d","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/assets/:id/transfer"},
{"file":"server/src/routes/assetMovements.js","fingerprint":"b239d697b3f7d3d616328076dfc48f7f081e0d74f178031d8760214a2b333137","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/assets/:id/transfer"},
{"file":"server/src/routes/assets.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/assets.js","fingerprint":"6c58fb0c0b2fb699a6a013d4d40a2b87c072068b06e766e74b0e092cc7a335d5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUuid"},
{"file":"server/src/routes/assets.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCategoryListQuery"},
{"file":"server/src/routes/assets.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseAssetListQuery"},
{"file":"server/src/routes/assets.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseAssigneeListQuery"},
{"file":"server/src/routes/assets.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateBody"},
{"file":"server/src/routes/assets.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parsePatchBody"},
{"file":"server/src/routes/assets.js","fingerprint":"c871135b601c023c98f31d5555f893efbefce44a1c0964220df341897674629c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parsePatchBody"},
{"file":"server/src/routes/assets.js","fingerprint":"01a2842a9be803e2beaabaec7fe403c509d868712d5e38b0650eafe19567fb96","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateAssetTextBoundaries"},
{"file":"server/src/routes/assets.js","fingerprint":"01a2842a9be803e2beaabaec7fe403c509d868712d5e38b0650eafe19567fb96","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateRedactedImportBoundaries"},
{"file":"server/src/routes/assets.js","fingerprint":"940b4a2172252aa6e6bc4f8dd8865e3361ba2c7fd53c77414f3d9ef73c1fdffa","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDocumentCategoryAvailable"},
{"file":"server/src/routes/assets.js","fingerprint":"ea52a43d4fba3f07b80c3328f4ff96fedf5a51348bee2ef60d5bf721deaef49c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDocumentCategoryAvailable"},
{"file":"server/src/routes/assets.js","fingerprint":"d701b972a1ed447a07f76be872f47a5988c6c9dd5ae4edeb4badaadec3f4bf85","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDocumentCategoryAvailable"},
{"file":"server/src/routes/assets.js","fingerprint":"b50f5f62f57a8d2e85808fd7633568a49e91b8a821e2afdead6647a7ea84b159","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureAssetCategoryExists"},
{"file":"server/src/routes/assets.js","fingerprint":"0ba3108bacfe6c81125153caf72003c1bd3d445e8493290d1e7e30b56e456b84","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureAssetCategoryExists"},
{"file":"server/src/routes/assets.js","fingerprint":"80914f548ee5899eb71a178c4a13ad10646bdd2c0fed7acd44c5924581dfc2aa","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureAssetCategoryExists"},
{"file":"server/src/routes/assets.js","fingerprint":"6f091706e1ec75d17b76658dd81f7a3f7347110b8c6a5e7a81b7fda6020358f4","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureLegalEntityAvailable"},
{"file":"server/src/routes/assets.js","fingerprint":"b1fab48ce07d1d7f2308476dd38dfbcce526a64bd50de2ac484d2e5b0e3e8fff","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureLegalEntityAvailable"},
{"file":"server/src/routes/assets.js","fingerprint":"d5aa541584c0cf873d161fc6e31f82c8a1ec62b67eebd4444c5d52270123eb13","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterAvailable"},
{"file":"server/src/routes/assets.js","fingerprint":"96523ea82ed258eb92ca4990dc8bc6bb84d3e3bbf76d66dca60b15931455cdc8","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterAvailable"},
{"file":"server/src/routes/assets.js","fingerprint":"57ccb19330174bae06b385dca2cb8c41ce487980099e934fef19f8fc3a15cba7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterAvailable"},
{"file":"server/src/routes/assets.js","fingerprint":"22b2935f24450ede35fa143304840ecaa3496a12c034f5ad284c2eef01c50cba","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureActiveEmployee"},
{"file":"server/src/routes/assets.js","fingerprint":"186bfa660e6ea4282e9f63afccc7348f2096c8402e8e901439fb15627837565a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureActiveEmployee"},
{"file":"server/src/routes/assets.js","fingerprint":"49c2624e15c3d37dbae668f8c668461f5451fc4bf15fea7cac68c42ec099fdc8","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureApprovedPurchaseRequest"},
{"file":"server/src/routes/assets.js","fingerprint":"4ae96897d6ee2f02a7aa80c4d966629721cb8ea4c47b5713c32dfc6274ad3e5a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureApprovedPurchaseRequest"},
{"file":"server/src/routes/assets.js","fingerprint":"d5b13762c5f0318690bb04b0e4d006e066524b06b185f9615a335396e6f0c93c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/asset-categories/:id"},
{"file":"server/src/routes/assets.js","fingerprint":"442e2a0cd3f40bb741d5d4727944806abae5ce494465168d0a45fa8e9c3063ca","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/assets/import/redacted-doc-index"},
{"file":"server/src/routes/assets.js","fingerprint":"d5b13762c5f0318690bb04b0e4d006e066524b06b185f9615a335396e6f0c93c","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/assets/:id"},
{"file":"server/src/routes/attendance.js","fingerprint":"6ed67e68f41d5964a43a8a6e8d0ed71207418c81f7ad013aa3cfd9158f1ae29c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseInput"},
{"file":"server/src/routes/attendance.js","fingerprint":"2a45170a8628f3a739cf666505b0f43784def012f00d0ed87a220b3d70d79589","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/attendance.js","fingerprint":"4ff302aeefae7b9ad6f35e657c3d2abf738531a34d8f7e655f31e09a27dc41f8","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureUnifiedFeatureEnabled"},
{"file":"server/src/routes/attendance.js","fingerprint":"a657a09fd09b5fe31ee45ebfaaeda99ba30d6944bade370d5d0d7ac3c7456a5a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseOptionalDateTime"},
{"file":"server/src/routes/attendance.js","fingerprint":"99ebe0d43dca95cb33f043545c5302d439e6822f524e9f2c083f1d9f46feb362","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseMonthRange"},
{"file":"server/src/routes/attendance.js","fingerprint":"02626f66ac5688562048fbc15a189c27d9c88c08778562564396aa8534eed7d1","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/attendance/clock-in"},
{"file":"server/src/routes/attendance.js","fingerprint":"02626f66ac5688562048fbc15a189c27d9c88c08778562564396aa8534eed7d1","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/attendance/clock-out"},
{"file":"server/src/routes/attendance.js","fingerprint":"287252e0c5d2fdaadef7cfed6b3ba8982e97a10db47a8e422a877eda7aa4d4bb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/attendance/clock-out"},
{"file":"server/src/routes/attendance.js","fingerprint":"8e69dcaef6fa42597a8ca6804bbcc30d18e4641c50a5945a4171e2f3fec1f59f","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/attendance/admin/backfill"},
{"file":"server/src/routes/attendance.js","fingerprint":"3bc8430a72bd41e66bb2de2fc5b38cb78b7306a353e0beb21adf56eefce42840","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/attendance/admin/backfill"},
{"file":"server/src/routes/audit.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseQuery"},
{"file":"server/src/routes/audit.js","fingerprint":"510d8ad88dde0075df37838cd0b7ff6241145d700817ea5f203086abbae5b568","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseQuery"},
{"file":"server/src/routes/auth.js","fingerprint":"b7c458d92f0ef3e6a3d2246d397cb09b60253fd5e8d0d83e8298d1b341c54c50","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:emailLoginCodeError"},
{"file":"server/src/routes/auth.js","fingerprint":"b7c458d92f0ef3e6a3d2246d397cb09b60253fd5e8d0d83e8298d1b341c54c50","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:passwordResetCodeError"},
{"file":"server/src/routes/auth.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/login"},
{"file":"server/src/routes/auth.js","fingerprint":"dfe43a919ec5e4c8e4f579fe990e6b4aaa6e59fc8a60a4f5c30586434d6d1f12","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/login"},
{"file":"server/src/routes/auth.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/email-login/code"},
{"file":"server/src/routes/auth.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/email-login/verify"},
{"file":"server/src/routes/auth.js","fingerprint":"dfe43a919ec5e4c8e4f579fe990e6b4aaa6e59fc8a60a4f5c30586434d6d1f12","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/email-login/verify"},
{"file":"server/src/routes/auth.js","fingerprint":"dfe43a919ec5e4c8e4f579fe990e6b4aaa6e59fc8a60a4f5c30586434d6d1f12","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/refresh"},
{"file":"server/src/routes/auth.js","fingerprint":"961c7d1a4eaf9137354b9e70d31fb0496fc2be2d130d9a9967507b6a0ca1f5ad","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/deeplink"},
{"file":"server/src/routes/auth.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/change-password"},
{"file":"server/src/routes/auth.js","fingerprint":"dfe43a919ec5e4c8e4f579fe990e6b4aaa6e59fc8a60a4f5c30586434d6d1f12","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/change-password"},
{"file":"server/src/routes/auth.js","fingerprint":"cb6e800502eb1c1b8117b9ae62a8c894756d2a062c3ea24b52a369af9a3be7a7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/change-password"},
{"file":"server/src/routes/auth.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/forgot-password"},
{"file":"server/src/routes/auth.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/reset-password"},
{"file":"server/src/routes/auth.js","fingerprint":"cb6e800502eb1c1b8117b9ae62a8c894756d2a062c3ea24b52a369af9a3be7a7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/reset-password"},
{"file":"server/src/routes/budgetControlRules.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/budgetControlRules.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseRuleId"},
{"file":"server/src/routes/budgetControlRules.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseQuery"},
{"file":"server/src/routes/budgetControlRules.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/budgetControlRules.js","fingerprint":"7c065e9daea0385b2bfb3ab1e1948e602a6376bac9e44d5d8c90564cd0f1c1f8","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/budgetControlRules.js","fingerprint":"c92ca88007ea087989f14e6555add522f0ab717298fc801fafc942e1187c93cf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/budgets.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/budgets.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBudgetId"},
{"file":"server/src/routes/budgets.js","fingerprint":"4bcacc7fa10fe5395c9233195e0941962c3f9fa6115492445e654f070fe58075","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDepartmentAvailable"},
{"file":"server/src/routes/budgets.js","fingerprint":"7f37e2cba05a7d3afeaf7a814afb721bad3a3ef590eb388aeb9149b612ab9cef","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDepartmentAvailable"},
{"file":"server/src/routes/budgets.js","fingerprint":"8506eaeef8adc8cc277fcffb3ce92086e955ecadb4149df7b5a01363da60d337","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureLegalEntityAvailable"},
{"file":"server/src/routes/budgets.js","fingerprint":"c4967a64f972aa0396e182d2d06880ab684c25d2545466aad0854828740ad39e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureLegalEntityAvailable"},
{"file":"server/src/routes/budgets.js","fingerprint":"3cc514556b50dae2ebe09ef19b335d02d760c8375bf6d85dae2b9cce523f1870","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterAvailable"},
{"file":"server/src/routes/budgets.js","fingerprint":"dc93103cf32df2eb4384550e3ae7ebcc290879e69e11aad7eaea6481f8f414fb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterAvailable"},
{"file":"server/src/routes/budgets.js","fingerprint":"0bd9235198a75999811434ce3e70e584019d64555ede15c2202bd5f1030edf89","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterAvailable"},
{"file":"server/src/routes/budgets.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/budgets"},
{"file":"server/src/routes/budgets.js","fingerprint":"12506f55725f28e98268fb5ffef82d9e96e5b7738fea52108977bb3a0efd511f","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/budgets"},
{"file":"server/src/routes/budgets.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/budgets/:id"},
{"file":"server/src/routes/budgets.js","fingerprint":"92e4ca56a07acf54433ae06c7af4755bd7aa80956a055e41da4388cb5ac61962","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/budgets/:id"},
{"file":"server/src/routes/confidentialDocRefs.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/confidentialDocRefs.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/confidentialDocRefs.js","fingerprint":"09dcc86e0b4bc71f7bd78b881d1bb47f727736ba45d232d08ed0bf87c23072de","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/confidentialDocRefs.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/confidential-doc-refs"},
{"file":"server/src/routes/configRead.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseEnabledQuery"},
{"file":"server/src/routes/configRead.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseDocumentTypeParams"},
{"file":"server/src/routes/contractsRegistry.js","fingerprint":"0d7d8d325c8629980a1d6f1c569255c2995059d5b19667be27f57eed2ea837af","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/contractsRegistry.js","fingerprint":"254541548938a93942be3a6f1bd3b33b17c82aa977fd75228871a5842d9c10c1","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/contractsRegistry.js","fingerprint":"92558dc9d5ba5ab7224a8267f2b79920feaec4457df73b8f30cacb6aef9f12d3","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:updateContractWithStateGuard"},
{"file":"server/src/routes/contractsRegistry.js","fingerprint":"cba9e7b82b29c4ec1f89720620c1f78ea2d67133338d9957c8baece08306bd2b","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/contracts-registry/:id"},
{"file":"server/src/routes/contractsRegistry.js","fingerprint":"60347bec707618636867c4310250919b6443c10f22f88f68f6c8d2c3de565d13","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/contracts-registry/:id"},
{"file":"server/src/routes/contractsRegistry.js","fingerprint":"0f95d3875c822fa7d8c016aa7be48ad2d20a658b57bed0cbed14dcfe4196a2b9","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/contracts-registry/:id/terminate"},
{"file":"server/src/routes/contractsRegistry.js","fingerprint":"c5fc8ab4ac98ddff39aa922bbd24744a815c5c423aa5b18687dc59611f1273da","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/contracts-registry/:id/terminate"},
{"file":"server/src/routes/contractsRegistry.js","fingerprint":"ad732dac27597f2483143c7f845399a79c2a276d4b89e32fcb96f583930b4240","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/contracts-registry/:id/corrections"},
{"file":"server/src/routes/contractsRegistry.js","fingerprint":"d3204f15378a5bfcdea2de26e96d097a89e142fa3329a2d7f742b917f2f22c39","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/contracts-registry/:id/corrections"},
{"file":"server/src/routes/contractsRegistry.js","fingerprint":"80c9775ffbd6d8c8f43d0baf4539ffa3649a923f74b3973d8fe78fff4a51ab26","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/contracts-registry/:id/renew"},
{"file":"server/src/routes/cutover.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/cutover.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseId"},
{"file":"server/src/routes/cutover.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/cutover.js","fingerprint":"43aaacd8708bb5c07aa913145a5dfa84cd402038b1fee3ada860ee249f8ce9c3","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/dashboard.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseQuery"},
{"file":"server/src/routes/dashboard.js","fingerprint":"510d8ad88dde0075df37838cd0b7ff6241145d700817ea5f203086abbae5b568","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseQuery"},
{"file":"server/src/routes/delegations.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseDelegationId"},
{"file":"server/src/routes/delegations.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateBody"},
{"file":"server/src/routes/delegations.js","fingerprint":"3e5bc2343859761fdfae1aedfba4dd9d8d252d16e479b01a5ac477e25b700209","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateBody"},
{"file":"server/src/routes/delegations.js","fingerprint":"abc5f3ef3dcaeb55ac80eb31853eed3c582d7c7ded7421c3903effdb2b83fabb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateBody"},
{"file":"server/src/routes/delegations.js","fingerprint":"1f1a149c83f74bc04b86baf4116b9601c8956a4a0bf75a66c3848108e9681fbb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateBody"},
{"file":"server/src/routes/delegations.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateGrantBody"},
{"file":"server/src/routes/delegations.js","fingerprint":"3e5bc2343859761fdfae1aedfba4dd9d8d252d16e479b01a5ac477e25b700209","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateGrantBody"},
{"file":"server/src/routes/delegations.js","fingerprint":"abc5f3ef3dcaeb55ac80eb31853eed3c582d7c7ded7421c3903effdb2b83fabb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateGrantBody"},
{"file":"server/src/routes/delegations.js","fingerprint":"1f1a149c83f74bc04b86baf4116b9601c8956a4a0bf75a66c3848108e9681fbb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateGrantBody"},
{"file":"server/src/routes/delegations.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseStatusQuery"},
{"file":"server/src/routes/delegations.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListDelegationGrantsQuery"},
{"file":"server/src/routes/delegations.js","fingerprint":"c65eb61b3d8ca109854fc10890d4307723bbd03dc93d1549964d328ae9bc5069","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDelegateEligible"},
{"file":"server/src/routes/delegations.js","fingerprint":"2d1c39e41522f8863cd6928b50a11bb9958d348b7a9f20c5292608e650667c17","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDelegateEligible"},
{"file":"server/src/routes/delegations.js","fingerprint":"dd4725b4c2c79ef5e59dbe55b5eb4c177d167186dc7ef8b6d17f6f61bebc43a5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDelegateEligible"},
{"file":"server/src/routes/delegations.js","fingerprint":"86ec38bd74707cd6917fe2a7ffc79440867b6a37e71efb3b6981aa9a7b560194","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/delegation-grants"},
{"file":"server/src/routes/delegations.js","fingerprint":"1ba976c328b41f63396aeb96618d8c525df8255f3ce17510fd019421207bd3d5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/delegation-grants"},
{"file":"server/src/routes/delegations.js","fingerprint":"86ec38bd74707cd6917fe2a7ffc79440867b6a37e71efb3b6981aa9a7b560194","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/delegations"},
{"file":"server/src/routes/departments.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseDepartmentId"},
{"file":"server/src/routes/departments.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/departments.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateBody"},
{"file":"server/src/routes/departments.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUpdateBody"},
{"file":"server/src/routes/departments.js","fingerprint":"dd4c7435c5433e19eb9893f762c76225772c4cf62eb01c6c45d7b8d1b207705a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUpdateBody"},
{"file":"server/src/routes/departments.js","fingerprint":"5326d4c4b0ac27a96de8b209570f3d8ee687aad427a8cb2b9957484b287e02ed","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/departments"},
{"file":"server/src/routes/departments.js","fingerprint":"8d444b923b812b3afe71913b82f132c3955e73935ebd12361bab0edd67214d28","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/departments"},
{"file":"server/src/routes/departments.js","fingerprint":"fa47bac9301143e633b50167c8fab4a40a1edb9397734104569905b99f5ad4aa","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/departments/:id"},
{"file":"server/src/routes/departments.js","fingerprint":"0c6eb41240d31840a48750871c49320b8feecf58984a2a969144276fac1298f7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/departments/:id"},
{"file":"server/src/routes/departments.js","fingerprint":"5326d4c4b0ac27a96de8b209570f3d8ee687aad427a8cb2b9957484b287e02ed","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/departments/:id"},
{"file":"server/src/routes/departments.js","fingerprint":"8d444b923b812b3afe71913b82f132c3955e73935ebd12361bab0edd67214d28","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/departments/:id"},
{"file":"server/src/routes/departments.js","fingerprint":"71cf92240dcb163524d928e3f792ac176896ff89107986f3fd9ad119ad49aa47","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/departments/:id"},
{"file":"server/src/routes/departments.js","fingerprint":"17e1fd34460204ab475dc670a1b4ea61a1f369ebbd8dd2efe5eec265255701f7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/departments/:id/archive"},
{"file":"server/src/routes/departments.js","fingerprint":"1e15674977106d900a40e34fe64657152da9dfe383e63fa88e6c0de70c4cb8d5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/departments/:id/archive"},
{"file":"server/src/routes/employees.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/employees.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseId"},
{"file":"server/src/routes/employees.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/employees.js","fingerprint":"64ec8f0736b31d4a76082fa397b9adf9107f0628c97b9547026f55a3629f71cd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/employees.js","fingerprint":"ab1ed75076ba8a91674136da33198f3bd082cf5c9633b45e97f78c456a0471dc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureWecomHrMasterSyncEnabled"},
{"file":"server/src/routes/employees.js","fingerprint":"cc8894f77bdedd30913ca06dae19111faa1c97bc65078db3821925a514742afa","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureWecomHrMasterSyncEnabled"},
{"file":"server/src/routes/employees.js","fingerprint":"99617ed3d2198ffedead181d3d347c8c6b3be4d886c92bcfa7c58f39389a6d57","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateContractDates"},
{"file":"server/src/routes/employees.js","fingerprint":"cdf73963d211c5dc959bbd8d24d489f7d33a2024f7613c6033341c15e4d8dc4e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateContractDates"},
{"file":"server/src/routes/employees.js","fingerprint":"99617ed3d2198ffedead181d3d347c8c6b3be4d886c92bcfa7c58f39389a6d57","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateTrainingDates"},
{"file":"server/src/routes/employees.js","fingerprint":"606fa5a41e86d696a118edf385c6a6e2f4bf1b2c41288420ebcbe01d46eac084","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateCertificateDates"},
{"file":"server/src/routes/employees.js","fingerprint":"9d60ee3ecc95ce9a699e29cf140233c55b0d8a3ee95946b4ec5f07f66d9610c9","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildContractAttachmentBlockedError"},
{"file":"server/src/routes/employees.js","fingerprint":"7ccdc0b4a4431f13ce618cd19f4da8fe8167d21ca3bcb55ae0bc7249f23b5607","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:employeesRoutes"},
{"file":"server/src/routes/employees.js","fingerprint":"3efb722614f1cdee938215738adeb5b2d4930fdb24c470e560b69244f0bcae7e","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:employeesRoutes"},
{"file":"server/src/routes/employees.js","fingerprint":"fd1f07cbb59e6103dfac77bafd01b3ed7a0ce88a5b45ea9f3d7238776249d498","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:employeesRoutes"},
{"file":"server/src/routes/employees.js","fingerprint":"71935e997078aefd6df41afdb612ae8b79e5e67356bcaea3c984b414cff95f57","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:employeesRoutes"},
{"file":"server/src/routes/employees.js","fingerprint":"0c5c7e91b73ae8e0c05e8c3a34dbbdb250559b2f46385fe5c66ed1aff3a02166","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:employeesRoutes"},
{"file":"server/src/routes/employees.js","fingerprint":"561527fb2f4fdce80bdcb3c9b02a1afc9c2664dfd9bcb592f7e554b0b447b8fa","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:employeesRoutes"},
{"file":"server/src/routes/employees.js","fingerprint":"cabd6d4585a224ce1759dbcb60d03a9544950051604a6cffaf0ad7b530f92a2c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:employeesRoutes"},
{"file":"server/src/routes/employees.js","fingerprint":"37cc3fcf7eb9efd9a9270eb9a0bcf68b4d083d484e70541c6afd5bb67ea1b3ce","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/employees/:id/wecom-directory-sync"},
{"file":"server/src/routes/employees.js","fingerprint":"37cc3fcf7eb9efd9a9270eb9a0bcf68b4d083d484e70541c6afd5bb67ea1b3ce","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/employees/:id/wecom-directory-sync/retry"},
{"file":"server/src/routes/employees.js","fingerprint":"bddf88642c81d891ef3da8820dc127dfa2ceffb3b74983791dd7deb00d2b32c5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/employees/:id/contracts"},
{"file":"server/src/routes/employees.js","fingerprint":"bddf88642c81d891ef3da8820dc127dfa2ceffb3b74983791dd7deb00d2b32c5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/employees/:id/contracts/:contractId"},
{"file":"server/src/routes/employees.js","fingerprint":"7c5310f7b2163f74cc5120711effbbd1f1bdc2cdfcd49d4e00b99665e2b1580a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:employeesRoutes"},
{"file":"server/src/routes/employees.js","fingerprint":"06f4a2e303107e87d1e94009275b39ed2838c7ec0fc45595a64dbbc4331b1106","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/employees/:id/contracts/:contractId"},
{"file":"server/src/routes/employees.js","fingerprint":"2b6b9e0ebbca3a448aaa94b95a2d6d43e3a14ccc3f5aeca4f956deec94aa3107","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/employees/:id/profile"},
{"file":"server/src/routes/expenseStandards.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/expenseStandards.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseExpenseStandardId"},
{"file":"server/src/routes/expenseStandards.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/expenseStandards.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/expenseStandards.js","fingerprint":"b9dc511484348ded712b71280db3e49c789be0666623eed6cb9cf7a4468aedf9","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/expenseStandards.js","fingerprint":"30b8f6d233001091ad09d7f6dc365136091288a663f1458bbce7201de1218b2c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureExpenseTypeExists"},
{"file":"server/src/routes/expenseStandards.js","fingerprint":"a0b01e1f14b7ede96e7cd1e8252c748afdf125feaa51441a37b9aabb363a328b","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureScopeKeyValid"},
{"file":"server/src/routes/expenseStandards.js","fingerprint":"a5ad69546d526d962f12d1c7e1d0e24a153f585fa6d61daa123fc1a2843f580e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureScopeKeyValid"},
{"file":"server/src/routes/expenseStandards.js","fingerprint":"d67e306450dee53a89c46327da7ca8273417de0f4f74f80a46b9711f513301c9","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/expense-standards"},
{"file":"server/src/routes/expenseStandards.js","fingerprint":"c871135b601c023c98f31d5555f893efbefce44a1c0964220df341897674629c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/expense-standards/:id"},
{"file":"server/src/routes/expenseStandards.js","fingerprint":"d67e306450dee53a89c46327da7ca8273417de0f4f74f80a46b9711f513301c9","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/expense-standards/:id"},
{"file":"server/src/routes/expenseTypes.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/expenseTypes.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/expenseTypes.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseExpenseTypeId"},
{"file":"server/src/routes/expenseTypes.js","fingerprint":"6fefcf7886d8aca7c344962a4621880f7da29f0915282c8763cf48d3049279a8","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/expense-types"},
{"file":"server/src/routes/expenseTypes.js","fingerprint":"dd4c7435c5433e19eb9893f762c76225772c4cf62eb01c6c45d7b8d1b207705a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/expense-types/:id"},
{"file":"server/src/routes/expenseTypes.js","fingerprint":"6fefcf7886d8aca7c344962a4621880f7da29f0915282c8763cf48d3049279a8","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/expense-types/:id"},
{"file":"server/src/routes/exports.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseQuery"},
{"file":"server/src/routes/exports.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/exports.js","fingerprint":"510d8ad88dde0075df37838cd0b7ff6241145d700817ea5f203086abbae5b568","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:toDateRange"},
{"file":"server/src/routes/exports.js","fingerprint":"48951ae2ad49593089e10d00afd95a5f18e10c2cbcb87f7956eb47a59d60cfa0","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureApOperationsExportEnabled"},
{"file":"server/src/routes/exports.js","fingerprint":"0160d938808a2f4bbee5beb67caad7ddcbea52af30db293b600c23db638e57f3","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureContractsRegistryExportEnabled"},
{"file":"server/src/routes/exports.js","fingerprint":"a0125087df9691c0e5f6f2989910035efba279b7f0bffdc10579765966fb150c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:toDateOnlyRange"},
{"file":"server/src/routes/exports.js","fingerprint":"67b9d4a42e614d1bf96832420680b7ca2ea4749f910828802ff3170eaeda6e64","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/exports/payslips"},
{"file":"server/src/routes/exports.js","fingerprint":"6d38b77a953af94b8bff5a99e5b3ab22f28507853694cd6fd50d38589850f8f1","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/exports/presence-history"},
{"file":"server/src/routes/exports.js","fingerprint":"510d8ad88dde0075df37838cd0b7ff6241145d700817ea5f203086abbae5b568","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/exports/audit-logs"},
{"file":"server/src/routes/featureFlags.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/featureFlags.js","fingerprint":"5fad3764129696d62548c50a8b78ee8f322b718e877eb8950d98ac0c331173c3","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildVersionConflictError"},
{"file":"server/src/routes/featureFlags.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseKey"},
{"file":"server/src/routes/featureFlags.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parsePageQuery"},
{"file":"server/src/routes/featureFlags.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUpdateBody"},
{"file":"server/src/routes/featureFlags.js","fingerprint":"6ef26793cac05e83ddd65bf556897f795f41584fe69f21c6b4842554f2e1c087","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUpdateBody"},
{"file":"server/src/routes/featureFlags.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBatchBody"},
{"file":"server/src/routes/featureFlags.js","fingerprint":"4d213e5dda91586732d96d078bbff9ee63351c094c47bc8d31b3e7450f8f1aab","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:featureFlagsRoutes"},
{"file":"server/src/routes/feeControl.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/feeControl.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/integrations.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/integrations.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseOutboxListQuery"},
{"file":"server/src/routes/integrations.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBindingsListQuery"},
{"file":"server/src/routes/integrations.js","fingerprint":"6c58fb0c0b2fb699a6a013d4d40a2b87c072068b06e766e74b0e092cc7a335d5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUuidParam"},
{"file":"server/src/routes/integrations.js","fingerprint":"48d75541b43988964ecf85d59f32007713a3be1bd381ef2bf4bcb3b38dc839bf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:requireErpManagementGate"},
{"file":"server/src/routes/integrations.js","fingerprint":"8a83e372ba9211a169ca20935d21336c95349f5db935bb4b4e73fa4c62912a43","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensurePaidReimbursement"},
{"file":"server/src/routes/integrations.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:normalizeProcurementTargets"},
{"file":"server/src/routes/integrations.js","fingerprint":"ac52892b0856c64c0b8918ee2e323d5965f8db02f2026199498e89128c7542fb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureProcurementTargetEligible"},
{"file":"server/src/routes/integrations.js","fingerprint":"e1eaff595d742e37768ec5e6b5859a29604d1a54fbee9da795bebb9397abf95c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureProcurementTargetEligible"},
{"file":"server/src/routes/integrations.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:normalizeResourcesInput"},
{"file":"server/src/routes/integrations.js","fingerprint":"efc285e1cd886a88a6e57e61ce5da1a202409c21a930621af47f4f8052ba90b9","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/integrations/outbox/:id/retry"},
{"file":"server/src/routes/integrations.js","fingerprint":"26c3c1c908fda8c7350351ee2f11769acdbf1bdee51bce704759d7d07f5eadae","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/integrations/erp/master-data/sync"},
{"file":"server/src/routes/integrations.js","fingerprint":"399f9ec607c4403c3c6c8763433114d192a5df162ba7d0fdd2fe03aaeb572ac7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/integrations/erp/journals/reimbursements/:reimbursementId/preview"},
{"file":"server/src/routes/integrations.js","fingerprint":"399f9ec607c4403c3c6c8763433114d192a5df162ba7d0fdd2fe03aaeb572ac7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/integrations/erp/journals/reimbursements/:reimbursementId/export"},
{"file":"server/src/routes/integrations.js","fingerprint":"095bb8a253c5934e8f8629e16c38b388f6d2cc6354343bf8506cf0787917c09e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/integrations/erp/procurement/purchase-requests/:purchaseRequestId/track"},
{"file":"server/src/routes/integrations.js","fingerprint":"095bb8a253c5934e8f8629e16c38b388f6d2cc6354343bf8506cf0787917c09e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/integrations/erp/procurement/purchase-requests/:purchaseRequestId/requeue"},
{"file":"server/src/routes/integrations.js","fingerprint":"6392bfcb435833f01d57d08f451784a96334daa7151cef168939c1160ad93fcd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/integrations/erp/assets/:assetId/track"},
{"file":"server/src/routes/integrations.js","fingerprint":"6392bfcb435833f01d57d08f451784a96334daa7151cef168939c1160ad93fcd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/integrations/erp/assets/:assetId/requeue"},
{"file":"server/src/routes/integrations.js","fingerprint":"1298b6503bead86f2a1f836887a4019b863e86cf40e33200ff554fac72afe4a6","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/integrations/erp/travel/reconciliation-batches/:batchId/track"},
{"file":"server/src/routes/integrations.js","fingerprint":"1298b6503bead86f2a1f836887a4019b863e86cf40e33200ff554fac72afe4a6","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/integrations/erp/travel/reconciliation-batches/:batchId/requeue"},
{"file":"server/src/routes/integrations.js","fingerprint":"9c6bf2a19d60e08f038020fe4faef2823e46f32d5d5523e4a484c5175840fc1c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/integrations/erp/travel/reconciliation-batches/:batchId/requeue"},
{"file":"server/src/routes/integrations.js","fingerprint":"399f9ec607c4403c3c6c8763433114d192a5df162ba7d0fdd2fe03aaeb572ac7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/integrations/erp/sync/:reimbursementId"},
{"file":"server/src/routes/inventoryChecks.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/inventoryChecks.js","fingerprint":"6c58fb0c0b2fb699a6a013d4d40a2b87c072068b06e766e74b0e092cc7a335d5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUuid"},
{"file":"server/src/routes/inventoryChecks.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/inventoryChecks.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/inventoryChecks.js","fingerprint":"435181ebd3fd0f7b87dc2c1708ae0454eec366f3084a133cf0bf7e292259dd27","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureScopeReference"},
{"file":"server/src/routes/inventoryChecks.js","fingerprint":"da55de4248c5c6a9283d50da6dd09edba0ce3f0a9e6a91af8e2d7d62b0785a4b","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureScopeReference"},
{"file":"server/src/routes/inventoryChecks.js","fingerprint":"d5b13762c5f0318690bb04b0e4d006e066524b06b185f9615a335396e6f0c93c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureInventoryCheck"},
{"file":"server/src/routes/inventoryChecks.js","fingerprint":"1bf348c5073f0a17d62a5ae67b998e93fe35c77c2abe4cb0784c153ca4f8272a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCheckStatus"},
{"file":"server/src/routes/inventoryChecks.js","fingerprint":"0492f0a2dab5df4df9bdd0504d07e51d03f2c56d6e0e516abb2c5ecac159c545","count":3,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureAssetInScope"},
{"file":"server/src/routes/inventoryChecks.js","fingerprint":"5a020ee415b339c0a8064f07afa8fd5c6e3b305186e24d2d1653cdc3143f1277","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/inventory-checks/:id/items"},
{"file":"server/src/routes/inventoryChecks.js","fingerprint":"466c3a313730aa3a9720bb5b37447aa5d1fa274961d8f64838c66b2780e69ef3","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/inventory-checks/:id/items"},
{"file":"server/src/routes/invoicePool/read.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseInvoiceListQuery"},
{"file":"server/src/routes/invoicePool/read.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBindableInvoiceQuery"},
{"file":"server/src/routes/invoicePool/read.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseInvoiceStatusQuery"},
{"file":"server/src/routes/invoicePool/read.js","fingerprint":"ec1e58e420efffb320f1456a4fb3931dd289f2aa0f6a9f14c555cae9042326fb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseInvoiceStatusQuery"},
{"file":"server/src/routes/invoicePool/read.js","fingerprint":"1ae6e5bbcf72222ccc4bc1c05914feaab32cf287ae1b775ea3cdf21aa1b2d841","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/bindable"},
{"file":"server/src/routes/invoicePool/read.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id"},
{"file":"server/src/routes/invoicePool/read.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/signed-url"},
{"file":"server/src/routes/invoicePool/shared/schemas.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseVerificationListQuery"},
{"file":"server/src/routes/invoicePool/verification.js","fingerprint":"4cedb22d4287fa69cd3d72c48ea82eb6afec9f21178d4cf779c73190e47c3acc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:throwVerificationDisabled"},
{"file":"server/src/routes/invoicePool/verification.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/verify"},
{"file":"server/src/routes/invoicePool/verification.js","fingerprint":"a09086681b58bdc8f82ade2e261299b2b3f870dda6095179fd1bb0a7d29a07d7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/verify"},
{"file":"server/src/routes/invoicePool/verification.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/verification"},
{"file":"server/src/routes/invoicePool/verification.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/verification/retry"},
{"file":"server/src/routes/invoicePool/verification.js","fingerprint":"d68782bf13b2a21884d9d0201289a6a7c31ed5b96dfe2fe42c1b402eddc59860","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/verification/retry"},
{"file":"server/src/routes/invoicePool/verification.js","fingerprint":"a09086681b58bdc8f82ade2e261299b2b3f870dda6095179fd1bb0a7d29a07d7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/verification/retry"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"b25d67f027d2694c50084f1e0711f2ced9e00f7df8200ee4027a71d08a1e8ccf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/upload"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"10705bd1e81bbfea513d812195a089c4bdd941e1f605f79e385c289a33b40fa1","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/upload"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"e2ff709dad27fe2e1b4729f73cf486907d2b63fc737309db2f4e722cdc09d54a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/upload"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"a351e1777344b858d6c66ba287c5b97ded0b99f3fda956cd42c05e5902425dbc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/upload"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"e4e5f0ca126ee9f9628b83f636647163895316df68e471e73948e8f721fc0c6d","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/upload"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"209e6cf90ca2987da39a2f283be0e303f6b3d04cdaa768ac8516352c8b43e43f","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/upload"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"323050fa9f5d706ccb2fbfc571c65db89da2a0abd86bd83d76247e6eed8825a3","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/upload"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"1fd8947fdbb80a0662debcc3fcb5a190bac9b9faae25d32f786c58290bbf21cb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/upload"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"4ea167a563f4638e4a6374c75b583eb0b9cb2e9a44972a7a2a918f66a3fb6479","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"996fd561bfa6933b8a36273a25fecfecc94d0b366da830685336ddc7f1212eca","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"4c88143346d3120b6288493bff1f7b1058e83024267177d6a040536ee522ef6f","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"6b7b1d70009de6e6ebc036a19d7c609d391e389c796e66917f8b052cd3733cd5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"aaa74bc004c1950f6bec6c292f64fac1bfd272711d91b63e0dae05fe60726112","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"755bc115dd1b0bfe92dafbf208638cfbdd64cb9c3397efd445f4951330c6b42c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"aaa1935c5f3309a25eab1efcd31752ef61b87a21b0a0e31cd2453025079b0861","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"26fae04e6caa2559ff1618111dc5eb4efa38c8844a4a6c1b9907408e578f825f","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"1c0abb2c2a9c5e68ab6d30a6bd151040f763e19ba4954f286ce5590b4c352cc1","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/archive"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"dd7712f7af206606ec186567d5aa5d472ba35e16d6c358bcb083fd59bac37e2d","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/archive"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"5901b98322f4641858b8140c0e996c83b377520c04839a5c0172203d336c19ba","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/archive"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/retry-ocr"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"0935512f403e99598a11968a8df9bebb90ce269d3c912b1651788dde85a56d55","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/retry-ocr"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"8ef9867968f4f4798471c123d10bb20e52622b9c47f6029b4e3fb2f4ace11061","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/retry-ocr"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/override-duplicate"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"4ea167a563f4638e4a6374c75b583eb0b9cb2e9a44972a7a2a918f66a3fb6479","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/override-duplicate"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"ccbeddf84642069d880ee7c3dc6fe2553cd272ae1ef05d8462e8645d8e2186af","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/override-duplicate"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"e21d267cf852d71361e8b4ae849a80a9e5aadfb610f8a16522a5cc6b3c532f76","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/override-duplicate"},
{"file":"server/src/routes/invoicePool.js","fingerprint":"f5cfabfacf1f557b5bc7812e5c88e8b1901712b2de6dfb31a97b8b26d4ced7e7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/invoice-pool/:id/override-duplicate"},
{"file":"server/src/routes/leave-types.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/leave-types.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseTypeId"},
{"file":"server/src/routes/leave-types.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseTypeQuery"},
{"file":"server/src/routes/leave-types.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseTypeBody"},
{"file":"server/src/routes/leave-types.js","fingerprint":"3a1dcc123e523c78566da31e7be2f3c32ec6cca7a1405869d9ff7cec8f51c970","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/leave-types"},
{"file":"server/src/routes/leave-types.js","fingerprint":"3a1dcc123e523c78566da31e7be2f3c32ec6cca7a1405869d9ff7cec8f51c970","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/leave-types/:id"},
{"file":"server/src/routes/masterData.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/masterData.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUuid"},
{"file":"server/src/routes/masterData.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/masterData.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateBody"},
{"file":"server/src/routes/masterData.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parsePatchBody"},
{"file":"server/src/routes/masterData.js","fingerprint":"c871135b601c023c98f31d5555f893efbefce44a1c0964220df341897674629c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parsePatchBody"},
{"file":"server/src/routes/masterData.js","fingerprint":"8506eaeef8adc8cc277fcffb3ce92086e955ecadb4149df7b5a01363da60d337","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureLegalEntityAvailable"},
{"file":"server/src/routes/masterData.js","fingerprint":"fdc96cb05717a00bf966b3db85f546eb54af6008074759298bb2532b1af926fa","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureLegalEntityAvailable"},
{"file":"server/src/routes/masterData.js","fingerprint":"57bed79bc70017e6d9ed3c59bfab4bbea560ee2b688d24e44d3a9e180bbb2d89","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureOwnerEmployeeAvailable"},
{"file":"server/src/routes/masterData.js","fingerprint":"7d66e43d79549021e2d29e57f896c77cb1a141106745c7fcef0921ef92eac9c6","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterParent"},
{"file":"server/src/routes/masterData.js","fingerprint":"28c3d33c45d69dc5760112592b2e589e13c781f795e0ee212e8fb44b363af293","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterParent"},
{"file":"server/src/routes/masterData.js","fingerprint":"84943edb161885fdf0358991a25cb76ab086fbd61abc0a918c98260319a2b631","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterParent"},
{"file":"server/src/routes/masterData.js","fingerprint":"ed6740bc6dd3fe1dcf7c6f61dcf0faaee12827f3d31984c9837dcc3e87a79070","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterParent"},
{"file":"server/src/routes/masterData.js","fingerprint":"246729bbf97af47de4eb151bba65d11dd270b868a2b928b3fab16ff1c5a230e7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterParent"},
{"file":"server/src/routes/masterData.js","fingerprint":"3a1dcc123e523c78566da31e7be2f3c32ec6cca7a1405869d9ff7cec8f51c970","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/legal-entities"},
{"file":"server/src/routes/masterData.js","fingerprint":"c57bb2bf24ae236cb93cf16651897e8c0b3da2be2744c6e5421311359c385c12","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/legal-entities/:id"},
{"file":"server/src/routes/masterData.js","fingerprint":"3a1dcc123e523c78566da31e7be2f3c32ec6cca7a1405869d9ff7cec8f51c970","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/legal-entities/:id"},
{"file":"server/src/routes/masterData.js","fingerprint":"ca92f646662d11a4962bd5affd2bd7355fdfc5e89c32d3a9ab63a55f83b80f55","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/legal-entities/:id/archive"},
{"file":"server/src/routes/masterData.js","fingerprint":"3a1dcc123e523c78566da31e7be2f3c32ec6cca7a1405869d9ff7cec8f51c970","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/cost-centers"},
{"file":"server/src/routes/masterData.js","fingerprint":"c57bb2bf24ae236cb93cf16651897e8c0b3da2be2744c6e5421311359c385c12","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/cost-centers/:id"},
{"file":"server/src/routes/masterData.js","fingerprint":"3a1dcc123e523c78566da31e7be2f3c32ec6cca7a1405869d9ff7cec8f51c970","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/cost-centers/:id"},
{"file":"server/src/routes/masterData.js","fingerprint":"693964faf025faa1aa110ecdd0ddc2a00538646cd68ab9956d46ef51ad584c9b","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/cost-centers/:id/archive"},
{"file":"server/src/routes/masterData.js","fingerprint":"3a1dcc123e523c78566da31e7be2f3c32ec6cca7a1405869d9ff7cec8f51c970","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/document-categories"},
{"file":"server/src/routes/masterData.js","fingerprint":"c57bb2bf24ae236cb93cf16651897e8c0b3da2be2744c6e5421311359c385c12","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/document-categories/:id"},
{"file":"server/src/routes/masterData.js","fingerprint":"3a1dcc123e523c78566da31e7be2f3c32ec6cca7a1405869d9ff7cec8f51c970","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/document-categories/:id"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUuid"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateBody"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parsePatchBody"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"c871135b601c023c98f31d5555f893efbefce44a1c0964220df341897674629c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parsePatchBody"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseReplaceDaysBody"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"8837b7f15919ad77abb757977808b0de05190f54a8df757a34637464b2d00d20","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseReplaceDaysBody"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"8506eaeef8adc8cc277fcffb3ce92086e955ecadb4149df7b5a01363da60d337","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureLegalEntityAvailable"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"fdc96cb05717a00bf966b3db85f546eb54af6008074759298bb2532b1af926fa","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureLegalEntityAvailable"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"3a1dcc123e523c78566da31e7be2f3c32ec6cca7a1405869d9ff7cec8f51c970","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/holiday-calendars"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"c57bb2bf24ae236cb93cf16651897e8c0b3da2be2744c6e5421311359c385c12","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/holiday-calendars/:id"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"3a1dcc123e523c78566da31e7be2f3c32ec6cca7a1405869d9ff7cec8f51c970","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/holiday-calendars/:id"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"c57bb2bf24ae236cb93cf16651897e8c0b3da2be2744c6e5421311359c385c12","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:masterDataHolidayCalendarsRoutes"},
{"file":"server/src/routes/masterDataHolidayCalendars.js","fingerprint":"27733508586c12b8ad87835d0b8eba2a47d1d70c27a7dd86b9424d106b0cc3df","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/holiday-calendars/:id/archive"},
{"file":"server/src/routes/masterDataSuppliers.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/masterDataSuppliers.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUuid"},
{"file":"server/src/routes/masterDataSuppliers.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/masterDataSuppliers.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateBody"},
{"file":"server/src/routes/masterDataSuppliers.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parsePatchBody"},
{"file":"server/src/routes/masterDataSuppliers.js","fingerprint":"c871135b601c023c98f31d5555f893efbefce44a1c0964220df341897674629c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parsePatchBody"},
{"file":"server/src/routes/masterDataSuppliers.js","fingerprint":"8506eaeef8adc8cc277fcffb3ce92086e955ecadb4149df7b5a01363da60d337","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureLegalEntityAvailable"},
{"file":"server/src/routes/masterDataSuppliers.js","fingerprint":"fdc96cb05717a00bf966b3db85f546eb54af6008074759298bb2532b1af926fa","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureLegalEntityAvailable"},
{"file":"server/src/routes/masterDataSuppliers.js","fingerprint":"3a1dcc123e523c78566da31e7be2f3c32ec6cca7a1405869d9ff7cec8f51c970","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/suppliers"},
{"file":"server/src/routes/masterDataSuppliers.js","fingerprint":"c57bb2bf24ae236cb93cf16651897e8c0b3da2be2744c6e5421311359c385c12","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/suppliers/:id"},
{"file":"server/src/routes/masterDataSuppliers.js","fingerprint":"3a1dcc123e523c78566da31e7be2f3c32ec6cca7a1405869d9ff7cec8f51c970","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/master-data/suppliers/:id"},
{"file":"server/src/routes/me.js","fingerprint":"91b404962c52c958cb17eba0f9c855f35e008307aae5b8e19e6a849f5ff36b0e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/me.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseNotificationPreferences"},
{"file":"server/src/routes/migrationRuns.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/migrationRuns.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseId"},
{"file":"server/src/routes/migrationRuns.js","fingerprint":"43aaacd8708bb5c07aa913145a5dfa84cd402038b1fee3ada860ee249f8ce9c3","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/migrationRuns.js","fingerprint":"3efb722614f1cdee938215738adeb5b2d4930fdb24c470e560b69244f0bcae7e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/migration-runs/dry-run"},
{"file":"server/src/routes/migrationRuns.js","fingerprint":"f19072dfa882b5edb238bce9545f1adb9ffb544f3571b0bf3f7ba50e28cc5afe","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/migration-runs/dry-run"},
{"file":"server/src/routes/notifications.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/notifications.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/notifications.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseDeliveryQuery"},
{"file":"server/src/routes/notifications.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseNotificationId"},
{"file":"server/src/routes/notifications.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/notifications.js","fingerprint":"57c60265b18cb39c11ef9a4e5ef528ea29ffae46c51f52b11e64a44564993d74","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:handleNotificationChannelWriteError"},
{"file":"server/src/routes/notifications.js","fingerprint":"8413330dba32e6adedc5c566d53c40d6e596465bf96637bcd76a272a9940c24f","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:handleNotificationChannelWriteError"},
{"file":"server/src/routes/paymentAccounts.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validationError"},
{"file":"server/src/routes/payroll.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/payroll.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseId"},
{"file":"server/src/routes/payroll.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/payroll.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseQuery"},
{"file":"server/src/routes/payroll.js","fingerprint":"8d8b2a6b503e009a90b28a9371fedbaf81f53d1fd0904330ff1a589b18a15868","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseMoneyList"},
{"file":"server/src/routes/payroll.js","fingerprint":"c2549c5ddc23dc22598ff4d22aa465eedb561a6b130059d44bcf3e279bb18a4b","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseMonthOrThrow"},
{"file":"server/src/routes/payroll.js","fingerprint":"eb4ae752a39b2db5f9a01b4a18a2ef4a033542477df821fde95a073332334a2a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/payroll/items"},
{"file":"server/src/routes/payroll.js","fingerprint":"eb4ae752a39b2db5f9a01b4a18a2ef4a033542477df821fde95a073332334a2a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/payroll/items/:itemId"},
{"file":"server/src/routes/payroll.js","fingerprint":"8f9fcc9a5f96c1d1ddc3792737989e17225d9e62ed663a6323e40c2182878439","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:payrollRoutes"},
{"file":"server/src/routes/payroll.js","fingerprint":"29feee7aa021e87ec66e539ddd587225b345306848abc928b326193854df5bbb","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/payroll/payslips/generate"},
{"file":"server/src/routes/payroll.js","fingerprint":"5cbd8ccfbb4a49806897f695843745f0f6978cf06798722867a1a86f8895ee49","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/payroll/payslips/generate"},
{"file":"server/src/routes/payroll.js","fingerprint":"cd1730346f59b6dd1ef61a96541d6b14fae7427014a4df660622d5db1971e7cb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/payroll/payslips/generate"},
{"file":"server/src/routes/payroll.js","fingerprint":"d504875855dd432ce8fd8e3712b810057bc498752f4f20d7b1ecdb1f0974b458","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/payroll/payslips/generate"},
{"file":"server/src/routes/payroll.js","fingerprint":"2f529b8652d855f222007a9aaf7efda679de466eb009646f68dec1c8af9878d5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/payroll/payslips/:payslipId"},
{"file":"server/src/routes/payroll.js","fingerprint":"8f9fcc9a5f96c1d1ddc3792737989e17225d9e62ed663a6323e40c2182878439","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/payroll/payslips/:payslipId"},
{"file":"server/src/routes/payroll.js","fingerprint":"e5bed6a1ef46e69a472e6a8e0f1637ef20b8a9a7b1b913981c715e6fa0ca53c5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/payroll/payslips/issue"},
{"file":"server/src/routes/payroll.js","fingerprint":"3d6f069f1e14a67b816a3f19cf9547f70835b326a63c8e97a730403c189fb0ff","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/payroll/payslips/issue"},
{"file":"server/src/routes/performance.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/performance.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseId"},
{"file":"server/src/routes/performance.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/performance.js","fingerprint":"99617ed3d2198ffedead181d3d347c8c6b3be4d886c92bcfa7c58f39389a6d57","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateCycleDates"},
{"file":"server/src/routes/performance.js","fingerprint":"64ec8f0736b31d4a76082fa397b9adf9107f0628c97b9547026f55a3629f71cd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/performance.js","fingerprint":"5feffb51751e2ea37516e71a87d53134a4dd4c62f8d1365f85071e9e669af061","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureNoOtherActiveCycle"},
{"file":"server/src/routes/performance.js","fingerprint":"e5be30f4e74036474ea74e5a14b970e139c14c3d245c0db50d115a51aa739287","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/performance/cycles/:cycleId"},
{"file":"server/src/routes/performance.js","fingerprint":"7ecf4a716d093c397f13de0cf527550ee5338bda3bb148ab77927eae1e278ec4","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/performance/cycles/:cycleId"},
{"file":"server/src/routes/performance.js","fingerprint":"ae0076b9e368da3ee5b33929325622ec7245142706ddc9b1b9ffc654bdb039a9","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/performance/cycles/:cycleId"},
{"file":"server/src/routes/performance.js","fingerprint":"f45ef4d4efbe70b8b8d9986fbfa81ab17ec19b4be5d49a05c50e4113870bf090","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/performance/cycles/:cycleId"},
{"file":"server/src/routes/performance.js","fingerprint":"b2271cd4b603af5a201ffeacd3f23408030b50cb56e9f7380996a3a90cfe241d","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:performanceRoutes"},
{"file":"server/src/routes/performance.js","fingerprint":"a36e4a2edab739bdd3595d96a3d8ac1c1e6dd814915bfa5b0a189c17da335abe","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:performanceRoutes"},
{"file":"server/src/routes/portal.js","fingerprint":"de0048472baa2ade9251177868ca94a4e86e149b88bb49f48d8cc7d10549767b","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/portal.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/presence.js","fingerprint":"8082c82f99fcf677784600a080881baa1d83e621985e6c5945f8c90cd05d4297","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseInput"},
{"file":"server/src/routes/presence.js","fingerprint":"2adc3ff7dd515743a159e4289f5872beaed9785c8fb49532b3c37485d6053cf1","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/presence.js","fingerprint":"ab9d9bd7ccc0aad6c6dd652ce7bc50b2c92691ab967336b355c61af3aa1ab0db","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:presenceRoutes"},
{"file":"server/src/routes/projects.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/projects.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/projects.js","fingerprint":"50daa180b4259d12a5b9bfa3bdf8c1dc8bf65b66fe736780985e0f5e0645bd87","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseId"},
{"file":"server/src/routes/projects.js","fingerprint":"786f7b3c31e737656476b5bf3d9ce110029b4d21a45c73d7bd0a9cd2141b77e4","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/projects.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/projects"},
{"file":"server/src/routes/projects.js","fingerprint":"05cb62a133436678bfe2db9f1af0acaf82a799ff8b253bd29820783f91d6f5ae","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/projects/:id/closure-checklist/:itemId"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parsePurchaseRequestId"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"74a25a3be91eff6f6378ec4bb7b73f089e32a91c4b72813dd6d8737f53072a12","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseAttachmentId"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parsePatchBody"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseArchiveBody"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"32c90a20fefee508b70375039699b0944f7f0e7393868e6f484667d827ec59eb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildPrecheckBlockedError"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"0d724434a86f1930647935a2be2f6d0c3cb0f6f972e32a28568a9db808b300de","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:decimalInputToString"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"abfc2c646e9b5a2faa92673d2e933810ba72e4f21f242139cbcb418f504322bd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:decimalInputToString"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"d74cf60fb2fbfb6c420e156df0c230eccd33b37dd768c62e4cddc729d2b8d36e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureLegalEntityAvailable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"c4967a64f972aa0396e182d2d06880ab684c25d2545466aad0854828740ad39e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureLegalEntityAvailable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"1e04b9db6494f88b9adbdfbc2cafcc8970bccbb039c99e67ce3ccbbfc868ace1","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterAvailable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"dc93103cf32df2eb4384550e3ae7ebcc290879e69e11aad7eaea6481f8f414fb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterAvailable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"0bd9235198a75999811434ce3e70e584019d64555ede15c2202bd5f1030edf89","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCostCenterAvailable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"36aa7be0c8e53d562f66944b903fb548652b9a5d882b3ca6bbbd15a71a3d70f8","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureSupplierAvailable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"31b1543cd9b1084ee05a0a1bd62272225e06a4c2ef43dbe002d5923c73fa46ae","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureSupplierAvailable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"5e473411e4ac8c5e89cc4593424ed01cab03c258c8d7ec3254ee4ed3a57ae9d4","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureSupplierAvailable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"1f616caf65363bde2fcebb6b65f22a06972c60903568ff1a1669f3198b66a322","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDocumentCategoryAvailable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"9c0bd30f0d8350f57b3e84625952980393265ba93a5faaab83814a12f8b97009","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDocumentCategoryAvailable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"1d5e08f2e6e5f0de7f26fb28c9a18afcad9bbca93ee7c289de73a80085d2d726","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDocumentCategoryAvailable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"6aeceeea0061576ecdce5bb9ba919e00f69c3ce34c7699223e15a0679d6d646e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureBudgetPoolAvailable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"acc1967cece89a983569e89f0e9a6993c85bcf6b85ab6322b6a8cd603ad29ac3","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureBudgetPoolAvailable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"625cfacc8d63ac5a09613bd85f9ce9bb3a6aef3b1c45c631d48e9c9c5effb54a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureProjectLinkingEnabled"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"e3a1f2bab30aec1c86d5589d1dbdc3350bafc1178f33178f1e33ece354de6567","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureProjectLinkable"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parsePurchaseAttachmentDownloadBody"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parsePurchaseAttachmentDeleteBody"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"09e6ed8568ec152bc9e258a6af9d3087ca261ab91afb35dacc64b39f0d7f2f77","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildAttachmentBlockedError"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"df339a0b9ae2b0568bbc36f464165a9786d9e7b82d19110b5be663c92adf6307","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"d99112098e6e3ce510653305291db41171fc8ed1e720cadfc66fcb32f6608eb6","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/submit"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"020f59053f8ead4a770124fc5ccae72fdd5e7a75fcde95800f1b6348b64ad0ff","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/submit"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"426a08b378106f678291428f006adcf189c14e99dc30a36b13ef515aa2022ed5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/submit"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"af136d0a2f077abe83abf80e534806deca3c33ebd048d5f8bd610662dad2234f","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/submit"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"b045c685e631112457b97aefd603efbe2d2dc7b4ba1f7e40eac3405cefe2adfd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/submit"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"ac621f7cc93e03e3255a541cbf63faea101f6ae1c017c732634c8d64a2d298f7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/submit"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"f79cec61c75f3cc4d3a3cd8fa999f2c9c90a3f056541020a4e3ac5ea4507f151","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/submit"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"c0e7abb9c3ac55801d691f055e8fa5f6f2d94915023a0684d6e9ad1768d85e87","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/submit"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"4fc911e68f5f8710662d80ffbdc0214968e726f4f25c06969c57cb11643d6147","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/submit"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"c6f59dbc2caae66192a4f48e0d9b6071d52c7e004e099b0413b68335b725a254","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/submit"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"f52ae13c892cffc99bb85b3059629d4b449f12f79fe89234a9a4fb4b741428f3","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/withdraw"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"08f8022fc7683b6eb3dbd6b92c8ebcb12cc6679b6ca4414842d575a6a7522d36","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/archive-order"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"df339a0b9ae2b0568bbc36f464165a9786d9e7b82d19110b5be663c92adf6307","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/attachments"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"5c5ab17d2c4d8884cbe06e4f4597264afd5afa49353d4727355711193f6fe26b","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/attachments"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"ed01eee13031abab1b3ab75739b4e3a06f1ffb3d4d6cda0e91a7dcc56b92383f","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/attachments"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"fd1f07cbb59e6103dfac77bafd01b3ed7a0ce88a5b45ea9f3d7238776249d498","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/attachments"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"561527fb2f4fdce80bdcb3c9b02a1afc9c2664dfd9bcb592f7e554b0b447b8fa","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/attachments"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"9eca7119004b10837ed03641c72eae0ec83e46d3e5ca20024bb41b95ce1b6f60","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/attachments"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"2b4bec423c5af9d8e18b6ba3196b28911651662dd0294bd835c51d2685146ba2","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/attachments"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"3efb722614f1cdee938215738adeb5b2d4930fdb24c470e560b69244f0bcae7e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/attachments"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"df339a0b9ae2b0568bbc36f464165a9786d9e7b82d19110b5be663c92adf6307","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/attachments/:attachmentId"},
{"file":"server/src/routes/purchaseRequests.js","fingerprint":"85a20df57650243bd107434cf30f3652f8c466f8040b9446a23bf5999dc24946","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/purchase-requests/:id/attachments/:attachmentId/download"},
{"file":"server/src/routes/reimbursementTemplates.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/reimbursementTemplates.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseTemplateId"},
{"file":"server/src/routes/reimbursementTemplates.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/reimbursementTemplates.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/reimbursementTemplates.js","fingerprint":"01ede183f16765ade77b433028000084374d4f113f6255e49d41f5c3d7f38646","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureExpenseTypesEnabled"},
{"file":"server/src/routes/reimbursementTemplates.js","fingerprint":"c871135b601c023c98f31d5555f893efbefce44a1c0964220df341897674629c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/reimbursement-templates/:id"},
{"file":"server/src/routes/riskFlags.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/riskFlags.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseQuery"},
{"file":"server/src/routes/riskFlags.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseId"},
{"file":"server/src/routes/riskFlags.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/riskFlags.js","fingerprint":"ccadcec1520fec31ce3906b9792f9f33e3633ce278244816db0e0c53a5f847ad","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:riskFlagsRoutes"},
{"file":"server/src/routes/search.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/search.js","fingerprint":"ca31c72fdd0b89c6ce11ac23fe5887903daf75f09eeef0e4af5e1330a9f747f7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/search.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/search"},
{"file":"server/src/routes/systemWorkspace.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/systemWorkspace.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/todos.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/todos.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/todos.js","fingerprint":"6c58fb0c0b2fb699a6a013d4d40a2b87c072068b06e766e74b0e092cc7a335d5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUuid"},
{"file":"server/src/routes/todos.js","fingerprint":"309a572d3996ecefcf2cd76ff4d999f27066193832e45339137025bd0444a326","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/todos.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/todos"},
{"file":"server/src/routes/travelAuthorizations.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/travelAuthorizations.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/travelAuthorizations.js","fingerprint":"ba1f56c27bbfd403f3f2ec44f1cc63120cd8619bb07758590c204ca433ea6195","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/authorizations/import"},
{"file":"server/src/routes/travelAuthorizations.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/authorizations/import"},
{"file":"server/src/routes/travelAuthorizations.js","fingerprint":"c25ad9fd794672ca12ee3f985568d7f60fcd71e6bf4f98397bbf0faa51e57835","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/authorizations/import"},
{"file":"server/src/routes/travelOrders.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/travelOrders.js","fingerprint":"ba1f56c27bbfd403f3f2ec44f1cc63120cd8619bb07758590c204ca433ea6195","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/orders/import"},
{"file":"server/src/routes/travelOrders.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/orders/import"},
{"file":"server/src/routes/travelOrders.js","fingerprint":"401a1e7b894d5bd123a72d89ca48ec5055bf228f8ab41429bc25304b4946b7ef","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/orders/import"},
{"file":"server/src/routes/travelOrders.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel-orders/:id/mark-paid"},
{"file":"server/src/routes/travelOrders.js","fingerprint":"6ed4360839cd3ae903e4ef9b4e339575c30c0f20a7166c1ea449f17d292ff844","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel-orders/:id/mark-paid"},
{"file":"server/src/routes/travelOrders.js","fingerprint":"f868afb0ae5510d8969c9b43b2f628a8af144ad8ff05768844e9139c7c6a5ecd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel-orders/:id/mark-paid"},
{"file":"server/src/routes/travelOrders.js","fingerprint":"1c393ac35b7be8c743b2904d8587c2e6fd564fa21eb31be689718225cb108291","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel-orders/:id/mark-paid"},
{"file":"server/src/routes/travelOrders.js","fingerprint":"c974531164266ba82a417ecc4fb030096b4abeff568aba3b70b613985301e819","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel-orders/:id/mark-paid"},
{"file":"server/src/routes/travelReconciliation.js","fingerprint":"4ce8a4863c616aaef038fd59c2bd34754e1f4a07a817afc733e8a0bd7dd6398e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/travelReconciliation.js","fingerprint":"9752938eeee08ad1684e1f901484bf811d47d2ebffbebd74ec34f73be5de7605","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureValidPeriod"},
{"file":"server/src/routes/travelReconciliation.js","fingerprint":"8506eaeef8adc8cc277fcffb3ce92086e955ecadb4149df7b5a01363da60d337","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateBatchScopeContext"},
{"file":"server/src/routes/travelReconciliation.js","fingerprint":"3cc514556b50dae2ebe09ef19b335d02d760c8375bf6d85dae2b9cce523f1870","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateBatchScopeContext"},
{"file":"server/src/routes/travelReconciliation.js","fingerprint":"4b8d375f7b60f2922bcd07343e3ea3a6e3b80c22cacb7f698540885224a1a259","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateBatchScopeContext"},
{"file":"server/src/routes/travelReconciliation.js","fingerprint":"4ce8a4863c616aaef038fd59c2bd34754e1f4a07a817afc733e8a0bd7dd6398e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/reconciliation/batches"},
{"file":"server/src/routes/travelReconciliation.js","fingerprint":"41b9715742da06f74902865fe5a1d9cf214f8fe8b47345e3aa827a458e3053b6","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/reconciliation/batches"},
{"file":"server/src/routes/travelReconciliation.js","fingerprint":"7754d00c7af4946d48b4e723e1e6165805cacbff52289e221127d905441f2823","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/reconciliation/batches/:batchId"},
{"file":"server/src/routes/travelReconciliation.js","fingerprint":"87cb200635db2286f2444af5847774354326bf8162317cdf6012214505a835ec","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/reconciliation/items/:itemId/confirm"},
{"file":"server/src/routes/travelReconciliation.js","fingerprint":"88ca8fcd24be33d22f8814a459047b1041e99774a9bce0a77991481749ee3fcc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/reconciliation/items/:itemId/confirm"},
{"file":"server/src/routes/travelReconciliation.js","fingerprint":"87cb200635db2286f2444af5847774354326bf8162317cdf6012214505a835ec","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/reconciliation/items/:itemId/reopen"},
{"file":"server/src/routes/travelReconciliation.js","fingerprint":"7754d00c7af4946d48b4e723e1e6165805cacbff52289e221127d905441f2823","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/reconciliation/batches/:batchId/finalize"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseTravelRequestId"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"e6cf16bb46d3d378312e4d0c034325e4c365748b562a58468eb66fcd1f021db0","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseDateTimeValue"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"0d724434a86f1930647935a2be2f6d0c3cb0f6f972e32a28568a9db808b300de","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:decimalInputToString"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"abfc2c646e9b5a2faa92673d2e933810ba72e4f21f242139cbcb418f504322bd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:decimalInputToString"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"6cb2563bca7c74fed4a6c76d781843eae305be58d2d29908ea589906b6bcef83","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:normalizeSegments"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"b1404f502f7dc79f6e49855018d6715052f64db61e4f93376bcde2f8c0934700","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDateRangeValid"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"32c90a20fefee508b70375039699b0944f7f0e7393868e6f484667d827ec59eb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildPrecheckBlockedError"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"646fc9e91a562d0983f9b5e15d73334df2faf1b7760e3c03bae4917cf83292b0","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFinancialDimensions"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"7de438154ec7f3addf2aa384c147760463b5b61a961d3026988b6ff9f71c051d","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFinancialDimensions"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"57ccb19330174bae06b385dca2cb8c41ce487980099e934fef19f8fc3a15cba7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFinancialDimensions"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"92e4ca56a07acf54433ae06c7af4755bd7aa80956a055e41da4388cb5ac61962","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel-requests/:id"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"df339a0b9ae2b0568bbc36f464165a9786d9e7b82d19110b5be663c92adf6307","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel-requests/:id"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"426a08b378106f678291428f006adcf189c14e99dc30a36b13ef515aa2022ed5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel-requests/:id/submit"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"5fd193c785794e7bfeca6cba08789d583371b625c54caad5e6ab70750d0381e8","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel-requests/:id/submit"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"d99112098e6e3ce510653305291db41171fc8ed1e720cadfc66fcb32f6608eb6","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel-requests/:id/submit"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"020f59053f8ead4a770124fc5ccae72fdd5e7a75fcde95800f1b6348b64ad0ff","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel-requests/:id/submit"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"5130090900bde10ab9d5955020cc5c2227dd5680b6036f83e7d14337c8fa5057","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel-requests/:id/link-project"},
{"file":"server/src/routes/travelRequests.js","fingerprint":"f52ae13c892cffc99bb85b3059629d4b449f12f79fe89234a9a4fb4b741428f3","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel-requests/:id/withdraw"},
{"file":"server/src/routes/travelTransactions.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/travelTransactions.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/travelTransactions.js","fingerprint":"ba1f56c27bbfd403f3f2ec44f1cc63120cd8619bb07758590c204ca433ea6195","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/transactions/import"},
{"file":"server/src/routes/travelTransactions.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/transactions/import"},
{"file":"server/src/routes/travelTransactions.js","fingerprint":"9d03fb23e8d0a0b4abb5044684e8f43268dacf0c6a6fb983034511ee56d598a7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/transactions/import"},
{"file":"server/src/routes/travelTransactions.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/transactions/:id/freeze-suggestion"},
{"file":"server/src/routes/travelTransactions.js","fingerprint":"6ed4360839cd3ae903e4ef9b4e339575c30c0f20a7166c1ea449f17d292ff844","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/travel/transactions/:id/freeze-suggestion"},
{"file":"server/src/routes/users.js","fingerprint":"eafa3d7b708b3d3b0052c67493ee3cf3f1ee62121cdbc3612f6d0c16b057eddc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUserId"},
{"file":"server/src/routes/users.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseListQuery"},
{"file":"server/src/routes/users.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateBody"},
{"file":"server/src/routes/users.js","fingerprint":"9629ec80c0f796bc6a00f7dfecb68242c56c3d9ff5fa981740c2afbee0bc5c21","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseCreateBody"},
{"file":"server/src/routes/users.js","fingerprint":"8aa13b8fbc71173fb12f1a2d9f197b2c993f28bd29015eee8d9a06e58b2e2aa7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUpdateBody"},
{"file":"server/src/routes/users.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUpdateBody"},
{"file":"server/src/routes/users.js","fingerprint":"dd4c7435c5433e19eb9893f762c76225772c4cf62eb01c6c45d7b8d1b207705a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseUpdateBody"},
{"file":"server/src/routes/users.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseSensitiveBody"},
{"file":"server/src/routes/users.js","fingerprint":"dd4c7435c5433e19eb9893f762c76225772c4cf62eb01c6c45d7b8d1b207705a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseSensitiveBody"},
{"file":"server/src/routes/users.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/users.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBatchBody"},
{"file":"server/src/routes/users.js","fingerprint":"e60ad7bc69f342e2ea14a2ef0d0debd98d71a4358ad3db7f1273d77e331d0188","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBatchBody"},
{"file":"server/src/routes/users.js","fingerprint":"64ec8f0736b31d4a76082fa397b9adf9107f0628c97b9547026f55a3629f71cd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureLifecycleFeatureEnabled"},
{"file":"server/src/routes/users.js","fingerprint":"ace2fbe941e2d06d8f7aac2467ce8475df71d7c14253228443c68ef83d294799","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDepartmentUsable"},
{"file":"server/src/routes/users.js","fingerprint":"0bd10e8819ebb46f6802f2308035459543c5aa995c12e10b3db34b1c0e4bcae4","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDepartmentUsable"},
{"file":"server/src/routes/users.js","fingerprint":"0369c70693eba9ea471e80c6d9c427eb0aa8818467889d3616cef56a3b6b678f","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureManagerUsable"},
{"file":"server/src/routes/users.js","fingerprint":"ace2fbe941e2d06d8f7aac2467ce8475df71d7c14253228443c68ef83d294799","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureManagerUsable"},
{"file":"server/src/routes/users.js","fingerprint":"8d5fb35f3a4cca58bc32a3966938d9c038cb755122c201e79191f648b3831393","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureManagerUsable"},
{"file":"server/src/routes/users.js","fingerprint":"facb330f41026823036cb195e4e3d9b76a3af9516e9353a9e4bfd944e9de68fd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:generateOneTimePassword"},
{"file":"server/src/routes/users.js","fingerprint":"561527fb2f4fdce80bdcb3c9b02a1afc9c2664dfd9bcb592f7e554b0b447b8fa","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:readUserImportPayload"},
{"file":"server/src/routes/users.js","fingerprint":"1b27bf379328573c70d6d1995fab5ec7ddb58308a9058235fc86451eb943efdb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:readUserImportPayload"},
{"file":"server/src/routes/users.js","fingerprint":"0df105fd6c424e2d4cf00942f34c4cd306bbcb55116207f24d365de76fe02d68","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:readUserImportPayload"},
{"file":"server/src/routes/users.js","fingerprint":"ce0ac252d216b04fd3edb044c4e83b79094808a6950e9443fd55c7231bb53b15","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:readUserImportPayload"},
{"file":"server/src/routes/users.js","fingerprint":"3714c500736e5dc6b230fb71e2895fc7fd653da4134811192579cb2a8f3403cf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:readUserImportPayload"},
{"file":"server/src/routes/users.js","fingerprint":"0c5c7e91b73ae8e0c05e8c3a34dbbdb250559b2f46385fe5c66ed1aff3a02166","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:readUserImportPayload"},
{"file":"server/src/routes/users.js","fingerprint":"6bc44520602f42ec038dcfd6f58b0c2f56dae06e8e26a50870b576066fad1eeb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:readUserImportPayload"},
{"file":"server/src/routes/users.js","fingerprint":"2255ec084503939a7653b2dc67783bca3f1affb9fa99c046d39b0b2d7fb35f98","count":3,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:usersRoutes"},
{"file":"server/src/routes/users.js","fingerprint":"ba37f131e8e7ec10223c5cbf578abe550f01988bd91f594862771bfae88a223c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:usersRoutes"},
{"file":"server/src/routes/users.js","fingerprint":"94d1f0e015e7774bcc0adaa3550ca8bebabea49c52e7ea9679ce22310f6c888d","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:usersRoutes"},
{"file":"server/src/routes/users.js","fingerprint":"f642f8ed261072a289ce343e133ac9b0954ebac4c531e1993209f7ea9876a7a6","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:usersRoutes"},
{"file":"server/src/routes/users.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":3,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:usersRoutes"},
{"file":"server/src/routes/users.js","fingerprint":"c2c42563d5083e1fcffc70061e11f05a5fb622030879e07c9112e128f780f0e4","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:usersRoutes"},
{"file":"server/src/routes/users.js","fingerprint":"8863d485ac227a65846c44668d0c84fb88de50f56f4f921f66a02685297720e7","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:usersRoutes"},
{"file":"server/src/routes/users.js","fingerprint":"a4ed2cb425167eb0564f1d3f38574b1caacfd0fbff3544381dd70f1992bade04","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:usersRoutes"},
{"file":"server/src/routes/users.js","fingerprint":"8c73684345b1f5a10d5cb4f2911293eab416bb4ee406354f3a8c4c0d4caee34a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:usersRoutes"},
{"file":"server/src/routes/wecomAuth.js","fingerprint":"a86ce70414af8f63b8b3ffc060802dcc2540bc915bfa8669802ff91f167a728f","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:normalizeJsSdkPageUrl"},
{"file":"server/src/routes/wecomAuth.js","fingerprint":"55c80c9479f485c70513c36176f96c81b8ee4ebdad4d77bcd5cab252f5370808","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:assertWecomOauthReady"},
{"file":"server/src/routes/wecomAuth.js","fingerprint":"d70cc75afc07320183256873a1bda11d5ec98169057ce9dd8af5ad976c573533","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:getWecomAccessToken"},
{"file":"server/src/routes/wecomAuth.js","fingerprint":"a86ce70414af8f63b8b3ffc060802dcc2540bc915bfa8669802ff91f167a728f","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/wecom/js-sdk-signature"},
{"file":"server/src/routes/wecomAuth.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/wecom/login"},
{"file":"server/src/routes/wecomAuth.js","fingerprint":"2b0b99fd226589d42c63361d123e4721928f1243cb62e242ec6563dd3349e58e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/wecom/login"},
{"file":"server/src/routes/wecomAuth.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/wecom/bind"},
{"file":"server/src/routes/weeklyReports.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/weeklyReports.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/weeklyReports.js","fingerprint":"503ec8814ed94eae417ed7863bf5f57b8f222346eeb202dd07517c08bacef9fc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/weeklyReports.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/weekly-reports/me"},
{"file":"server/src/routes/weeklyReports.js","fingerprint":"c467510c074544d5abb39af6d212600918c78bae7e46bb8c1837b82b1eb16ccb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:weeklyReportsRoutes"},
{"file":"server/src/routes/weeklyReports.js","fingerprint":"d3e6e1b5646c7292abc574c53b3894ca82620c1f2cb70d3c0f968e0bea4764df","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:weeklyReportsRoutes"},
{"file":"server/src/routes/weeklyReports.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/weekly-reports/team"},
{"file":"server/src/routes/workflow.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseRequiredComment"},
{"file":"server/src/routes/workflow.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseTransferBody"},
{"file":"server/src/routes/workflow.js","fingerprint":"25e610a1c5a3132b78c8415455bdb93b3bd9c914260ba91f3a024e3c7daa9baf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseTaskId"},
{"file":"server/src/routes/workflow.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBatchApproveBody"},
{"file":"server/src/routes/workflow.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBatchRejectBody"},
{"file":"server/src/routes/workflow.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseRemindTaskBody"},
{"file":"server/src/routes/workflow.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseTaskSummaryQuery"},
{"file":"server/src/routes/workflow.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseTaskListQuery"},
{"file":"server/src/routes/workflow.js","fingerprint":"4673e0a3f7e965475ce8a75bc2dd25a14481692a28acdad66e22e8f66d5b2910","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseApproverListQuery"},
{"file":"server/src/routes/workflow.js","fingerprint":"62b930e24657a4fa191f8eb1a42f94f756fb2fb753d8107b7fff4f869b963cfc","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensurePendingTask"},
{"file":"server/src/routes/workflow.js","fingerprint":"1fd5b3afeebac7940f58367d8416482429745f220faa980d888d2fb839ab2675","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:loadWorkflowApplicantIds"},
{"file":"server/src/routes/workflow.js","fingerprint":"6ba473aef620919735b77748bb35859436274ea27837e76d065c6dd25bba0c76","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureNoSelfApproval"},
{"file":"server/src/routes/workflow.js","fingerprint":"14397be7e0b880038157ab116206bdb75936bf78db446e399c28f380969107f6","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureTransferTargetEligible"},
{"file":"server/src/routes/workflow.js","fingerprint":"32ceca17a6cc314683a6f2a1c22eab24df1b295620797e7be26259eeb05210f8","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureTransferTargetEligible"},
{"file":"server/src/routes/workflow.js","fingerprint":"b3753b0ea33a451bea3240fa5fb5fbd456cf8a8998c3330f4744ea7cf7d77cb1","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureWorkflowEntityType"},
{"file":"server/src/routes/workflow.js","fingerprint":"4a23ae0c571add78e35c3497b671980724a9f24e196c0ebf52fa037851212237","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureWorkflowReminderAllowed"},
{"file":"server/src/routes/workflow.js","fingerprint":"54e37689f6b2e7a59fdd73829ca7fbf5dc92808e6034d7387eded9bcc279991c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureWorkflowReminderCooldown"},
{"file":"server/src/routes/workflow.js","fingerprint":"21e9a7c8e6efa358a778df97819620a4a2d4959ec8644e049793fb5b0d2e5a69","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:executeApproveWorkflowTask"},
{"file":"server/src/routes/workflow.js","fingerprint":"928259183e400603e036b96ba45612554560c9ba37cf8975a99f040231aa8393","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/workflow/tasks/:taskId/transfer"},
{"file":"server/src/routes/workflow.js","fingerprint":"d92b664e7497a7bf4f353e457cc7484fe9d9dbf3fa8328002a724f18f9f432e6","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/workflow/tasks/:taskId/transfer"},
{"file":"server/src/routes/workflowDefinitions.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/routes/workflowDefinitions.js","fingerprint":"aec1243e92a1893e8584e71f90bbf22620de1e5299a2d6f7a1ce88308b3448ac","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseId"},
{"file":"server/src/routes/workflowDefinitions.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseBody"},
{"file":"server/src/routes/workflowDefinitions.js","fingerprint":"440871aaf1bf287ce45b8aff11b1c04f2f21151f3898d17f7752253ea9debfed","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseEntityType"},
{"file":"server/src/routes/workflowDefinitions.js","fingerprint":"39168f5e747cc5770558e5a046456e986f365b93b4072c93a56dc14761b770dd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureFeatureEnabled"},
{"file":"server/src/routes/workflowDefinitions.js","fingerprint":"2236cc64fcafe74edc1e79d11a6e93efc7379d02257b7d5fc1e454ee5f9bde95","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/workflow-definitions"},
{"file":"server/src/routes/workflowDefinitions.js","fingerprint":"2236cc64fcafe74edc1e79d11a6e93efc7379d02257b7d5fc1e454ee5f9bde95","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/workflow-definitions/:id"},
{"file":"server/src/routes/workflowDefinitions.js","fingerprint":"567632d90a6968fcc24a576a4699526e2b0fbe15ff285494c449014f955cac77","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/workflow-definitions/:id"},
{"file":"server/src/routes/workflowDefinitions.js","fingerprint":"2236cc64fcafe74edc1e79d11a6e93efc7379d02257b7d5fc1e454ee5f9bde95","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/workflow-definitions/:id/publish"},
{"file":"server/src/routes/workflowDefinitions.js","fingerprint":"9e2ed33d6382c6f9d820878049ac1c9d4b11cdecfae6efe6e5a4a4924b7e87b5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/workflow-definitions/:id/publish"},
{"file":"server/src/routes/workflowDefinitions.js","fingerprint":"2236cc64fcafe74edc1e79d11a6e93efc7379d02257b7d5fc1e454ee5f9bde95","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"/workflow-definitions/:id/archive"},
{"file":"server/src/services/ai/suggestions.js","fingerprint":"43c8e843f54d9ce819d8d943da46acf1637e682537b116b3d070d3e67523e256","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildAiDisabledError"},
{"file":"server/src/services/attendance.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildAttendanceValidationError"},
{"file":"server/src/services/attendance.js","fingerprint":"2ee8bb29f955fb3062828a3dcf1e0e9cdf801091629163c8a5e7f500dd8ec64d","count":2,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseTimeToMinutes"},
{"file":"server/src/services/attendance.js","fingerprint":"362096848d0df743442bfb66cdf57a594b74d7ad1d8c49068b4e623fadd51483","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateAttendancePolicyInput"},
{"file":"server/src/services/attendance.js","fingerprint":"57d8963b1bfd4124fc9dd309a0fe0d6fa2286251aa94ba702cde045a4fba70fa","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateAttendancePolicyInput"},
{"file":"server/src/services/attendance.js","fingerprint":"33f1a6a204fc84205fa86787d743080b63ccbe0c1c29700bbb6a6987870fad24","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateAttendancePolicyInput"},
{"file":"server/src/services/attendance.js","fingerprint":"541024df6a9deeb726d27470531467a9244f66d9196f1658867f494c04757798","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:validateAttendancePolicyInput"},
{"file":"server/src/services/attendance.js","fingerprint":"2ce1b8dcfb0321bbef8d1102f6c60cd5dfe1aab6fd4b4ccf7f86e3dedaf4e6ff","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildMonthRange"},
{"file":"server/src/services/attendance.js","fingerprint":"99ebe0d43dca95cb33f043545c5302d439e6822f524e9f2c083f1d9f46feb362","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildMonthRange"},
{"file":"server/src/services/budgetPool.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/services/budgetPool.js","fingerprint":"8ea8274148c5fc65a473197d6a2617bf27971d7d8de4b26f9c3ec4e28bc7879e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:resolveBudgetScope"},
{"file":"server/src/services/budgetPool.js","fingerprint":"15b3efedb6c7af2094e04906a764b073066e16614492a2e8eadb52d4cea4899c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:resolveBudgetScope"},
{"file":"server/src/services/budgetPool.js","fingerprint":"78bb3b7b0cb97341c844a151c521cb679fc0c4115369dffcadcae5f15b48b594","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:resolveBudgetScope"},
{"file":"server/src/services/budgetPool.js","fingerprint":"8ea8274148c5fc65a473197d6a2617bf27971d7d8de4b26f9c3ec4e28bc7879e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:normalizeBudgetContext"},
{"file":"server/src/services/budgetPool.js","fingerprint":"c10503d03230e1ac3fb2c83e632f28ff6054d39cb3522e43daa8929b4085fc62","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:selectBudgetPoolCandidate"},
{"file":"server/src/services/budgetPool.js","fingerprint":"b9800e3b093f9036da6d9eb06e688b99636297e64ae73052c309b2724c24fd22","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:resolveApplicableBudgetPoolByContext"},
{"file":"server/src/services/contractRegistryFilters.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildContractRegistryValidationError"},
{"file":"server/src/services/contractRegistryFilters.js","fingerprint":"0d7d8d325c8629980a1d6f1c569255c2995059d5b19667be27f57eed2ea837af","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseContractRegistryFilters"},
{"file":"server/src/services/contractRegistryFilters.js","fingerprint":"0157cfe6b95bfb9824b92b3187d51e79af6759e2082f6f829c29ec79dc41eed1","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseContractRegistryFilters"},
{"file":"server/src/services/contractRegistryFilters.js","fingerprint":"0d7d8d325c8629980a1d6f1c569255c2995059d5b19667be27f57eed2ea837af","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseContractRegistryExpiringQuery"},
{"file":"server/src/services/cutover.js","fingerprint":"547aac9dca5ebf4d86d1488f348d7cc03c32103a801898e4c8acc9866dd47da7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureCutoverAllowed"},
{"file":"server/src/services/documentSequence.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/services/documentSequence.js","fingerprint":"b929462215d217481e0021dbc6414f9dd0d13a7ff37a3485e70e251a34649f96","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:normalizeDocumentType"},
{"file":"server/src/services/documentSequence.js","fingerprint":"52251a414cebd986f1a66ada25aa84ad8201ef118fffda7cb25a4b0c5eb7b3c6","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:findDocumentCategory"},
{"file":"server/src/services/documentSequence.js","fingerprint":"17742cab588bf1fa29dcb863cac76aa8e313cc9fbe831e384e6076865c9f4c92","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:findDocumentCategory"},
{"file":"server/src/services/documentSequence.js","fingerprint":"218b770702cc4205ce09e22bf23938845fbe17e9677d717994df7ef74cf7c4c9","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:findDocumentCategory"},
{"file":"server/src/services/employeeDirectoryFilters.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/services/employeeDirectoryFilters.js","fingerprint":"278003b27036a26a6c6c2bac8e175081f3d2f7f950a5d3c2ed2c8e71f604bfdd","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseEmployeeDirectoryFilters"},
{"file":"server/src/services/employeeDirectoryFilters.js","fingerprint":"c7d5cb33dcb5fc3619be1f8aa436be5129922d43e88e5723b1077127d3980e9c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseEmployeeDirectoryFilters"},
{"file":"server/src/services/feeControl.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/services/feeControl.js","fingerprint":"82e83bd1db5eb446f90f9a600d75e13a0f9d71bd48454bb770ab8a1055fa4a96","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:loadPrecheckEntityContext"},
{"file":"server/src/services/feeControl.js","fingerprint":"82e83bd1db5eb446f90f9a600d75e13a0f9d71bd48454bb770ab8a1055fa4a96","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildEntityPrecheck"},
{"file":"server/src/services/presenceHistory.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildPresenceValidationError"},
{"file":"server/src/services/presenceHistory.js","fingerprint":"819ebeb74216f9c73d4d3677beea18e8919b8d4803a7f52d7d0b01c2dfdbe1cb","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:resolvePresenceHistoryRange"},
{"file":"server/src/services/presenceHistory.js","fingerprint":"903f7ff892fca342b08ad67d8edd56defbb3981edc8974890b0151a0fe53bee0","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:resolvePresenceHistoryRange"},
{"file":"server/src/services/purchaseBudget.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildPurchaseBudgetValidationError"},
{"file":"server/src/services/purchaseBudget.js","fingerprint":"e4d46dd9d7b30343b07d84605262f5a96f639d93f3362edbc9eb0f4448ff0d0b","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:reserveBudgetForEntity"},
{"file":"server/src/services/purchaseBudget.js","fingerprint":"5d79c097496ad908123fee937c35dbd8ddcb6d0f79bfb3dc697bff0cee946c0c","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:reserveBudgetForEntity"},
{"file":"server/src/services/travelPayments.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildTravelPaymentValidationError"},
{"file":"server/src/services/travelPayments.js","fingerprint":"70cfb750ebbaa9e398ab6a8ab743ea042422ada7a3ee87be85ab9314c5ce6f09","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:applyBudgetPoolUsageDelta"},
{"file":"server/src/services/travelReconciliation.js","fingerprint":"1ccd0d7139b90bcb4360218527df65fcbec8936865ee49a7abf1f8eb175deabf","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildValidationError"},
{"file":"server/src/services/travelReconciliation.js","fingerprint":"387721ed02fa83f31bd471035145206c36af7484532afe5353955e667dfc89d1","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureDraftBatch"},
{"file":"server/src/services/travelReconciliation.js","fingerprint":"17733bab2d92da224cf69fdedb3ee146cbf121eef58e881794e407a51b058f68","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensurePendingReconciliationItem"},
{"file":"server/src/services/travelReconciliation.js","fingerprint":"0443a8ffa4324e1e4e08fa89037c0b28b24213ab09941a8ceff40126a457ea1e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureConfirmedReconciliationItem"},
{"file":"server/src/services/travelReconciliation.js","fingerprint":"bbe9cd46973ca0c77ae1763e6adba68e26441e9584168373a97324864b6697ea","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureReopenableReconciliationItem"},
{"file":"server/src/services/travelReconciliation.js","fingerprint":"3ac5e1d7ea04918b94a7e5083b0a6c4ef002a6fe68b1b168d6be7a58125ed0a8","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:ensureBatchFinalizable"},
{"file":"server/src/utils/workflow.js","fingerprint":"04bd03a6f4a2518dcecf7d1702e4695d0104b403a6b929a1147a17e9a32ba6a5","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:parseWorkflowSteps"},
{"file":"server/src/utils/workflow.js","fingerprint":"0cd1c6b1f3fa1355f63b8def66e42f047c1ac5bacbdea9a4a8258212cd401648","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:assertBusinessApprovalRoleCode"},
{"file":"server/src/utils/workflow.js","fingerprint":"207961eec0e9985eb51a9108be430c27f2365413a4cefe00cdec2d58df4e228a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildWorkflowStepsFromApprovalRule"},
{"file":"server/src/utils/workflow.js","fingerprint":"7ba594f9bf352b163a734e664825e1e964ed4491d0120eef78a9fc3aa8b43a07","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildWorkflowStepsFromApprovalRule"},
{"file":"server/src/utils/workflow.js","fingerprint":"b8698fe0fd39540fb0a1d21bf610947927564bdf7150e35b98ee0077317cc84b","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:assertOwnerSelfFallbackRole"},
{"file":"server/src/utils/workflow.js","fingerprint":"ca50ced3104a846b28238b26edb8fcb7af6d10be5fa07ff92438f33b65b659aa","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:assertOwnerSelfFallbackRole"},
{"file":"server/src/utils/workflow.js","fingerprint":"d08fdd02992c489e39264bf0b214f52d0bd1ce5a1c58b6e5342d5bd4347a7057","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildApprovalWorkflowSnapshot"},
{"file":"server/src/utils/workflow.js","fingerprint":"d8fafe98ccaff8cefb614917926b0bc1f90244ac555e5a8eb18d0d32ee5a1d72","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:buildApprovalWorkflowSnapshot"},
{"file":"server/src/utils/workflow.js","fingerprint":"f5c8f2ab31ac7144fd05d20bdcc8db4cf6d92a7fb6e8405326dc77f9e68e2b84","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:resolveAssigneeFromRule"},
{"file":"server/src/utils/workflow.js","fingerprint":"feb0686cc3f1b63906adb701af0d69479951f3a81172d7b2c0414e5b1b6b7ce2","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:resolveAssigneeFromRule"},
{"file":"server/src/utils/workflow.js","fingerprint":"5206020abf57bcd7c7ba728ca164031668424f199e062694d69a57cfdaf5980e","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:resolveAssigneeFromRule"},
{"file":"server/src/utils/workflow.js","fingerprint":"806e44515d6d7eb6dc41a7fa24363b1fb61a6ab6a28dd1168b2d855ca8e0b994","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:resolveAssigneeFromRule"},
{"file":"server/src/utils/workflow.js","fingerprint":"ce63d28720f7d951355348d5ab600ff5009a35ed855613caf5706f9fb007a2d7","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:resolveAssigneeFromRule"},
{"file":"server/src/utils/workflow.js","fingerprint":"b312a21b28c9dbe1efc4bd7f5fd029b62a2c0cceedf88f4e621a84f948896e8a","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:resolveActionableStep"},
{"file":"server/src/utils/workflow.js","fingerprint":"7e3347da9ac0499d2aa163fe5f743ed01b3abeb75a3267626b76e4a566d246de","count":1,"owner":"shunli","reason":"非首批迁移域;修改此调用时须完成原因码注册、客户端映射和具体内容断言","expiresOn":"2026-10-05","operation":"function:resolveActionableStep"}
]
}
+2
查看文件
@@ -89,6 +89,8 @@ stage_project_wide_static() {
local issue_csv="$ROOT_DIR/issues/2026-05-08_23-38-37-project-wide-qa-second-pass.csv"
python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py "$issue_csv"
node "$ROOT_DIR/scripts/qa/check-ui-design-rules.mjs" --strict
node "$ROOT_DIR/scripts/qa/check-error-reason-contract.mjs" --strict
node "$ROOT_DIR/scripts/qa/check-error-reason-contract.mjs" --self-test
node "$ROOT_DIR/scripts/qa/check-mobile-route-contract.mjs"
node "$ROOT_DIR/scripts/qa/check-flow-coverage.mjs" --json > "$ARTIFACT_ROOT/flow-coverage.json"
node "$ROOT_DIR/scripts/qa/check-flow-coverage.mjs" --markdown "$ARTIFACT_ROOT/flow-coverage.md"
+155
查看文件
@@ -0,0 +1,155 @@
const ERROR_REASONS = [
{"code":"reimbursement.material_preview_limit","domain":"reimbursement","audience":"user","defaultMessage":"报销材料较多,暂时无法连续预览,请分批查看材料或联系管理员协助。","recovery":"view_individually"},
{"code":"purchase_attachment.unsupported_type","domain":"purchase_attachment","audience":"user","defaultMessage":"仅支持 PDF、JPG、PNG、WEBP 文件,请重新选择。","recovery":"choose_file"},
{"code":"reimbursement.invoice_amount_required","domain":"reimbursement","audience":"user","defaultMessage":"票据金额尚未确认,无法自动生成明细,请先核对票据金额或手动选择明细。","recovery":"follow_guidance"},
{"code":"reimbursement.expense_type_required","domain":"reimbursement","audience":"user","defaultMessage":"当前没有可用费用类型,请联系管理员维护后再添加票据。","recovery":"follow_guidance"},
{"code":"reimbursement.invoice_not_bound","domain":"reimbursement","audience":"user","defaultMessage":"该票据已不再关联业务单据,请刷新票据列表后确认。","recovery":"follow_guidance"},
{"code":"reimbursement.invoice_unbind_status_invalid","domain":"reimbursement","audience":"user","defaultMessage":"报销单当前不能移出票据,请先核实审批和付款状态。","recovery":"follow_guidance"},
{"code":"reimbursement.invoice_bind_status_invalid","domain":"reimbursement","audience":"user","defaultMessage":"报销单当前不能添加票据,请刷新并确认单据状态。","recovery":"follow_guidance"},
{"code":"reimbursement.invoice_owner_mismatch","domain":"reimbursement","audience":"user","defaultMessage":"票据所属人与报销人不一致,请选择本人对应的票据。","recovery":"follow_guidance"},
{"code":"reimbursement.invoice_item_required","domain":"reimbursement","audience":"user","defaultMessage":"请为该票据选择对应的报销明细后再添加。","recovery":"follow_guidance"},
{"code":"reimbursement.travel_invoice_bind_status_invalid","domain":"reimbursement","audience":"user","defaultMessage":"差旅订单当前不支持添加票据,请核实订单状态。","recovery":"follow_guidance"},
{"code":"invoice_verification.too_frequent","domain":"invoice_verification","audience":"user","defaultMessage":"验真触发过于频繁,请稍后重试。","recovery":"follow_guidance"},
{"code":"invoice_verification.attempts_exceeded","domain":"invoice_verification","audience":"user","defaultMessage":"验真重试次数已达上限,请联系财务人工核验。","recovery":"follow_guidance"},
{"code":"reimbursement.invalid_input","domain":"reimbursement","audience":"user","defaultMessage":"报销内容不完整或格式不正确,请核对页面必填项和日期后重试。","recovery":"follow_guidance"},
{"code":"reimbursement.invalid_uuid","domain":"reimbursement","audience":"user","defaultMessage":"记录标识已失效,请刷新页面后重新选择。","recovery":"follow_guidance"},
{"code":"reimbursement.body_empty","domain":"reimbursement","audience":"user","defaultMessage":"尚未填写修改内容,请补充后再保存。","recovery":"follow_guidance"},
{"code":"reimbursement.date_to_must_be_after_date_from","domain":"reimbursement","audience":"user","defaultMessage":"结束日期不能早于开始日期,请调整查询日期范围。","recovery":"follow_guidance"},
{"code":"reimbursement.amount_must_be_positive","domain":"reimbursement","audience":"user","defaultMessage":"明细金额必须大于 0。","recovery":"follow_guidance"},
{"code":"reimbursement.template","domain":"reimbursement","audience":"user","defaultMessage":"模板中的明细格式已不适用,请联系管理员修正模板后重试。","recovery":"follow_guidance"},
{"code":"reimbursement.owner_employee_id_invalid_owner","domain":"reimbursement","audience":"user","defaultMessage":"所选报销人不可用,请重新选择有效员工。","recovery":"follow_guidance"},
{"code":"reimbursement.total_amount_invalid","domain":"reimbursement","audience":"user","defaultMessage":"报销合计金额不正确,请核对各项明细金额。","recovery":"follow_guidance"},
{"code":"reimbursement.total_amount_negative_not_allowed","domain":"reimbursement","audience":"user","defaultMessage":"报销合计不能为负数,请核对各项明细金额。","recovery":"follow_guidance"},
{"code":"reimbursement.workflow_missing_approval_rule","domain":"reimbursement","audience":"user","defaultMessage":"尚未配置适用的报销审批规则,请联系管理员维护后重试。","recovery":"follow_guidance"},
{"code":"reimbursement.precheck","domain":"reimbursement","audience":"user","defaultMessage":"报销提交检查未通过,请按页面检查清单逐项处理后重试。","recovery":"follow_guidance"},
{"code":"reimbursement.template_id_invalid_template","domain":"reimbursement","audience":"user","defaultMessage":"所选报销模板已失效,请重新选择有效模板。","recovery":"follow_guidance"},
{"code":"reimbursement.template_id_contains_disabled_expense_type","domain":"reimbursement","audience":"user","defaultMessage":"模板包含已停用的费用类型,请联系管理员修正模板后重试。","recovery":"follow_guidance"},
{"code":"reimbursement.linked_travel_request_id_not_found","domain":"reimbursement","audience":"user","defaultMessage":"所选出差申请已不存在,请刷新后重新选择。","recovery":"follow_guidance"},
{"code":"reimbursement.linked_travel_request_id_not_approved","domain":"reimbursement","audience":"user","defaultMessage":"所选出差申请尚未获批,请先完成出差审批。","recovery":"follow_guidance"},
{"code":"reimbursement.linked_travel_request_id_owner_mismatch","domain":"reimbursement","audience":"user","defaultMessage":"出差申请与报销人不一致,请选择本人对应的出差申请。","recovery":"follow_guidance"},
{"code":"reimbursement.linked_travel_request_id_already_linked","domain":"reimbursement","audience":"user","defaultMessage":"该出差申请已关联其他报销单,请核对已有报销记录。","recovery":"follow_guidance"},
{"code":"reimbursement.linked_travel_request_id_missing_expense_type_mapping","domain":"reimbursement","audience":"user","defaultMessage":"该出差项目尚未配置对应费用类型,请联系管理员维护后重试。","recovery":"follow_guidance"},
{"code":"reimbursement.status_invalid_edit_status","domain":"reimbursement","audience":"user","defaultMessage":"当前报销单状态不允许编辑,请刷新并确认审批进度。","recovery":"follow_guidance"},
{"code":"reimbursement.travel_end_date_invalid_date_range","domain":"reimbursement","audience":"user","defaultMessage":"出差结束日期不能早于开始日期,请调整日期。","recovery":"follow_guidance"},
{"code":"reimbursement.status_invalid_delete_status","domain":"reimbursement","audience":"user","defaultMessage":"当前报销单状态不允许删除,请核实审批和付款进度。","recovery":"follow_guidance"},
{"code":"reimbursement.status_invalid_attachment_edit_status","domain":"reimbursement","audience":"user","defaultMessage":"当前报销单不能修改附件,请刷新并确认单据状态。","recovery":"follow_guidance"},
{"code":"reimbursement.file_unsupported_mime_type","domain":"reimbursement","audience":"user","defaultMessage":"附件类型不支持,请选择允许的图片或文档格式。","recovery":"follow_guidance"},
{"code":"reimbursement.file_file_too_large","domain":"reimbursement","audience":"user","defaultMessage":"附件超过大小限制,请压缩或更换文件后重试。","recovery":"follow_guidance"},
{"code":"reimbursement.file_empty_file","domain":"reimbursement","audience":"user","defaultMessage":"所选附件为空,请重新选择有效文件。","recovery":"follow_guidance"},
{"code":"reimbursement.file_too_many_files","domain":"reimbursement","audience":"user","defaultMessage":"一次最多上传 5 个附件,请减少数量后重试。","recovery":"follow_guidance"},
{"code":"reimbursement.file_missing","domain":"reimbursement","audience":"user","defaultMessage":"请先选择需要上传的附件。","recovery":"follow_guidance"},
{"code":"reimbursement.reason_required","domain":"reimbursement","audience":"user","defaultMessage":"读取该材料需要填写用途,请补充访问原因。","recovery":"follow_guidance"},
{"code":"reimbursement.status_invalid_item_edit_status","domain":"reimbursement","audience":"user","defaultMessage":"当前报销单不能修改明细,请刷新并确认单据状态。","recovery":"follow_guidance"},
{"code":"reimbursement.expense_type_id_invalid_or_disabled","domain":"reimbursement","audience":"user","defaultMessage":"所选费用类型已失效或停用,请重新选择。","recovery":"follow_guidance"},
{"code":"reimbursement.reimbursement_invalid_status_for_invoice_item_link","domain":"reimbursement","audience":"user","defaultMessage":"当前报销单状态已变化,不能再关联发票,请刷新页面。","recovery":"follow_guidance"},
{"code":"reimbursement.invoice_invalid_unlinked_item_state","domain":"reimbursement","audience":"user","defaultMessage":"这张发票的关联状态已变化,请刷新后重试。","recovery":"follow_guidance"},
{"code":"reimbursement.linked_item_id_item_not_in_reimbursement","domain":"reimbursement","audience":"user","defaultMessage":"所选明细已不存在或不属于本单,请刷新后重新选择。","recovery":"follow_guidance"},
{"code":"reimbursement.status_invalid_submit_status","domain":"reimbursement","audience":"user","defaultMessage":"当前报销单状态不允许提交,请刷新并确认是否已提交。","recovery":"follow_guidance"},
{"code":"reimbursement.items_at_least_one_required","domain":"reimbursement","audience":"user","defaultMessage":"请先添加至少 1 条报销明细。","recovery":"follow_guidance"},
{"code":"reimbursement.items_positive_amount_required","domain":"reimbursement","audience":"user","defaultMessage":"所有报销明细金额都必须大于 0,请修改后再提交。","recovery":"follow_guidance"},
{"code":"reimbursement.items_invoice_item_guard_failed","domain":"reimbursement","audience":"user","defaultMessage":"票据检查未通过,请核对每条明细与发票的关联和金额后重试。","recovery":"follow_guidance"},
{"code":"reimbursement.travel_summary_incomplete","domain":"reimbursement","audience":"user","defaultMessage":"出差汇总信息尚未填写完整,请补充页面提示的出差信息后提交。","recovery":"follow_guidance"},
{"code":"reimbursement.submit_already_in_review","domain":"reimbursement","audience":"user","defaultMessage":"该报销单已经在审批中,请勿重复提交,可查看审批进度。","recovery":"follow_guidance"},
{"code":"reimbursement.invoices_bound_invoice_required","domain":"reimbursement","audience":"user","defaultMessage":"报销单需要有效发票,请先添加并关联发票后提交。","recovery":"follow_guidance"},
{"code":"reimbursement.budget_exceeded_block_mode","domain":"reimbursement","audience":"user","defaultMessage":"本次报销超出可用预算,请调整金额或联系预算管理员处理。","recovery":"follow_guidance"},
{"code":"reimbursement.workflow_missing_definition","domain":"reimbursement","audience":"user","defaultMessage":"尚未配置可用报销审批流程,请联系管理员维护后重试。","recovery":"follow_guidance"},
{"code":"reimbursement.workflow_no_actionable_approver","domain":"reimbursement","audience":"user","defaultMessage":"当前没有可处理该报销的审批人,请联系管理员核实审批配置。","recovery":"follow_guidance"},
{"code":"reimbursement.status_invalid_withdraw_status","domain":"reimbursement","audience":"user","defaultMessage":"当前报销单不能撤回,请刷新并核实审批进度。","recovery":"follow_guidance"},
{"code":"reimbursement.withdraw_no_pending_instance","domain":"reimbursement","audience":"user","defaultMessage":"当前报销单没有可撤回的审批,请刷新并核实进度。","recovery":"follow_guidance"},
{"code":"reimbursement.withdraw_already_processed_by_approver","domain":"reimbursement","audience":"user","defaultMessage":"审批人已处理该报销单,不能撤回,请联系审批人确认后续处理。","recovery":"follow_guidance"},
{"code":"reimbursement.feature_flag_project_linking_v1_disabled","domain":"reimbursement","audience":"user","defaultMessage":"项目关联暂未开放,请联系管理员确认处理方式。","recovery":"follow_guidance"},
{"code":"reimbursement.reimbursement_invalid_status_for_project_link","domain":"reimbursement","audience":"user","defaultMessage":"当前报销单不能修改关联项目,请刷新并确认单据状态。","recovery":"follow_guidance"},
{"code":"reimbursement.payment_payment_batch_v1_required","domain":"reimbursement","audience":"user","defaultMessage":"该报销单须通过付款批次安排付款,请前往付款批次查看或创建付款安排。","recovery":"follow_guidance"},
{"code":"reimbursement.status_invalid_pay_status","domain":"reimbursement","audience":"user","defaultMessage":"当前报销单尚不满足付款条件,请核实审批和已有付款记录。","recovery":"follow_guidance"},
{"code":"reimbursement.payment_already_paid_conflict","domain":"reimbursement","audience":"user","defaultMessage":"该报销单已有不同的付款记录,请核实原付款,勿重复登记。","recovery":"follow_guidance"},
{"code":"payment_batch.invalid_identifier","domain":"payment_batch","audience":"user","defaultMessage":"付款记录标识已失效,请刷新后重新选择。","recovery":"follow_guidance"},
{"code":"payment_batch.invalid_input","domain":"payment_batch","audience":"user","defaultMessage":"付款操作内容不完整或格式不正确,请核对页面必填项后重试。","recovery":"follow_guidance"},
{"code":"payment_batch.bank_receipt_required","domain":"payment_batch","audience":"user","defaultMessage":"银行付款登记前必须上传有效银行回单,请先补充回单。","recovery":"follow_guidance"},
{"code":"payment_batch.department_unavailable","domain":"payment_batch","audience":"user","defaultMessage":"当前单据所属部门暂不能使用付款批次,请联系管理员确认处理方式。","recovery":"follow_guidance"},
{"code":"payment_batch.source_not_found","domain":"payment_batch","audience":"user","defaultMessage":"部分来源单据已不存在或不可用,请刷新后重新选择。","recovery":"follow_guidance"},
{"code":"payment_batch.daily_sequence_exhausted","domain":"payment_batch","audience":"user","defaultMessage":"今日付款批次编号已用完,请联系管理员处理,勿重复提交。","recovery":"follow_guidance"},
{"code":"payment_batch.payee_account_unavailable","domain":"payment_batch","audience":"user","defaultMessage":"收款账户资料暂不可用,请联系管理员检查后重试。","recovery":"follow_guidance"},
{"code":"payment_batch.source_status_invalid","domain":"payment_batch","audience":"user","defaultMessage":"来源单据尚未满足付款条件或已经付款,请刷新并核实审批与付款状态。","recovery":"follow_guidance"},
{"code":"payment_batch.already_paid_conflict","domain":"payment_batch","audience":"user","defaultMessage":"来源单据已有不同的付款记录,请核实已有记录,勿重复付款。","recovery":"follow_guidance"},
{"code":"payment_batch.source_amount_changed","domain":"payment_batch","audience":"user","defaultMessage":"来源单据金额已变化,请核对单据后重新安排付款。","recovery":"follow_guidance"},
{"code":"payment_batch.settled_transaction_required","domain":"payment_batch","audience":"user","defaultMessage":"尚未找到已结算的交易,请确认结算记录后再登记付款。","recovery":"follow_guidance"},
{"code":"payment_batch.receipt_security_failed","domain":"payment_batch","audience":"user","defaultMessage":"付款回单未通过安全检查,请联系管理员核实并重新上传有效回单。","recovery":"follow_guidance"},
{"code":"payment_batch.receipt_field_invalid","domain":"payment_batch","audience":"user","defaultMessage":"请通过回单上传入口重新选择文件。","recovery":"follow_guidance"},
{"code":"payment_batch.receipt_too_large","domain":"payment_batch","audience":"user","defaultMessage":"回单文件超过大小限制,请压缩后重试。","recovery":"follow_guidance"},
{"code":"payment_batch.receipt_too_many","domain":"payment_batch","audience":"user","defaultMessage":"上传回单数量超过限制,请减少文件数量后重试。","recovery":"follow_guidance"},
{"code":"payment_batch.receipt_upload_limit","domain":"payment_batch","audience":"user","defaultMessage":"回单上传超过请求限制,请减少文件数量或大小后重试。","recovery":"follow_guidance"},
{"code":"payment_batch.receipt_file_invalid","domain":"payment_batch","audience":"user","defaultMessage":"回单文件不符合上传要求,请核对格式、大小和文件内容后重新上传。","recovery":"follow_guidance"},
{"code":"payment_batch.receipt_required","domain":"payment_batch","audience":"user","defaultMessage":"请先选择有效的付款回单文件。","recovery":"follow_guidance"},
{"code":"payment_batch.travel_payment_frozen","domain":"payment_batch","audience":"user","defaultMessage":"差旅付款组批已暂停,请联系财务确认处理方式。","recovery":"follow_guidance"},
{"code":"payment_batch.account_reveal_unavailable","domain":"payment_batch","audience":"user","defaultMessage":"当前批次不支持查看收款账户,请核实批次类型和状态。","recovery":"follow_guidance"},
{"code":"payment_batch.payee_snapshot_missing","domain":"payment_batch","audience":"user","defaultMessage":"历史批次缺少收款资料快照,请联系管理员核实,不要据此重复付款。","recovery":"follow_guidance"},
{"code":"payment_batch.receipt_not_editable","domain":"payment_batch","audience":"user","defaultMessage":"当前付款状态不允许修改回单,请刷新并确认批次进度。","recovery":"follow_guidance"},
{"code":"payment_batch.receipt_voided","domain":"payment_batch","audience":"user","defaultMessage":"该回单已作废,请选择有效回单。","recovery":"follow_guidance"},
{"code":"payment_batch.download_reason_required","domain":"payment_batch","audience":"user","defaultMessage":"下载该回单需要填写用途,请补充下载原因。","recovery":"follow_guidance"},
{"code":"payment_batch.receipt_void_required","domain":"payment_batch","audience":"user","defaultMessage":"回单不能直接删除,请使用作废操作并说明原因。","recovery":"follow_guidance"},
{"code":"payment_batch.receipt_already_voided","domain":"payment_batch","audience":"user","defaultMessage":"该回单已经作废,请刷新列表,勿重复操作。","recovery":"follow_guidance"},
{"code":"payment_batch.metadata_not_editable","domain":"payment_batch","audience":"user","defaultMessage":"当前批次状态不允许修改付款信息,请刷新并核实进度。","recovery":"follow_guidance"},
{"code":"payment_batch.item_not_editable","domain":"payment_batch","audience":"user","defaultMessage":"只能修改尚未完成的付款明细,请核实该明细状态。","recovery":"follow_guidance"},
{"code":"payment_batch.batch_not_reviewable","domain":"payment_batch","audience":"user","defaultMessage":"当前批次状态不允许复核,请刷新并确认是否已处理。","recovery":"follow_guidance"},
{"code":"payment_batch.items_not_pending","domain":"payment_batch","audience":"user","defaultMessage":"部分明细已处理,不能复核该批次,请刷新并核实明细状态。","recovery":"follow_guidance"},
{"code":"payment_batch.maker_required","domain":"payment_batch","audience":"user","defaultMessage":"该批次缺少制单人信息,请联系管理员核实后再复核。","recovery":"follow_guidance"},
{"code":"payment_batch.maker_checker_conflict","domain":"payment_batch","audience":"user","defaultMessage":"制单人不能复核自己的付款批次,请由另一位有权限的经理或负责人复核。","recovery":"follow_guidance"},
{"code":"payment_batch.risk_resolution_required","domain":"payment_batch","audience":"user","defaultMessage":"还有未处理的风险提示,请逐项确认并处理后再复核。","recovery":"follow_guidance"},
{"code":"payment_batch.batch_not_executable","domain":"payment_batch","audience":"user","defaultMessage":"只有已复核或待重试的批次才能登记付款,请先确认批次状态。","recovery":"follow_guidance"},
{"code":"payment_batch.review_required","domain":"payment_batch","audience":"user","defaultMessage":"该批次尚未完成有效复核,请先由独立复核人完成复核。","recovery":"follow_guidance"},
{"code":"payment_batch.batch_not_cancellable","domain":"payment_batch","audience":"user","defaultMessage":"该批次当前不能取消,请核实是否已有付款结果并联系财务处理。","recovery":"follow_guidance"},
{"code":"leave.invalid_uuid","domain":"leave","audience":"user","defaultMessage":"记录标识已失效,请刷新页面后重新选择。","recovery":"follow_guidance"},
{"code":"leave.invalid_input","domain":"leave","audience":"user","defaultMessage":"提交内容不完整或格式不正确,请核对必填项和日期后重试。","recovery":"follow_guidance"},
{"code":"leave.date_from_invalid","domain":"leave","audience":"user","defaultMessage":"开始日期不正确,请重新选择。","recovery":"follow_guidance"},
{"code":"leave.date_to_invalid","domain":"leave","audience":"user","defaultMessage":"结束日期不正确,请重新选择。","recovery":"follow_guidance"},
{"code":"leave.date_to_must_be_after_date_from","domain":"leave","audience":"user","defaultMessage":"结束日期不能早于开始日期,请调整日期范围。","recovery":"follow_guidance"},
{"code":"leave.department_id_scope_all_only","domain":"leave","audience":"user","defaultMessage":"当前查询范围不支持指定部门,请调整查询范围。","recovery":"follow_guidance"},
{"code":"leave.date_invalid","domain":"leave","audience":"user","defaultMessage":"日期格式不正确,请重新选择日期。","recovery":"follow_guidance"},
{"code":"leave.file_only_single_file_supported","domain":"leave","audience":"user","defaultMessage":"每次只能上传一个证明文件,请重新选择。","recovery":"follow_guidance"},
{"code":"leave.file_file_too_large","domain":"leave","audience":"user","defaultMessage":"文件超过上传大小限制,请压缩或更换文件后重试。","recovery":"follow_guidance"},
{"code":"leave.leave_type_id_missing_active_policy","domain":"leave","audience":"user","defaultMessage":"当前假种尚未配置有效策略,请联系人事维护后重试。","recovery":"follow_guidance"},
{"code":"leave.file_required","domain":"leave","audience":"user","defaultMessage":"该假种需要证明材料,请上传后再提交。","recovery":"follow_guidance"},
{"code":"leave.file_unsupported_mime_type","domain":"leave","audience":"user","defaultMessage":"文件类型不支持,请选择允许的证明材料格式。","recovery":"follow_guidance"},
{"code":"leave.status_invalid_edit_status","domain":"leave","audience":"user","defaultMessage":"当前单据状态不允许编辑,请刷新并确认审批进度。","recovery":"follow_guidance"},
{"code":"leave.status_invalid_submit_status","domain":"leave","audience":"user","defaultMessage":"当前单据状态不允许提交,请刷新并确认是否已提交。","recovery":"follow_guidance"},
{"code":"leave.submit_already_in_review","domain":"leave","audience":"user","defaultMessage":"该申请已经在审批中,请勿重复提交,可查看审批进度。","recovery":"follow_guidance"},
{"code":"leave.leave_conflict_range","domain":"leave","audience":"user","defaultMessage":"该时段已有请假申请,请调整日期或先处理已有申请。","recovery":"follow_guidance"},
{"code":"leave.leave_insufficient_balance","domain":"leave","audience":"user","defaultMessage":"可用假期余额不足,请调整天数或联系人事核实。","recovery":"follow_guidance"},
{"code":"leave.workflow_missing_definition","domain":"leave","audience":"user","defaultMessage":"尚未配置可用审批流程,请联系管理员维护后重试。","recovery":"follow_guidance"},
{"code":"leave.workflow_no_actionable_approver","domain":"leave","audience":"user","defaultMessage":"当前没有可处理该申请的审批人,请联系管理员核实审批配置。","recovery":"follow_guidance"},
{"code":"leave.status_invalid_withdraw_status","domain":"leave","audience":"user","defaultMessage":"当前单据状态不允许撤回,请刷新并确认审批进度。","recovery":"follow_guidance"},
{"code":"leave.withdraw_no_pending_instance","domain":"leave","audience":"user","defaultMessage":"当前没有可撤回的审批,请刷新并确认审批进度。","recovery":"follow_guidance"},
{"code":"leave.withdraw_already_processed_by_approver","domain":"leave","audience":"user","defaultMessage":"审批人已处理该申请,不能再撤回,请联系审批人确认后续处理。","recovery":"follow_guidance"},
{"code":"leave.session_invalid","domain":"leave","audience":"user","defaultMessage":"请选择有效的上午或下午时段。","recovery":"follow_guidance"},
{"code":"leave.leave_invalid_range","domain":"leave","audience":"user","defaultMessage":"请假结束时间早于开始时间,请调整请假时段。","recovery":"follow_guidance"},
{"code":"leave.leave_non_working_range","domain":"leave","audience":"user","defaultMessage":"所选时段没有可请假的工作日,请调整请假日期。","recovery":"follow_guidance"},
{"code":"leave.leave_missing_reserve_ledger","domain":"leave","audience":"user","defaultMessage":"请假额度预留记录缺失,请联系人事核实后处理。","recovery":"follow_guidance"},
{"code":"leave.file_empty","domain":"leave","audience":"user","defaultMessage":"所选文件为空,请重新选择有效文件。","recovery":"follow_guidance"},
{"code":"attendance_correction.invalid_input","domain":"attendance_correction","audience":"user","defaultMessage":"提交内容不完整或格式不正确,请核对必填项和日期后重试。","recovery":"follow_guidance"},
{"code":"attendance_correction.invalid_uuid","domain":"attendance_correction","audience":"user","defaultMessage":"记录标识已失效,请刷新页面后重新选择。","recovery":"follow_guidance"},
{"code":"attendance_correction.datetime_invalid","domain":"attendance_correction","audience":"user","defaultMessage":"时间格式不正确,请重新选择发生时间。","recovery":"follow_guidance"},
{"code":"attendance_correction.feature_flag_attendance_unified_v1_disabled","domain":"attendance_correction","audience":"user","defaultMessage":"考勤修正暂未开放,请联系管理员确认处理方式。","recovery":"follow_guidance"},
{"code":"attendance_correction.workflow_missing_definition","domain":"attendance_correction","audience":"user","defaultMessage":"尚未配置可用审批流程,请联系管理员维护后重试。","recovery":"follow_guidance"},
{"code":"attendance_correction.workflow_no_actionable_approver","domain":"attendance_correction","audience":"user","defaultMessage":"当前没有可处理该申请的审批人,请联系管理员核实审批配置。","recovery":"follow_guidance"},
{"code":"attendance_correction.status_invalid_cancel_status","domain":"attendance_correction","audience":"user","defaultMessage":"当前申请已不能取消,请刷新并确认审批结果。","recovery":"follow_guidance"},
{ code: "payment_batch.legal_entity_required", domain: "payment_batch", audience: "user", defaultMessage: "报销人尚未维护所属公司,请联系管理员补充后再安排付款。", recovery: "contact_admin" },
{ code: "payment_batch.multiple_legal_entities", domain: "payment_batch", audience: "user", defaultMessage: "所选单据属于不同公司,请按公司分别创建付款安排。", recovery: "change_selection" },
{ code: "payment_batch.payee_bank_required", domain: "payment_batch", audience: "user", defaultMessage: "收款人尚未维护开户银行,请联系管理员核实真实开户行后补充。", recovery: "contact_admin" },
{ code: "payment_batch.payee_account_required", domain: "payment_batch", audience: "user", defaultMessage: "收款人尚未维护银行卡,请联系管理员补充真实收款资料。", recovery: "contact_admin" },
{ code: "payment_batch.payee_bank_unsupported", domain: "payment_batch", audience: "user", defaultMessage: "当前银行付款仅支持建行收款账户,请停止本次银行付款并联系财务确认处理方式,不要修改真实开户行。", recovery: "contact_finance" },
{ code: "payment_batch.payment_account_required", domain: "payment_batch", audience: "user", defaultMessage: "所属公司尚无可用的建行付款账户,请联系账户管理员维护后重试。", recovery: "contact_admin" },
{ code: "payment_batch.payment_account_unavailable", domain: "payment_batch", audience: "user", defaultMessage: "公司付款账户暂不可用,请联系管理员检查账户配置后重试。", recovery: "contact_admin" },
{ code: "payment_batch.active_batch_exists", domain: "payment_batch", audience: "user", defaultMessage: "所选单据已进入其他付款安排,请刷新列表并查看已有批次。", recovery: "refresh" },
{ code: "payment_batch.reviewer_required", domain: "payment_batch", audience: "user", defaultMessage: "当前没有可复核的经理或负责人,制单人不能复核自己的批次,请联系管理员安排独立复核人。", recovery: "contact_admin" },
];
const reasonsByCode = new Map(ERROR_REASONS.map((reason) => [reason.code, Object.freeze(reason)]));
Object.freeze(ERROR_REASONS);
function getErrorReason(code) {
const reason = reasonsByCode.get(code);
if (!reason) throw new TypeError("Unregistered business error reason");
return reason;
}
module.exports = { ERROR_REASONS, getErrorReason };
+9 -18
查看文件
@@ -1,3 +1,4 @@
const { buildBusinessValidationError } = require("../utils/businessValidationError");
const { z } = require("zod");
const { hasPermission } = require("../constants/permissions");
@@ -60,19 +61,12 @@ const LIST_QUERY_SCHEMA = z.object({
pageSize: z.coerce.number().int().min(1).max(100).optional(),
});
function buildValidationError(details, statusCode = 400) {
return new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode,
details,
});
}
function parseInput(schema, value) {
const parsed = schema.safeParse(value ?? {});
if (!parsed.success) {
throw buildValidationError(parsed.error.flatten());
throw buildBusinessValidationError("attendance_correction.invalid_input", parsed.error.flatten(), 400);
}
return parsed.data;
}
@@ -80,7 +74,7 @@ function parseInput(schema, value) {
function parseUuid(value, key = "id") {
const parsed = UUID_SCHEMA.safeParse(value);
if (!parsed.success) {
throw buildValidationError({ [key]: "invalid_uuid" });
throw buildBusinessValidationError("attendance_correction.invalid_uuid", { [key]: "invalid_uuid" }, 400);
}
return parsed.data;
}
@@ -93,7 +87,7 @@ function parseOptionalDateTime(value) {
if (!value) return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
throw buildValidationError({ datetime: "invalid" });
throw buildBusinessValidationError("attendance_correction.datetime_invalid", { datetime: "invalid" }, 400);
}
return date;
}
@@ -119,10 +113,7 @@ async function ensureUnifiedFeatureEnabled(fastify, request) {
strictDepartment: false,
});
if (!gate.enabled) {
throw buildValidationError(
{ featureFlag: "attendance_unified_v1_disabled", reason: gate.reason },
409,
);
throw buildBusinessValidationError("attendance_correction.feature_flag_attendance_unified_v1_disabled", { featureFlag: "attendance_unified_v1_disabled", reason: gate.reason }, 409);
}
}
@@ -289,7 +280,7 @@ async function attendanceCorrectionsRoutes(fastify) {
select: { id: true, definition_json: true },
});
if (!workflowDefinition) {
throw buildValidationError({ workflow: "missing_definition" }, 409);
throw buildBusinessValidationError("attendance_correction.workflow_missing_definition", { workflow: "missing_definition" }, 409);
}
const workflowSteps = parseWorkflowSteps(workflowDefinition.definition_json);
const { actionable, skipped } = await resolveActionableStep({
@@ -301,7 +292,7 @@ async function attendanceCorrectionsRoutes(fastify) {
blockOnUnresolvedAssignee: true,
});
if (!actionable) {
throw buildValidationError({ workflow: "no_actionable_approver" }, 409);
throw buildBusinessValidationError("attendance_correction.workflow_no_actionable_approver", { workflow: "no_actionable_approver" }, 409);
}
const workflowInstance = await tx.workflowInstance.create({
@@ -512,7 +503,7 @@ async function attendanceCorrectionsRoutes(fastify) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 });
}
if (correction.status !== "reviewing") {
throw buildValidationError({ status: "invalid_cancel_status" }, 409);
throw buildBusinessValidationError("attendance_correction.status_invalid_cancel_status", { status: "invalid_cancel_status" }, 409);
}
const workflowInstance = await tx.workflowInstance.findFirst({
+13 -72
查看文件
@@ -1,3 +1,4 @@
const { buildBusinessValidationError } = require("../utils/businessValidationError");
const crypto = require("node:crypto");
const path = require("node:path");
@@ -273,12 +274,7 @@ async function resolveInvoiceLinkedItem(tx, { reimbursementId, reimbursementDate
select: { id: true },
});
if (!linkedItem) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { linkedItemId: "item_not_in_reimbursement" },
});
throw buildBusinessValidationError("reimbursement.linked_item_id_item_not_in_reimbursement", { linkedItemId: "item_not_in_reimbursement" }, 400);
}
return { linkedItemId: linkedItem.id, createdItem: false };
}
@@ -305,22 +301,12 @@ async function resolveInvoiceLinkedItem(tx, { reimbursementId, reimbursementDate
}
if (invoice.total_amount == null) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { invoice: "missing_total_amount_for_auto_item" },
});
throw buildBusinessValidationError("reimbursement.invoice_amount_required", { invoice: "missing_total_amount_for_auto_item" }, 409);
}
const expenseType = await resolveDefaultExpenseType(tx);
if (!expenseType) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { expenseTypeId: "missing_enabled_expense_type" },
});
throw buildBusinessValidationError("reimbursement.expense_type_required", { expenseTypeId: "missing_enabled_expense_type" }, 409);
}
const expenseDate =
@@ -1023,12 +1009,7 @@ async function invoicePoolRoutes(fastify) {
async (request, reply) => {
const parsedInvoiceId = z.string().uuid().safeParse(request.params?.id);
if (!parsedInvoiceId.success) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { id: "invalid_uuid" },
});
throw buildBusinessValidationError("reimbursement.invalid_uuid", { id: "invalid_uuid" }, 400);
}
const invoiceId = parsedInvoiceId.data;
@@ -1052,12 +1033,7 @@ async function invoicePoolRoutes(fastify) {
const invoice = invoiceRows[0];
if (invoice.status !== "bound" || (!invoice.linked_reimbursement_id && !invoice.travel_order_id)) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { invoice: "not_bound" },
});
throw buildBusinessValidationError("reimbursement.invoice_not_bound", { invoice: "not_bound" }, 409);
}
if (invoice.linked_reimbursement_id) {
@@ -1089,12 +1065,7 @@ async function invoicePoolRoutes(fastify) {
}
if (!["draft", "rejected"].includes(reimbursement.status)) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { reimbursement: "invalid_status_for_unbind" },
});
throw buildBusinessValidationError("reimbursement.invoice_unbind_status_invalid", { reimbursement: "invalid_status_for_unbind" }, 409);
}
} else {
if (!fastify.hasPermission(request.employee, "travel.payment.manage")) {
@@ -1457,22 +1428,12 @@ async function invoicePoolRoutes(fastify) {
async (request, reply) => {
const parsedInvoiceId = z.string().uuid().safeParse(request.params?.id);
if (!parsedInvoiceId.success) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { id: "invalid_uuid" },
});
throw buildBusinessValidationError("reimbursement.invalid_uuid", { id: "invalid_uuid" }, 400);
}
const parsedBody = bindBodySchema.safeParse(request.body ?? {});
if (!parsedBody.success) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: parsedBody.error.flatten(),
});
throw buildBusinessValidationError("reimbursement.invalid_input", parsedBody.error.flatten(), 400);
}
const invoiceId = parsedInvoiceId.data;
@@ -1600,21 +1561,11 @@ async function invoicePoolRoutes(fastify) {
}
if (!["draft", "rejected"].includes(reimbursement.status)) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { reimbursement: "invalid_status_for_bind" },
});
throw buildBusinessValidationError("reimbursement.invoice_bind_status_invalid", { reimbursement: "invalid_status_for_bind" }, 409);
}
if (invoice.owner_employee_id !== reimbursement.owner_id) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { invoice: "owner_mismatch" },
});
throw buildBusinessValidationError("reimbursement.invoice_owner_mismatch", { invoice: "owner_mismatch" }, 409);
}
const invoiceGuardGate = await evaluateFeatureFlag({
@@ -1625,12 +1576,7 @@ async function invoicePoolRoutes(fastify) {
strictDepartment: true,
});
if (invoiceGuardGate.enabled && !linkedItemId) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: { linkedItemId: "required_when_invoice_guard_enabled" },
});
throw buildBusinessValidationError("reimbursement.invoice_item_required", { linkedItemId: "required_when_invoice_guard_enabled" }, 400);
}
const resolvedLink = await resolveInvoiceLinkedItem(tx, {
@@ -1683,12 +1629,7 @@ async function invoicePoolRoutes(fastify) {
const travelOrder = travelOrderRows[0];
if (!["confirmed", "paid"].includes(travelOrder.status)) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { travelOrder: "invalid_status_for_bind" },
});
throw buildBusinessValidationError("reimbursement.travel_invoice_bind_status_invalid", { travelOrder: "invalid_status_for_bind" }, 409);
}
await tx.invoicePool.update({
+4 -18
查看文件
@@ -1,3 +1,4 @@
const { buildBusinessValidationError } = require("../../utils/businessValidationError");
const { z } = require("zod");
const { AppError } = require("../../utils/AppError");
@@ -199,12 +200,7 @@ function registerInvoiceVerificationRoutes(fastify) {
const cooldownMs =
Math.max(1, Number(fastify.config.verificationRetryCooldownSeconds ?? 60)) * 1000;
if (diffMs < cooldownMs) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { verification: "too_frequent" },
});
throw buildBusinessValidationError("invoice_verification.too_frequent", { verification: "too_frequent" }, 409);
}
}
@@ -447,22 +443,12 @@ function registerInvoiceVerificationRoutes(fastify) {
Math.max(1, Number(fastify.config.verificationRetryCooldownSeconds ?? 60)) * 1000;
const diffMs = Date.now() - new Date(latest.requested_at).getTime();
if (diffMs < cooldownMs) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { verification: "too_frequent" },
});
throw buildBusinessValidationError("invoice_verification.too_frequent", { verification: "too_frequent" }, 409);
}
const attempts = Number(latest.attempts ?? 1) + 1;
if (attempts > Number(fastify.config.verificationMaxAttempts ?? 3)) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { verification: "attempts_exceeded" },
});
throw buildBusinessValidationError("invoice_verification.attempts_exceeded", { verification: "attempts_exceeded" }, 409);
}
const createdRecord = await tx.invoiceVerificationRecord.create({
+30 -39
查看文件
@@ -1,3 +1,4 @@
const { buildBusinessValidationError } = require("../utils/businessValidationError");
const { z } = require("zod");
const { AppError } = require("../utils/AppError");
@@ -134,19 +135,12 @@ const LEAVE_WORKFLOW_SELECT = {
},
};
function buildValidationError(details, statusCode = 400) {
return new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode,
details,
});
}
function parseLeaveId(value) {
const parsed = z.string().uuid().safeParse(value);
if (!parsed.success) {
throw buildValidationError({ id: "invalid_uuid" });
throw buildBusinessValidationError("leave.invalid_uuid", { id: "invalid_uuid" }, 400);
}
return parsed.data;
}
@@ -154,22 +148,22 @@ function parseLeaveId(value) {
function parseLeaveListQuery(query) {
const parsed = leaveListQuerySchema.safeParse(query ?? {});
if (!parsed.success) {
throw buildValidationError(parsed.error.flatten());
throw buildBusinessValidationError("leave.invalid_input", parsed.error.flatten(), 400);
}
const dateFrom = parsed.data.dateFrom ? new Date(`${parsed.data.dateFrom}T00:00:00.000Z`) : null;
const dateTo = parsed.data.dateTo ? new Date(`${parsed.data.dateTo}T00:00:00.000Z`) : null;
if (dateFrom && Number.isNaN(dateFrom.getTime())) {
throw buildValidationError({ dateFrom: "invalid" });
throw buildBusinessValidationError("leave.date_from_invalid", { dateFrom: "invalid" }, 400);
}
if (dateTo && Number.isNaN(dateTo.getTime())) {
throw buildValidationError({ dateTo: "invalid" });
throw buildBusinessValidationError("leave.date_to_invalid", { dateTo: "invalid" }, 400);
}
if (dateFrom && dateTo && dateFrom > dateTo) {
throw buildValidationError({ dateTo: "must_be_after_dateFrom" });
throw buildBusinessValidationError("leave.date_to_must_be_after_date_from", { dateTo: "must_be_after_dateFrom" }, 400);
}
if (parsed.data.departmentId && parsed.data.scope !== "all") {
throw buildValidationError({ departmentId: "scope_all_only" });
throw buildBusinessValidationError("leave.department_id_scope_all_only", { departmentId: "scope_all_only" }, 400);
}
return {
@@ -186,7 +180,7 @@ function parseLeaveListQuery(query) {
function parseLeaveBalanceQuery(query) {
const parsed = leaveBalanceQuerySchema.safeParse(query ?? {});
if (!parsed.success) {
throw buildValidationError(parsed.error.flatten());
throw buildBusinessValidationError("leave.invalid_input", parsed.error.flatten(), 400);
}
return parsed.data;
@@ -195,7 +189,7 @@ function parseLeaveBalanceQuery(query) {
function parseWithdrawBody(body) {
const parsed = withdrawBodySchema.safeParse(body ?? {});
if (!parsed.success) {
throw buildValidationError(parsed.error.flatten());
throw buildBusinessValidationError("leave.invalid_input", parsed.error.flatten(), 400);
}
return {
@@ -206,14 +200,14 @@ function parseWithdrawBody(body) {
function parseCreateBody(body) {
const parsed = createLeaveSchema.safeParse(body ?? {});
if (!parsed.success) {
throw buildValidationError(parsed.error.flatten());
throw buildBusinessValidationError("leave.invalid_input", parsed.error.flatten(), 400);
}
const startDate = new Date(`${parsed.data.startDate}T00:00:00.000Z`);
const endDate = new Date(`${parsed.data.endDate}T00:00:00.000Z`);
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
throw buildValidationError({ date: "invalid" });
throw buildBusinessValidationError("leave.date_invalid", { date: "invalid" }, 400);
}
const startSession = parsed.data.startSession ?? "am";
@@ -239,14 +233,14 @@ function parseCreateBody(body) {
function parseWorkingDaysPreviewQuery(query) {
const parsed = workingDaysPreviewQuerySchema.safeParse(query ?? {});
if (!parsed.success) {
throw buildValidationError(parsed.error.flatten());
throw buildBusinessValidationError("leave.invalid_input", parsed.error.flatten(), 400);
}
const startDate = new Date(`${parsed.data.startDate}T00:00:00.000Z`);
const endDate = new Date(`${parsed.data.endDate}T00:00:00.000Z`);
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
throw buildValidationError({ date: "invalid" });
throw buildBusinessValidationError("leave.date_invalid", { date: "invalid" }, 400);
}
const requestedDays = calculateRequestedDays({
@@ -351,7 +345,7 @@ async function collectCreatePayload(request) {
for await (const part of request.parts()) {
if (part.type === "file") {
if (filePart) {
throw buildValidationError({ file: "only_single_file_supported" });
throw buildBusinessValidationError("leave.file_only_single_file_supported", { file: "only_single_file_supported" }, 400);
}
filePart = part;
} else {
@@ -360,7 +354,7 @@ async function collectCreatePayload(request) {
}
} catch (error) {
if (error?.code === "FST_REQ_FILE_TOO_LARGE") {
throw buildValidationError({ file: "file_too_large" });
throw buildBusinessValidationError("leave.file_file_too_large", { file: "file_too_large" }, 400);
}
throw error;
}
@@ -482,7 +476,7 @@ async function loadLeaveTypeOrThrow(prisma, leaveTypeId) {
});
if (!leaveType || !leaveType.enabled || !leaveType.leave_policy || !leaveType.leave_policy.enabled) {
throw buildValidationError({ leaveTypeId: "missing_active_policy" }, 409);
throw buildBusinessValidationError("leave.leave_type_id_missing_active_policy", { leaveTypeId: "missing_active_policy" }, 409);
}
return leaveType;
@@ -500,14 +494,14 @@ async function resolveLeaveTypeAttachment(
if (!filePart) {
if (requireWhenMissing && requiresAttachment) {
throw buildValidationError({ file: "required" }, 422);
throw buildBusinessValidationError("leave.file_required", { file: "required" }, 422);
}
return null;
}
const mimeAllowlist = new Set(allowedMimeTypes.length ? allowedMimeTypes : DEFAULT_ATTACHMENT_MIME_TYPES);
if (!mimeAllowlist.has(filePart.mimetype)) {
throw buildValidationError({ file: "unsupported_mime_type" });
throw buildBusinessValidationError("leave.file_unsupported_mime_type", { file: "unsupported_mime_type" }, 400);
}
return persistLeaveAttachment({
@@ -807,7 +801,7 @@ async function leaveRoutes(fastify) {
}
if (!["draft", "rejected"].includes(leave.status)) {
throw buildValidationError({ status: "invalid_edit_status" }, 409);
throw buildBusinessValidationError("leave.status_invalid_edit_status", { status: "invalid_edit_status" }, 409);
}
const leaveType = await loadLeaveTypeOrThrow(tx, body.leaveTypeId);
@@ -1007,7 +1001,7 @@ async function leaveRoutes(fastify) {
}
if (!["draft", "rejected"].includes(leave.status)) {
throw buildValidationError({ status: "invalid_submit_status" }, 409);
throw buildBusinessValidationError("leave.status_invalid_submit_status", { status: "invalid_submit_status" }, 409);
}
const activeInstance = await tx.workflowInstance.findFirst({
@@ -1019,12 +1013,12 @@ async function leaveRoutes(fastify) {
select: { id: true },
});
if (activeInstance) {
throw buildValidationError({ submit: "already_in_review" }, 409);
throw buildBusinessValidationError("leave.submit_already_in_review", { submit: "already_in_review" }, 409);
}
const leaveType = await loadLeaveTypeOrThrow(tx, leave.leave_type_id);
if (leaveType.requires_attachment && !leave.attachment_storage_path) {
throw buildValidationError({ file: "required" }, 422);
throw buildBusinessValidationError("leave.file_required", { file: "required" }, 422);
}
const overlappingRequests = await tx.leaveRequest.findMany({
@@ -1061,10 +1055,7 @@ async function leaveRoutes(fastify) {
),
);
if (conflicting) {
throw buildValidationError(
{ leave: "conflict_range", conflictLeaveRequestId: conflicting.id },
422,
);
throw buildBusinessValidationError("leave.leave_conflict_range", { leave: "conflict_range", conflictLeaveRequestId: conflicting.id }, 422);
}
await lockEmployeeForLeave(tx, leave.owner_id);
@@ -1078,7 +1069,7 @@ async function leaveRoutes(fastify) {
if (
toHalfDayUnits(balanceSnapshot.summary.available) < toHalfDayUnits(leave.requested_days)
) {
throw buildValidationError({ leave: "insufficient_balance" }, 422);
throw buildBusinessValidationError("leave.leave_insufficient_balance", { leave: "insufficient_balance" }, 422);
}
const workflowDefinition = await tx.workflowDefinition.findFirst({
@@ -1087,7 +1078,7 @@ async function leaveRoutes(fastify) {
select: { id: true },
});
if (!workflowDefinition) {
throw buildValidationError({ workflow: "missing_definition" });
throw buildBusinessValidationError("leave.workflow_missing_definition", { workflow: "missing_definition" }, 400);
}
const { actionable, skipped } = await resolveActionableStep({
@@ -1099,7 +1090,7 @@ async function leaveRoutes(fastify) {
blockOnUnresolvedAssignee: true,
});
if (!actionable) {
throw buildValidationError({ workflow: "no_actionable_approver" }, 409);
throw buildBusinessValidationError("leave.workflow_no_actionable_approver", { workflow: "no_actionable_approver" }, 409);
}
await tx.leaveRequest.update({
@@ -1244,7 +1235,7 @@ async function leaveRoutes(fastify) {
}
if (leave.status !== "reviewing") {
throw buildValidationError({ status: "invalid_withdraw_status" }, 409);
throw buildBusinessValidationError("leave.status_invalid_withdraw_status", { status: "invalid_withdraw_status" }, 409);
}
const activeInstance = await tx.workflowInstance.findFirst({
@@ -1258,7 +1249,7 @@ async function leaveRoutes(fastify) {
});
if (!activeInstance) {
throw buildValidationError({ withdraw: "no_pending_instance" }, 409);
throw buildBusinessValidationError("leave.withdraw_no_pending_instance", { withdraw: "no_pending_instance" }, 409);
}
const processedTaskCount = await tx.workflowTask.count({
@@ -1268,7 +1259,7 @@ async function leaveRoutes(fastify) {
},
});
if (processedTaskCount > 0) {
throw buildValidationError({ withdraw: "already_processed_by_approver" }, 409);
throw buildBusinessValidationError("leave.withdraw_already_processed_by_approver", { withdraw: "already_processed_by_approver" }, 409);
}
const ledger = await loadLeaveLedgerSummaryForRequest(tx, leaveId);
+151 -196
查看文件
@@ -1,3 +1,6 @@
const { buildBusinessValidationError } = require("../utils/businessValidationError");
const { ERROR_REASONS } = require("../constants/errorReasons");
const { resolvePaymentBatchCreationReadiness, paymentReadinessError } = require("../services/paymentBatchReadiness");
const crypto = require("node:crypto");
const path = require("node:path");
@@ -55,6 +58,7 @@ const LIST_QUERY_SCHEMA = z.object({
});
const CANDIDATES_QUERY_SCHEMA = z.object({
paymentChannel: PAYMENT_CHANNEL_SCHEMA.default("bank"),
q: z.string().trim().max(100).optional(),
page: z.coerce.number().int().min(1).default(1),
pageSize: z.coerce.number().int().min(1).max(100).default(20),
@@ -91,19 +95,12 @@ const RECEIPT_FILE_QUERY_SCHEMA = z.object({
const HIGH_RISK_BATCH_THRESHOLD_AMOUNT = 50000;
const PAYMENT_BATCH_DAILY_SEQUENCE_MAX = 999;
function buildValidationError(details, statusCode = 400) {
return new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode,
details,
});
}
function parseId(value, key = "id") {
const parsed = UUID_SCHEMA.safeParse(value);
if (!parsed.success) {
throw buildValidationError({ [key]: "invalid_uuid" });
throw buildBusinessValidationError("payment_batch.invalid_identifier", { [key]: "invalid_uuid" }, 400);
}
return parsed.data;
}
@@ -111,7 +108,7 @@ function parseId(value, key = "id") {
function parseBody(schema, body) {
const parsed = schema.safeParse(body ?? {});
if (!parsed.success) {
throw buildValidationError(parsed.error.flatten());
throw buildBusinessValidationError("payment_batch.invalid_input", parsed.error.flatten(), 400);
}
return parsed.data;
}
@@ -157,7 +154,7 @@ async function ensureBankReceiptForExecutableBatch(tx, batch) {
where: { payment_batch_id: batch.id, archived_at: null },
});
if (receiptCount <= 0) {
throw buildValidationError({ receiptAttachments: "bank_receipt_required" }, 409);
throw buildBusinessValidationError("payment_batch.bank_receipt_required", { receiptAttachments: "bank_receipt_required" }, 409);
}
}
@@ -170,7 +167,7 @@ async function ensureFeatureEnabled(fastify) {
});
if (!gate.enabled) {
throw buildValidationError({ featureFlag: "payment_batch_v1_disabled", reason: gate.reason }, 409);
throw buildBusinessValidationError("payment_batch.department_unavailable", { featureFlag: "payment_batch_v1_disabled", reason: gate.reason }, 409);
}
return gate;
@@ -200,7 +197,7 @@ async function evaluatePaymentBatchDepartments(fastify, prisma, departmentIds) {
async function ensurePaymentBatchDepartmentsEnabled(fastify, prisma, departmentIds) {
const result = await evaluatePaymentBatchDepartments(fastify, prisma, departmentIds);
if (!result.enabled) {
throw buildValidationError({
throw buildBusinessValidationError("payment_batch.department_unavailable", {
featureFlag: "payment_batch_v1_disabled",
reason: result.gate?.reason ?? "department_not_allowed",
}, 409);
@@ -221,7 +218,7 @@ async function loadBatchSourceDepartmentIds(tx, batch) {
select: { id: true, department_id: true },
});
if (sources.length !== entityIds.length) {
throw buildValidationError({ entityIds: "contains_not_found" }, 404);
throw buildBusinessValidationError("payment_batch.source_not_found", { entityIds: "contains_not_found" }, 404);
}
return sources.map((source) => source.department_id ?? null);
}
@@ -421,7 +418,7 @@ async function allocatePaymentBatchNumber(tx, timeZone) {
`;
const nextSequence = parsePaymentBatchDailySequence(rows?.[0]?.batch_no, compactDate) + 1;
if (nextSequence > PAYMENT_BATCH_DAILY_SEQUENCE_MAX) {
throw buildValidationError({ batchNo: "daily_sequence_exhausted" }, 409);
throw buildBusinessValidationError("payment_batch.daily_sequence_exhausted", { batchNo: "daily_sequence_exhausted" }, 409);
}
const sequence = String(nextSequence).padStart(3, "0");
@@ -520,15 +517,20 @@ function buildPayeeSnapshot(config, owner, options = {}) {
let bankAccount = null;
try {
bankAccount = encryptedAccount ? decryptStringOrThrow(config, encryptedAccount) : null;
if (encryptedAccount && !bankAccount) throw new Error("Payee bank account could not be decrypted");
} catch (error) {
if (options.forReadiness) {
return {
bankInstitutionCode: owner?.bank_institution?.code ?? null,
bankAccountEncrypted: encryptedAccount ?? null,
bankAccountUnavailable: true,
};
}
if (options.includeEncrypted) {
throw buildValidationError({ payeeBankAccount: "decrypt_failed" }, 409);
throw buildBusinessValidationError("payment_batch.payee_account_unavailable", { payeeBankAccount: "decrypt_failed" }, 409);
}
throw error;
}
if (encryptedAccount && !bankAccount) {
throw new Error("Payee bank account could not be decrypted");
}
return {
name: owner?.name ?? null,
bankInstitutionId: owner?.bank_institution?.id ?? null,
@@ -539,7 +541,7 @@ function buildPayeeSnapshot(config, owner, options = {}) {
};
}
async function loadEntitySnapshots(tx, entityType, entityIds, config) {
async function loadEntitySnapshots(tx, entityType, entityIds, config, options = {}) {
if (entityType === "reimbursement") {
const items = await tx.reimbursementForm.findMany({
where: { id: { in: entityIds } },
@@ -582,7 +584,7 @@ async function loadEntitySnapshots(tx, entityType, entityIds, config) {
paymentDate: item.payment_date?.toISOString?.()?.slice(0, 10) ?? null,
paymentChannel: item.payment_channel ?? null,
paidAt: item.paid_at?.toISOString?.() ?? null,
payee: buildPayeeSnapshot(config, item.owner, { includeEncrypted: true }),
payee: buildPayeeSnapshot(config, item.owner, { includeEncrypted: true, ...options }),
},
}));
}
@@ -620,19 +622,60 @@ async function loadEntitySnapshots(tx, entityType, entityIds, config) {
}));
}
function ensureBatchEntityStatus(entityType, snapshot) {
if (
entityType === "reimbursement" &&
(snapshot.status !== "approved" || snapshot.snapshotJson?.paidAt)
) {
throw buildValidationError({ entity: `${snapshot.entityCode}:invalid_pay_status` }, 409);
async function loadPaymentCreationFacts(fastify, tx, { entityType, entityIds, paymentChannel, makerId }) {
const snapshots = await loadEntitySnapshots(tx, entityType, entityIds, fastify.config, { forReadiness: true });
const departmentReadiness = new Map();
const flagReads = new Map();
const gatePrisma = { featureFlag: { findUnique(query) {
const key = query.where.flag_key;
if (!flagReads.has(key)) flagReads.set(key, tx.featureFlag.findUnique(query));
return flagReads.get(key);
} } };
for (const departmentId of new Set(snapshots.map((snapshot) => snapshot.departmentId ?? null))) {
departmentReadiness.set(departmentId, await evaluatePaymentBatchDepartments(fastify, gatePrisma, [departmentId]));
}
if (
entityType === "travel_order" &&
(snapshot.status !== "confirmed" || snapshot.snapshotJson?.paidAt)
) {
throw buildValidationError({ entity: `${snapshot.entityCode}:invalid_pay_status` }, 409);
const companyIds = [...new Set(snapshots.map((snapshot) => snapshot.legalEntityId).filter(Boolean))];
const accounts = paymentChannel === "bank" && companyIds.length ? await tx.paymentAccount.findMany({
where: {
legal_entity_id: { in: companyIds }, enabled: true, archived_at: null,
bank_institution: { code: "CCB", enabled: true, archived_at: null },
},
include: PAYMENT_ACCOUNT_INCLUDE,
orderBy: { id: "asc" },
}) : [];
const accountReadiness = new Map();
for (const paymentAccount of accounts) {
if (accountReadiness.has(paymentAccount.legal_entity_id)) continue;
let paymentAccountSnapshot = null;
try {
const accountNumber = decryptStringOrThrow(fastify.config, paymentAccount.account_number_encrypted);
if (accountNumber) {
paymentAccountSnapshot = {
paymentAccountId: paymentAccount.id,
legalEntityId: paymentAccount.legal_entity_id,
accountName: paymentAccount.account_name,
bankInstitutionId: paymentAccount.bank_institution.id,
bankInstitutionCode: paymentAccount.bank_institution.code,
bankName: paymentAccount.bank_institution.name,
accountNumberEncrypted: paymentAccount.account_number_encrypted,
accountNumberMasked: maskBankAccount(accountNumber),
};
}
} catch {}
accountReadiness.set(paymentAccount.legal_entity_id, { paymentAccount, paymentAccountSnapshot });
}
const reservedItems = await tx.paymentBatchItem.findMany({
where: {
entity_id: { in: entityIds },
payment_batch: { entity_type: entityType, status: { not: "cancelled" } },
},
select: { entity_id: true },
});
const reviewerIds = await resolvePaymentReviewRecipientIds(tx, makerId);
return {
entityType, entityIds, paymentChannel, snapshots, departmentReadiness, accountReadiness,
reservedEntityIds: new Set(reservedItems.map((item) => item.entity_id)), reviewerIds,
};
}
function normalizeSnapshotText(value) {
@@ -972,13 +1015,13 @@ async function executeReimbursementPayment(tx, { batch, item, actorId, index, re
throw buildNotFoundError();
}
if (current.status === "paid") {
throw buildValidationError({ payment: "already_paid_conflict" }, 409);
throw buildBusinessValidationError("payment_batch.already_paid_conflict", { payment: "already_paid_conflict" }, 409);
}
if (current.status !== "approved") {
throw buildValidationError({ status: "invalid_pay_status" }, 409);
throw buildBusinessValidationError("payment_batch.source_status_invalid", { status: "invalid_pay_status" }, 409);
}
if (toMoneyString(current.total_amount) !== toMoneyString(item.amount)) {
throw buildValidationError({ payment: "source_amount_changed" }, 409);
throw buildBusinessValidationError("payment_batch.source_amount_changed", { payment: "source_amount_changed" }, 409);
}
const voucher = normalizeNullableString(item.payment_voucher_no_override) ??
@@ -1186,13 +1229,13 @@ async function executeTravelOrderPayment(tx, { batch, item, actorId, index, time
throw buildNotFoundError();
}
if (current.status === "paid") {
throw buildValidationError({ payment: "already_paid_conflict" }, 409);
throw buildBusinessValidationError("payment_batch.already_paid_conflict", { payment: "already_paid_conflict" }, 409);
}
if (current.status !== "confirmed") {
throw buildValidationError({ status: "invalid_pay_status" }, 409);
throw buildBusinessValidationError("payment_batch.source_status_invalid", { status: "invalid_pay_status" }, 409);
}
if (toMoneyString(current.amount) !== toMoneyString(item.amount) || current.currency !== item.currency_code) {
throw buildValidationError({ payment: "source_amount_changed" }, 409);
throw buildBusinessValidationError("payment_batch.source_amount_changed", { payment: "source_amount_changed" }, 409);
}
const voucher = normalizeNullableString(item.payment_voucher_no_override) ??
@@ -1213,7 +1256,7 @@ async function executeTravelOrderPayment(tx, { batch, item, actorId, index, time
select: { id: true },
});
if (!matchedTransaction) {
throw buildValidationError({ payment: "missing_settled_transaction" }, 409);
throw buildBusinessValidationError("payment_batch.settled_transaction_required", { payment: "missing_settled_transaction" }, 409);
}
await tx.travelOrder.update({
@@ -1293,12 +1336,7 @@ async function readBoundedStream(stream, maxBytes) {
}
function buildReceiptAttachmentBlockedError(reasonCode) {
return new AppError({
code: "VALID_001",
message: "付款回单未通过安全校验",
statusCode: 409,
details: { receiptAttachment: "security_validation_failed", reason: reasonCode },
});
return buildBusinessValidationError("payment_batch.receipt_security_failed", { receiptAttachment: "security_validation_failed", reason: reasonCode }, 409);
}
async function auditBlockedReceiptDownload({ fastify, request, batchId, attachment, reasonCode, downloadReason }) {
@@ -1351,7 +1389,7 @@ async function readReceiptUploadFiles(fastify, request) {
}
const buffer = await readBoundedStream(part.file, maxBytes);
if (part.fieldname !== "files") {
throw buildValidationError({ file: "invalid_field" });
throw buildBusinessValidationError("payment_batch.receipt_field_invalid", { file: "invalid_field" }, 400);
}
files.push(await validateAndNormalizePurchaseAttachment({
buffer,
@@ -1363,22 +1401,22 @@ async function readReceiptUploadFiles(fastify, request) {
}
} catch (error) {
if (error instanceof ReceiptAttachmentSizeLimitError || error?.code === "FST_REQ_FILE_TOO_LARGE") {
throw buildValidationError({ file: "file_too_large" });
throw buildBusinessValidationError("payment_batch.receipt_too_large", { file: "file_too_large" }, 400);
}
if (error?.code === "FST_FILES_LIMIT") {
throw buildValidationError({ file: "too_many_files" });
throw buildBusinessValidationError("payment_batch.receipt_too_many", { file: "too_many_files" }, 400);
}
if (error?.code === "FST_FIELDS_LIMIT" || error?.code === "FST_PARTS_LIMIT") {
throw buildValidationError({ file: "multipart_limit_exceeded" });
throw buildBusinessValidationError("payment_batch.receipt_upload_limit", { file: "multipart_limit_exceeded" }, 400);
}
if (error instanceof PurchaseAttachmentPolicyError) {
throw buildValidationError(error.details ?? { file: error.reasonCode });
throw buildBusinessValidationError("payment_batch.receipt_file_invalid", error.details ?? { file: error.reasonCode }, 400);
}
throw error;
}
if (!files.length) {
throw buildValidationError({ file: "missing" });
throw buildBusinessValidationError("payment_batch.receipt_required", { file: "missing" }, 400);
}
return files;
@@ -1389,24 +1427,12 @@ async function paymentBatchesRoutes(fastify) {
await ensureFeatureEnabled(fastify);
ensureBatchManagePermission(fastify, request);
const parsed = CANDIDATES_QUERY_SCHEMA.safeParse(request.query ?? {});
if (!parsed.success) throw buildValidationError(parsed.error.flatten());
const { q, page, pageSize } = parsed.data;
if (!parsed.success) throw buildBusinessValidationError("payment_batch.invalid_input", parsed.error.flatten(), 400);
const { q, page, pageSize, paymentChannel } = parsed.data;
const reservedItems = await fastify.prisma.paymentBatchItem.findMany({
where: {
payment_batch: {
entity_type: "reimbursement",
status: { not: "cancelled" },
},
},
distinct: ["entity_id"],
select: { entity_id: true },
});
const reservedIds = reservedItems.map((item) => item.entity_id);
const where = {
status: "approved",
paid_at: null,
...(reservedIds.length ? { id: { notIn: reservedIds } } : {}),
...(q
? {
OR: [
@@ -1433,26 +1459,15 @@ async function paymentBatchesRoutes(fastify) {
},
orderBy: [{ reimbursement_date: "desc" }, { id: "desc" }],
});
const departmentGateCache = new Map();
const eligibleCandidates = [];
for (const candidate of allCandidates) {
const departmentId = candidate.department_id ?? null;
const cacheKey = departmentId ?? "__missing_department__";
let enabled = departmentGateCache.get(cacheKey);
if (enabled == null) {
const result = await evaluatePaymentBatchDepartments(fastify, fastify.prisma, [departmentId]);
enabled = result.enabled;
departmentGateCache.set(cacheKey, enabled);
}
if (enabled) {
eligibleCandidates.push(candidate);
}
}
const total = eligibleCandidates.length;
const candidates = eligibleCandidates.slice((page - 1) * pageSize, page * pageSize);
const total = allCandidates.length;
const candidates = allCandidates.slice((page - 1) * pageSize, page * pageSize);
const facts = await loadPaymentCreationFacts(fastify, fastify.prisma, {
entityType: "reimbursement", entityIds: candidates.map((candidate) => candidate.id), paymentChannel, makerId: request.employee.id,
});
return sendData(reply, request, {
items: candidates.map((item) => ({
readiness: resolvePaymentBatchCreationReadiness({ ...facts, entityIds: [item.id] }),
entityId: item.id,
entityType: "reimbursement",
businessNo: item.form_number ?? "待编号报销单",
@@ -1472,11 +1487,22 @@ async function paymentBatchesRoutes(fastify) {
});
});
fastify.post("/payment-batches/readiness", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify);
ensureBatchManagePermission(fastify, request);
const body = parseBody(CREATE_SCHEMA.pick({ entityType: true, entityIds: true, defaultPaymentChannel: true }), request.body);
const entityIds = [...new Set(body.entityIds)];
const facts = await loadPaymentCreationFacts(fastify, fastify.prisma, {
entityType: body.entityType, entityIds, paymentChannel: body.defaultPaymentChannel ?? "bank", makerId: request.employee.id,
});
return sendData(reply, request, resolvePaymentBatchCreationReadiness(facts));
});
fastify.get("/payment-batches", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify);
ensureBatchReadPermission(fastify, request);
const queryParsed = LIST_QUERY_SCHEMA.safeParse(request.query ?? {});
if (!queryParsed.success) throw buildValidationError(queryParsed.error.flatten());
if (!queryParsed.success) throw buildBusinessValidationError("payment_batch.invalid_input", queryParsed.error.flatten(), 400);
const query = queryParsed.data;
const items = await fastify.prisma.paymentBatch.findMany({
where: {
@@ -1510,97 +1536,20 @@ async function paymentBatchesRoutes(fastify) {
ensureBatchManagePermission(fastify, request);
const body = parseBody(CREATE_SCHEMA, request.body);
if (body.entityType !== "reimbursement") {
throw buildValidationError({ entityType: "travel_payment_frozen" }, 409);
throw buildBusinessValidationError("payment_batch.travel_payment_frozen", { entityType: "travel_payment_frozen" }, 409);
}
const entityIds = Array.from(new Set(body.entityIds)).sort();
const { batch: created, notificationTask } = await fastify.prisma.$transaction(async (tx) => {
await lockPaymentEntities(tx, body.entityType, entityIds);
const snapshots = await loadEntitySnapshots(tx, body.entityType, entityIds, fastify.config);
if (snapshots.length !== entityIds.length) {
throw buildValidationError({ entityIds: "contains_not_found" }, 404);
}
await ensurePaymentBatchDepartmentsEnabled(
fastify,
tx,
snapshots.map((snapshot) => snapshot.departmentId ?? null),
);
for (const snapshot of snapshots) {
ensureBatchEntityStatus(body.entityType, snapshot);
}
const isBankBatch = (body.defaultPaymentChannel ?? "bank") === "bank";
let paymentAccount = null;
let paymentAccountSnapshot = null;
let legalEntityId = null;
if (isBankBatch) {
const legalEntityIds = Array.from(
new Set(snapshots.map((snapshot) => snapshot.legalEntityId).filter(Boolean)),
);
if (legalEntityIds.length !== 1 || snapshots.some((snapshot) => !snapshot.legalEntityId)) {
throw buildValidationError({ legalEntity: "single_legal_entity_required" }, 409);
}
legalEntityId = legalEntityIds[0];
const invalidPayee = snapshots.find((snapshot) => {
const payee = snapshot.snapshotJson?.payee;
return payee?.bankInstitutionCode !== "CCB" || !payee?.bankAccountEncrypted;
const facts = await loadPaymentCreationFacts(fastify, tx, {
entityType: body.entityType, entityIds, paymentChannel: body.defaultPaymentChannel ?? "bank", makerId: request.employee.id,
});
if (invalidPayee) {
throw buildValidationError({
payeeBankAccount: `${invalidPayee.entityCode}:ccb_account_required`,
}, 409);
}
paymentAccount = await tx.paymentAccount.findFirst({
where: {
legal_entity_id: legalEntityId,
enabled: true,
archived_at: null,
bank_institution: { code: "CCB", enabled: true, archived_at: null },
},
include: PAYMENT_ACCOUNT_INCLUDE,
});
if (!paymentAccount) {
throw buildValidationError({ paymentAccount: "active_ccb_account_required" }, 409);
}
let paymentAccountNumber;
try {
paymentAccountNumber = decryptStringOrThrow(
fastify.config,
paymentAccount.account_number_encrypted,
);
} catch (error) {
throw buildValidationError({ paymentAccount: "decrypt_failed" }, 409);
}
paymentAccountSnapshot = {
paymentAccountId: paymentAccount.id,
legalEntityId,
accountName: paymentAccount.account_name,
bankInstitutionId: paymentAccount.bank_institution.id,
bankInstitutionCode: paymentAccount.bank_institution.code,
bankName: paymentAccount.bank_institution.name,
accountNumberEncrypted: paymentAccount.account_number_encrypted,
accountNumberMasked: maskBankAccount(paymentAccountNumber),
};
}
const reservedItems = await tx.paymentBatchItem.findMany({
where: {
entity_id: { in: entityIds },
payment_batch: {
entity_type: body.entityType,
status: { not: "cancelled" },
},
},
select: { entity_id: true },
});
if (reservedItems.length) {
throw buildValidationError({ entityIds: "contains_active_payment_batch" }, 409);
}
const recipientEmployeeIds = await resolvePaymentReviewRecipientIds(tx, request.employee.id);
if (!recipientEmployeeIds.length) {
throw buildValidationError({ review: "no_eligible_reviewer" }, 409);
}
const readiness = resolvePaymentBatchCreationReadiness(facts);
if (!readiness.ready) throw paymentReadinessError(readiness, facts.departmentReadiness);
const { snapshots, reviewerIds: recipientEmployeeIds } = facts;
const legalEntityId = facts.paymentChannel === "bank" ? snapshots[0].legalEntityId : null;
const { paymentAccount = null, paymentAccountSnapshot = null } = facts.accountReadiness.get(legalEntityId) ?? {};
const allocatedNumber = await allocatePaymentBatchNumber(tx, fastify.config.appTimezone);
@@ -1786,19 +1735,19 @@ async function paymentBatchesRoutes(fastify) {
const batch = await loadBatchWithItems(fastify.prisma, batchId);
await ensureBatchSourceDepartmentsEnabled(fastify, fastify.prisma, batch);
if (!["reviewed", "failed"].includes(batch.status)) {
throw buildValidationError({ status: "payee_bank_reveal_not_available" }, 409);
throw buildBusinessValidationError("payment_batch.account_reveal_unavailable", { status: "payee_bank_reveal_not_available" }, 409);
}
const item = batch.items.find((candidate) => candidate.id === itemId);
if (!item) throw buildNotFoundError();
const encryptedAccount = item.snapshot_json?.payee?.bankAccountEncrypted;
if (!encryptedAccount) {
throw buildValidationError({ payeeBankAccount: "legacy_snapshot_missing" }, 409);
throw buildBusinessValidationError("payment_batch.payee_snapshot_missing", { payeeBankAccount: "legacy_snapshot_missing" }, 409);
}
let bankAccount;
try {
bankAccount = decryptStringOrThrow(fastify.config, encryptedAccount);
} catch (error) {
throw buildValidationError({ payeeBankAccount: "decrypt_failed" }, 409);
throw buildBusinessValidationError("payment_batch.payee_account_unavailable", { payeeBankAccount: "decrypt_failed" }, 409);
}
await fastify.prisma.auditLog.create({
data: buildAuditLogData(request, {
@@ -1860,7 +1809,7 @@ async function paymentBatchesRoutes(fastify) {
const batch = await loadBatchWithItems(fastify.prisma, batchId);
await ensureBatchSourceDepartmentsEnabled(fastify, fastify.prisma, batch);
if (!["reviewed", "failed"].includes(batch.status)) {
throw buildValidationError({ status: "receipt_attachment_not_editable" }, 409);
throw buildBusinessValidationError("payment_batch.receipt_not_editable", { status: "receipt_attachment_not_editable" }, 409);
}
const files = await readReceiptUploadFiles(fastify, request);
@@ -1886,7 +1835,7 @@ async function paymentBatchesRoutes(fastify) {
const lockedBatch = await lockBatchWithItems(tx, batchId);
await ensureBatchSourceDepartmentsEnabled(fastify, tx, lockedBatch);
if (!["reviewed", "failed"].includes(lockedBatch.status)) {
throw buildValidationError({ status: "receipt_attachment_not_editable" }, 409);
throw buildBusinessValidationError("payment_batch.receipt_not_editable", { status: "receipt_attachment_not_editable" }, 409);
}
const created = [];
@@ -1970,7 +1919,7 @@ async function paymentBatchesRoutes(fastify) {
const batchId = parseId(request.params?.id);
const attachmentId = parseId(request.params?.attachmentId, "attachmentId");
const queryParsed = RECEIPT_FILE_QUERY_SCHEMA.safeParse(request.query ?? {});
if (!queryParsed.success) throw buildValidationError(queryParsed.error.flatten());
if (!queryParsed.success) throw buildBusinessValidationError("payment_batch.invalid_input", queryParsed.error.flatten(), 400);
const previewMode = queryParsed.data.mode === "preview";
const batch = await loadBatchWithItems(fastify.prisma, batchId);
await ensureBatchSourceDepartmentsEnabled(fastify, fastify.prisma, batch);
@@ -1998,13 +1947,13 @@ async function paymentBatchesRoutes(fastify) {
throw buildNotFoundError();
}
if (attachment.archived_at) {
throw buildValidationError({ receiptAttachment: "voided" }, 409);
throw buildBusinessValidationError("payment_batch.receipt_voided", { receiptAttachment: "voided" }, 409);
}
const downloadReason = previewMode
? "付款批次内在线预览"
: sanitizeDownloadReason(queryParsed.data.reason);
if (!previewMode && requiresDownloadReason(attachment) && !downloadReason) {
throw buildValidationError({ reason: "required" });
throw buildBusinessValidationError("payment_batch.download_reason_required", { reason: "required" }, 400);
}
if (attachment.governance_status === "quarantined") {
@@ -2146,7 +2095,7 @@ async function paymentBatchesRoutes(fastify) {
await ensureFeatureEnabled(fastify);
parseId(request.params?.id);
parseId(request.params?.attachmentId, "attachmentId");
throw buildValidationError({ receiptAttachment: "void_required" }, 409);
throw buildBusinessValidationError("payment_batch.receipt_void_required", { receiptAttachment: "void_required" }, 409);
});
fastify.post("/payment-batches/:id/receipt-attachments/:attachmentId/void", { preHandler: fastify.authenticate }, async (request, reply) => {
@@ -2160,7 +2109,7 @@ async function paymentBatchesRoutes(fastify) {
const batch = await lockBatchWithItems(tx, batchId);
await ensureBatchSourceDepartmentsEnabled(fastify, tx, batch);
if (!["reviewed", "failed"].includes(batch.status)) {
throw buildValidationError({ status: "receipt_attachment_not_editable" }, 409);
throw buildBusinessValidationError("payment_batch.receipt_not_editable", { status: "receipt_attachment_not_editable" }, 409);
}
const attachment = await tx.paymentBatchReceiptAttachment.findFirst({
where: { id: attachmentId, payment_batch_id: batchId },
@@ -2169,7 +2118,7 @@ async function paymentBatchesRoutes(fastify) {
throw buildNotFoundError();
}
if (attachment.archived_at) {
throw buildValidationError({ receiptAttachment: "already_voided" }, 409);
throw buildBusinessValidationError("payment_batch.receipt_already_voided", { receiptAttachment: "already_voided" }, 409);
}
const voidedAt = new Date();
@@ -2212,14 +2161,14 @@ async function paymentBatchesRoutes(fastify) {
const batch = await lockBatchWithItems(tx, batchId);
await ensureBatchSourceDepartmentsEnabled(fastify, tx, batch);
if (!["reviewed", "failed"].includes(batch.status)) {
throw buildValidationError({ status: "payment_metadata_not_editable" }, 409);
throw buildBusinessValidationError("payment_batch.metadata_not_editable", { status: "payment_metadata_not_editable" }, 409);
}
const item = batch.items.find((entry) => entry.id === itemId);
if (!item) {
throw buildNotFoundError();
}
if (!["pending", "failed"].includes(item.status)) {
throw buildValidationError({ item: "only_open_item_editable" }, 409);
throw buildBusinessValidationError("payment_batch.item_not_editable", { item: "only_open_item_editable" }, 409);
}
const before = {
@@ -2276,16 +2225,16 @@ async function paymentBatchesRoutes(fastify) {
const isFirstReview = batch.status === "draft" && !batch.reviewed_at;
if (!isFirstReview) {
throw buildValidationError({ review: "batch_not_reviewable" }, 409);
throw buildBusinessValidationError("payment_batch.batch_not_reviewable", { review: "batch_not_reviewable" }, 409);
}
if (batch.items.some((item) => item.status !== "pending")) {
throw buildValidationError({ items: "all_items_must_be_pending" }, 409);
throw buildBusinessValidationError("payment_batch.items_not_pending", { items: "all_items_must_be_pending" }, 409);
}
if (!batch.created_by) {
throw buildValidationError({ review: "maker_identity_missing" }, 409);
throw buildBusinessValidationError("payment_batch.maker_required", { review: "maker_identity_missing" }, 409);
}
if (batch.created_by === request.employee.id) {
throw buildValidationError({ review: "maker_checker_conflict" }, 409);
throw buildBusinessValidationError("payment_batch.maker_checker_conflict", { review: "maker_checker_conflict" }, 409);
}
const openRiskFlags = await tx.entityRiskFlag.findMany({
where: {
@@ -2297,7 +2246,7 @@ async function paymentBatchesRoutes(fastify) {
const resolveIds = new Set(body.resolveRiskFlagIds ?? []);
const unresolved = openRiskFlags.filter((item) => !resolveIds.has(item.id));
if (unresolved.length) {
throw buildValidationError({ resolveRiskFlagIds: "open_risk_flags_must_be_resolved" }, 409);
throw buildBusinessValidationError("payment_batch.risk_resolution_required", { resolveRiskFlagIds: "open_risk_flags_must_be_resolved" }, 409);
}
if (openRiskFlags.length) {
@@ -2377,10 +2326,10 @@ async function paymentBatchesRoutes(fastify) {
const batch = await lockBatchWithItems(tx, batchId);
const batchRisk = summarizeBatchRisk(batch);
if (!["reviewed", "failed"].includes(batch.status)) {
throw buildValidationError({ status: "only_reviewed_or_failed_executable" }, 409);
throw buildBusinessValidationError("payment_batch.batch_not_executable", { status: "only_reviewed_or_failed_executable" }, 409);
}
if (batchRisk.reviewRequirement.completedReviewCount < batchRisk.reviewRequirement.requiredReviewCount) {
throw buildValidationError({ review: "batch_review_required" }, 409);
throw buildBusinessValidationError("payment_batch.review_required", { review: "batch_review_required" }, 409);
}
await lockPaymentEntities(
tx,
@@ -2448,6 +2397,7 @@ async function paymentBatchesRoutes(fastify) {
}
const failureCode = error.code;
const failureMessage = error.message;
const failureReason = ERROR_REASONS.find((reason) => reason.code === error.details?.reasonCode);
await tx.paymentBatchItem.update({
where: { id: item.id },
data: {
@@ -2457,21 +2407,26 @@ async function paymentBatchesRoutes(fastify) {
},
});
finalItemStatuses.set(item.id, "failed");
await ensureRiskFlag(tx, {
const riskDetail = failureReason?.defaultMessage ?? "付款登记未完成,请核对单据状态和付款资料后重试,仍无法完成请联系管理员。";
const riskPayload = {
batchId: batch.id, itemId: item.id, failureCode,
...(failureReason ? { reasonCode: failureReason.code } : {}),
};
const riskFlag = await ensureRiskFlag(tx, {
entityType: batch.entity_type,
entityId: item.entity_id,
riskCode: "payment_batch_execute_failed",
level: "high",
title: "支付批次执行失败",
detail: `${item.entity_code} 执行失败:${failureMessage}`,
detail: riskDetail,
source: "payment_batch",
actorId: request.employee.id,
payloadJson: {
batchId: batch.id,
itemId: item.id,
failureCode,
},
payloadJson: riskPayload,
});
if (riskFlag.detail !== riskDetail || riskFlag.payload_json?.batchId !== batch.id
|| riskFlag.payload_json?.itemId !== item.id || riskFlag.payload_json?.reasonCode !== failureReason?.code) {
await tx.entityRiskFlag.update({ where: { id: riskFlag.id }, data: { detail: riskDetail, payload_json: riskPayload } });
}
attemptFailedCount += 1;
}
}
@@ -2552,7 +2507,7 @@ async function paymentBatchesRoutes(fastify) {
items: batch.items.filter((item) => ["pending", "failed"].includes(item.status)),
});
if (["executed", "cancelled"].includes(batch.status)) {
throw buildValidationError({ status: "batch_not_cancellable" }, 409);
throw buildBusinessValidationError("payment_batch.batch_not_cancellable", { status: "batch_not_cancellable" }, 409);
}
const openItemIds = batch.items
.filter((item) => ["pending", "failed"].includes(item.status))
+2
查看文件
@@ -1,4 +1,5 @@
const { z } = require("zod");
const { buildBusinessValidationError } = require("../utils/businessValidationError");
const { AppError } = require("../utils/AppError");
const { buildAuditLogData } = require("../utils/audit");
@@ -2400,6 +2401,7 @@ async function purchaseRequestRoutes(fastify, options = {}) {
throw buildValidationError({ file: "multipart_limit_exceeded" });
}
if (error instanceof PurchaseAttachmentPolicyError) {
if (error.reasonCode === "unsupported_type") throw buildBusinessValidationError("purchase_attachment.unsupported_type", error.details ?? { file: error.reasonCode });
throw buildValidationError(error.details ?? { file: error.reasonCode });
}
throw error;
文件差异内容过多而无法显示 加载差异
+10 -17
查看文件
@@ -1,7 +1,7 @@
const { buildBusinessValidationError } = require("../utils/businessValidationError");
const crypto = require("node:crypto");
const path = require("node:path");
const { AppError } = require("../utils/AppError");
const { compactDateString } = require("../utils/date");
const { writeBuffer } = require("./storage");
@@ -17,14 +17,7 @@ const SESSION_ORDER = {
pm: 1,
};
function buildLeaveValidationError(details, statusCode = 422) {
return new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode,
details,
});
}
function normalizeDateOnly(value) {
if (value instanceof Date) {
@@ -35,7 +28,7 @@ function normalizeDateOnly(value) {
return new Date(`${value}T00:00:00.000Z`);
}
throw buildLeaveValidationError({ date: "invalid" }, 400);
throw buildBusinessValidationError("leave.date_invalid", { date: "invalid" }, 400);
}
function formatDateOnly(value) {
@@ -64,7 +57,7 @@ function toHalfDaySlot(dateValue, session) {
const sessionIndex = SESSION_ORDER[session];
if (sessionIndex == null) {
throw buildLeaveValidationError({ session: "invalid" }, 400);
throw buildBusinessValidationError("leave.session_invalid", { session: "invalid" }, 400);
}
const dayIndex = Math.floor(date.getTime() / 86400000);
@@ -76,7 +69,7 @@ function calculateRequestedDays({ startDate, endDate, startSession, endSession }
const endSlot = toHalfDaySlot(endDate, endSession);
if (endSlot < startSlot) {
throw buildLeaveValidationError({ leave: "invalid_range" });
throw buildBusinessValidationError("leave.leave_invalid_range", { leave: "invalid_range" }, 422);
}
return fromHalfDayUnits(endSlot - startSlot + 1);
@@ -186,7 +179,7 @@ function calculateRequestedDaysPreviewByWorkingCalendar({
if (requestedUnits <= 0) {
if (!allowNonWorkingRange) {
throw buildLeaveValidationError({ leave: "non_working_range" });
throw buildBusinessValidationError("leave.leave_non_working_range", { leave: "non_working_range" }, 422);
}
return {
@@ -313,7 +306,7 @@ async function lockEmployeeForLeave(tx, employeeId) {
async function ensureAnnualLeaveGrant(tx, { employeeId, leaveType, actorId, referenceDate = new Date() }) {
const policy = leaveType?.leave_policy;
if (!leaveType || !leaveType.enabled || !policy || !policy.enabled) {
throw buildLeaveValidationError({ leaveTypeId: "missing_active_policy" }, 409);
throw buildBusinessValidationError("leave.leave_type_id_missing_active_policy", { leaveTypeId: "missing_active_policy" }, 409);
}
const { periodStart, periodEnd } = currentLeavePeriod(referenceDate);
@@ -532,7 +525,7 @@ async function consumeReservedLeave(tx, {
const requestedUnits = toHalfDayUnits(requestedDays);
if (reservedUnits < requestedUnits || requestedUnits <= 0) {
throw buildLeaveValidationError({ leave: "missing_reserve_ledger" }, 409);
throw buildBusinessValidationError("leave.leave_missing_reserve_ledger", { leave: "missing_reserve_ledger" }, 409);
}
await createLeaveLedgerEntry(tx, {
@@ -571,13 +564,13 @@ async function persistLeaveAttachment({ config, employeeId, filePart }) {
for await (const chunk of filePart.file) {
fileSize += chunk.length;
if (fileSize > config.maxUploadSizeBytes) {
throw buildLeaveValidationError({ file: "file_too_large" }, 400);
throw buildBusinessValidationError("leave.file_file_too_large", { file: "file_too_large" }, 400);
}
chunks.push(chunk);
}
if (fileSize <= 0) {
throw buildLeaveValidationError({ file: "empty" }, 400);
throw buildBusinessValidationError("leave.file_empty", { file: "empty" }, 400);
}
const buffer = Buffer.concat(chunks);
@@ -0,0 +1,60 @@
const { getErrorReason } = require("../constants/errorReasons");
const { buildBusinessValidationError } = require("../utils/businessValidationError");
function resolvePaymentBatchCreationReadiness({ entityType, entityIds, paymentChannel = "bank", snapshots, departmentReadiness, accountReadiness, reservedEntityIds, reviewerIds }) {
const blockers = [];
const addPaymentReadinessBlocker = (reasonCode, snapshot) => {
getErrorReason(reasonCode);
blockers.push({ reasonCode, ...(snapshot ? { entityId: snapshot.entityId, entityCode: snapshot.entityCode } : {}) });
};
if (entityType !== "reimbursement") addPaymentReadinessBlocker("payment_batch.travel_payment_frozen");
const selected = snapshots.filter((snapshot) => entityIds.includes(snapshot.entityId));
if (!entityIds.length || selected.length !== entityIds.length) addPaymentReadinessBlocker("payment_batch.source_not_found");
const companies = new Set();
for (const snapshot of selected) {
if (snapshot.status !== "approved" || snapshot.snapshotJson?.paidAt) addPaymentReadinessBlocker("payment_batch.source_status_invalid", snapshot);
if (!departmentReadiness.get(snapshot.departmentId ?? null)?.enabled) addPaymentReadinessBlocker("payment_batch.department_unavailable", snapshot);
if (reservedEntityIds.has(snapshot.entityId)) addPaymentReadinessBlocker("payment_batch.active_batch_exists", snapshot);
const payee = snapshot.snapshotJson?.payee;
if (payee?.bankAccountUnavailable) addPaymentReadinessBlocker("payment_batch.payee_account_unavailable", snapshot);
if (paymentChannel !== "bank") continue;
if (!snapshot.legalEntityId) addPaymentReadinessBlocker("payment_batch.legal_entity_required", snapshot);
else companies.add(snapshot.legalEntityId);
if (!payee?.bankInstitutionCode) addPaymentReadinessBlocker("payment_batch.payee_bank_required", snapshot);
else if (payee.bankInstitutionCode !== "CCB") addPaymentReadinessBlocker("payment_batch.payee_bank_unsupported", snapshot);
if (!payee?.bankAccountEncrypted) addPaymentReadinessBlocker("payment_batch.payee_account_required", snapshot);
}
if (paymentChannel === "bank") {
if (companies.size > 1) addPaymentReadinessBlocker("payment_batch.multiple_legal_entities");
for (const companyId of companies) {
const account = accountReadiness.get(companyId);
if (!account?.paymentAccount) addPaymentReadinessBlocker("payment_batch.payment_account_required");
else if (!account.paymentAccountSnapshot) addPaymentReadinessBlocker("payment_batch.payment_account_unavailable");
}
}
if (!reviewerIds.length) addPaymentReadinessBlocker("payment_batch.reviewer_required");
return { ready: blockers.length === 0, blockers };
}
function paymentReadinessError(readiness, departmentReadiness) {
const blocker = readiness.blockers[0];
const detailsByReason = {
"payment_batch.travel_payment_frozen": { entityType: "travel_payment_frozen" },
"payment_batch.source_not_found": { entityIds: "contains_not_found" },
"payment_batch.source_status_invalid": { entity: `${blocker.entityCode}:invalid_pay_status` },
"payment_batch.department_unavailable": { featureFlag: "payment_batch_v1_disabled", reason: [...departmentReadiness.values()].find((value) => !value.enabled)?.gate?.reason ?? "department_not_allowed" },
"payment_batch.active_batch_exists": { entityIds: "contains_active_payment_batch" },
"payment_batch.payee_account_unavailable": { payeeBankAccount: "decrypt_failed" },
"payment_batch.legal_entity_required": { legalEntity: "single_legal_entity_required" },
"payment_batch.multiple_legal_entities": { legalEntity: "single_legal_entity_required" },
"payment_batch.payee_bank_required": { payeeBankAccount: `${blocker.entityCode}:ccb_account_required` },
"payment_batch.payee_account_required": { payeeBankAccount: `${blocker.entityCode}:ccb_account_required` },
"payment_batch.payee_bank_unsupported": { payeeBankAccount: `${blocker.entityCode}:ccb_account_required` },
"payment_batch.payment_account_required": { paymentAccount: "active_ccb_account_required" },
"payment_batch.payment_account_unavailable": { paymentAccount: "decrypt_failed" },
"payment_batch.reviewer_required": { review: "no_eligible_reviewer" },
};
return buildBusinessValidationError(getErrorReason(blocker.reasonCode).code, detailsByReason[blocker.reasonCode], blocker.reasonCode === "payment_batch.source_not_found" ? 404 : 409);
}
module.exports = { resolvePaymentBatchCreationReadiness, paymentReadinessError };
@@ -0,0 +1,14 @@
const { AppError } = require("./AppError");
const { getErrorReason } = require("../constants/errorReasons");
function buildBusinessValidationError(reasonCode, details = {}, statusCode = 400) {
const reason = getErrorReason(reasonCode);
return new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode,
details: { ...details, reasonCode: reason.code },
});
}
module.exports = { buildBusinessValidationError };
@@ -0,0 +1,144 @@
{
"reimbursement.material_preview_limit": "报销材料较多,暂时无法连续预览,请分批查看材料或联系管理员协助。",
"purchase_attachment.unsupported_type": "仅支持 PDF、JPG、PNG、WEBP 文件,请重新选择。",
"reimbursement.invoice_amount_required": "票据金额尚未确认,无法自动生成明细,请先核对票据金额或手动选择明细。",
"reimbursement.expense_type_required": "当前没有可用费用类型,请联系管理员维护后再添加票据。",
"reimbursement.invoice_not_bound": "该票据已不再关联业务单据,请刷新票据列表后确认。",
"reimbursement.invoice_unbind_status_invalid": "报销单当前不能移出票据,请先核实审批和付款状态。",
"reimbursement.invoice_bind_status_invalid": "报销单当前不能添加票据,请刷新并确认单据状态。",
"reimbursement.invoice_owner_mismatch": "票据所属人与报销人不一致,请选择本人对应的票据。",
"reimbursement.invoice_item_required": "请为该票据选择对应的报销明细后再添加。",
"reimbursement.travel_invoice_bind_status_invalid": "差旅订单当前不支持添加票据,请核实订单状态。",
"invoice_verification.too_frequent": "验真触发过于频繁,请稍后重试。",
"invoice_verification.attempts_exceeded": "验真重试次数已达上限,请联系财务人工核验。",
"reimbursement.invalid_input": "报销内容不完整或格式不正确,请核对页面必填项和日期后重试。",
"reimbursement.invalid_uuid": "记录标识已失效,请刷新页面后重新选择。",
"reimbursement.body_empty": "尚未填写修改内容,请补充后再保存。",
"reimbursement.date_to_must_be_after_date_from": "结束日期不能早于开始日期,请调整查询日期范围。",
"reimbursement.amount_must_be_positive": "明细金额必须大于 0。",
"reimbursement.template": "模板中的明细格式已不适用,请联系管理员修正模板后重试。",
"reimbursement.owner_employee_id_invalid_owner": "所选报销人不可用,请重新选择有效员工。",
"reimbursement.total_amount_invalid": "报销合计金额不正确,请核对各项明细金额。",
"reimbursement.total_amount_negative_not_allowed": "报销合计不能为负数,请核对各项明细金额。",
"reimbursement.workflow_missing_approval_rule": "尚未配置适用的报销审批规则,请联系管理员维护后重试。",
"reimbursement.precheck": "报销提交检查未通过,请按页面检查清单逐项处理后重试。",
"reimbursement.template_id_invalid_template": "所选报销模板已失效,请重新选择有效模板。",
"reimbursement.template_id_contains_disabled_expense_type": "模板包含已停用的费用类型,请联系管理员修正模板后重试。",
"reimbursement.linked_travel_request_id_not_found": "所选出差申请已不存在,请刷新后重新选择。",
"reimbursement.linked_travel_request_id_not_approved": "所选出差申请尚未获批,请先完成出差审批。",
"reimbursement.linked_travel_request_id_owner_mismatch": "出差申请与报销人不一致,请选择本人对应的出差申请。",
"reimbursement.linked_travel_request_id_already_linked": "该出差申请已关联其他报销单,请核对已有报销记录。",
"reimbursement.linked_travel_request_id_missing_expense_type_mapping": "该出差项目尚未配置对应费用类型,请联系管理员维护后重试。",
"reimbursement.status_invalid_edit_status": "当前报销单状态不允许编辑,请刷新并确认审批进度。",
"reimbursement.travel_end_date_invalid_date_range": "出差结束日期不能早于开始日期,请调整日期。",
"reimbursement.status_invalid_delete_status": "当前报销单状态不允许删除,请核实审批和付款进度。",
"reimbursement.status_invalid_attachment_edit_status": "当前报销单不能修改附件,请刷新并确认单据状态。",
"reimbursement.file_unsupported_mime_type": "附件类型不支持,请选择允许的图片或文档格式。",
"reimbursement.file_file_too_large": "附件超过大小限制,请压缩或更换文件后重试。",
"reimbursement.file_empty_file": "所选附件为空,请重新选择有效文件。",
"reimbursement.file_too_many_files": "一次最多上传 5 个附件,请减少数量后重试。",
"reimbursement.file_missing": "请先选择需要上传的附件。",
"reimbursement.reason_required": "读取该材料需要填写用途,请补充访问原因。",
"reimbursement.status_invalid_item_edit_status": "当前报销单不能修改明细,请刷新并确认单据状态。",
"reimbursement.expense_type_id_invalid_or_disabled": "所选费用类型已失效或停用,请重新选择。",
"reimbursement.reimbursement_invalid_status_for_invoice_item_link": "当前报销单状态已变化,不能再关联发票,请刷新页面。",
"reimbursement.invoice_invalid_unlinked_item_state": "这张发票的关联状态已变化,请刷新后重试。",
"reimbursement.linked_item_id_item_not_in_reimbursement": "所选明细已不存在或不属于本单,请刷新后重新选择。",
"reimbursement.status_invalid_submit_status": "当前报销单状态不允许提交,请刷新并确认是否已提交。",
"reimbursement.items_at_least_one_required": "请先添加至少 1 条报销明细。",
"reimbursement.items_positive_amount_required": "所有报销明细金额都必须大于 0,请修改后再提交。",
"reimbursement.items_invoice_item_guard_failed": "票据检查未通过,请核对每条明细与发票的关联和金额后重试。",
"reimbursement.travel_summary_incomplete": "出差汇总信息尚未填写完整,请补充页面提示的出差信息后提交。",
"reimbursement.submit_already_in_review": "该报销单已经在审批中,请勿重复提交,可查看审批进度。",
"reimbursement.invoices_bound_invoice_required": "报销单需要有效发票,请先添加并关联发票后提交。",
"reimbursement.budget_exceeded_block_mode": "本次报销超出可用预算,请调整金额或联系预算管理员处理。",
"reimbursement.workflow_missing_definition": "尚未配置可用报销审批流程,请联系管理员维护后重试。",
"reimbursement.workflow_no_actionable_approver": "当前没有可处理该报销的审批人,请联系管理员核实审批配置。",
"reimbursement.status_invalid_withdraw_status": "当前报销单不能撤回,请刷新并核实审批进度。",
"reimbursement.withdraw_no_pending_instance": "当前报销单没有可撤回的审批,请刷新并核实进度。",
"reimbursement.withdraw_already_processed_by_approver": "审批人已处理该报销单,不能撤回,请联系审批人确认后续处理。",
"reimbursement.feature_flag_project_linking_v1_disabled": "项目关联暂未开放,请联系管理员确认处理方式。",
"reimbursement.reimbursement_invalid_status_for_project_link": "当前报销单不能修改关联项目,请刷新并确认单据状态。",
"reimbursement.payment_payment_batch_v1_required": "该报销单须通过付款批次安排付款,请前往付款批次查看或创建付款安排。",
"reimbursement.status_invalid_pay_status": "当前报销单尚不满足付款条件,请核实审批和已有付款记录。",
"reimbursement.payment_already_paid_conflict": "该报销单已有不同的付款记录,请核实原付款,勿重复登记。",
"payment_batch.invalid_identifier": "付款记录标识已失效,请刷新后重新选择。",
"payment_batch.invalid_input": "付款操作内容不完整或格式不正确,请核对页面必填项后重试。",
"payment_batch.bank_receipt_required": "银行付款登记前必须上传有效银行回单,请先补充回单。",
"payment_batch.department_unavailable": "当前单据所属部门暂不能使用付款批次,请联系管理员确认处理方式。",
"payment_batch.source_not_found": "部分来源单据已不存在或不可用,请刷新后重新选择。",
"payment_batch.daily_sequence_exhausted": "今日付款批次编号已用完,请联系管理员处理,勿重复提交。",
"payment_batch.payee_account_unavailable": "收款账户资料暂不可用,请联系管理员检查后重试。",
"payment_batch.source_status_invalid": "来源单据尚未满足付款条件或已经付款,请刷新并核实审批与付款状态。",
"payment_batch.already_paid_conflict": "来源单据已有不同的付款记录,请核实已有记录,勿重复付款。",
"payment_batch.source_amount_changed": "来源单据金额已变化,请核对单据后重新安排付款。",
"payment_batch.settled_transaction_required": "尚未找到已结算的交易,请确认结算记录后再登记付款。",
"payment_batch.receipt_security_failed": "付款回单未通过安全检查,请联系管理员核实并重新上传有效回单。",
"payment_batch.receipt_field_invalid": "请通过回单上传入口重新选择文件。",
"payment_batch.receipt_too_large": "回单文件超过大小限制,请压缩后重试。",
"payment_batch.receipt_too_many": "上传回单数量超过限制,请减少文件数量后重试。",
"payment_batch.receipt_upload_limit": "回单上传超过请求限制,请减少文件数量或大小后重试。",
"payment_batch.receipt_file_invalid": "回单文件不符合上传要求,请核对格式、大小和文件内容后重新上传。",
"payment_batch.receipt_required": "请先选择有效的付款回单文件。",
"payment_batch.travel_payment_frozen": "差旅付款组批已暂停,请联系财务确认处理方式。",
"payment_batch.account_reveal_unavailable": "当前批次不支持查看收款账户,请核实批次类型和状态。",
"payment_batch.payee_snapshot_missing": "历史批次缺少收款资料快照,请联系管理员核实,不要据此重复付款。",
"payment_batch.receipt_not_editable": "当前付款状态不允许修改回单,请刷新并确认批次进度。",
"payment_batch.receipt_voided": "该回单已作废,请选择有效回单。",
"payment_batch.download_reason_required": "下载该回单需要填写用途,请补充下载原因。",
"payment_batch.receipt_void_required": "回单不能直接删除,请使用作废操作并说明原因。",
"payment_batch.receipt_already_voided": "该回单已经作废,请刷新列表,勿重复操作。",
"payment_batch.metadata_not_editable": "当前批次状态不允许修改付款信息,请刷新并核实进度。",
"payment_batch.item_not_editable": "只能修改尚未完成的付款明细,请核实该明细状态。",
"payment_batch.batch_not_reviewable": "当前批次状态不允许复核,请刷新并确认是否已处理。",
"payment_batch.items_not_pending": "部分明细已处理,不能复核该批次,请刷新并核实明细状态。",
"payment_batch.maker_required": "该批次缺少制单人信息,请联系管理员核实后再复核。",
"payment_batch.maker_checker_conflict": "制单人不能复核自己的付款批次,请由另一位有权限的经理或负责人复核。",
"payment_batch.risk_resolution_required": "还有未处理的风险提示,请逐项确认并处理后再复核。",
"payment_batch.batch_not_executable": "只有已复核或待重试的批次才能登记付款,请先确认批次状态。",
"payment_batch.review_required": "该批次尚未完成有效复核,请先由独立复核人完成复核。",
"payment_batch.batch_not_cancellable": "该批次当前不能取消,请核实是否已有付款结果并联系财务处理。",
"leave.invalid_uuid": "记录标识已失效,请刷新页面后重新选择。",
"leave.invalid_input": "提交内容不完整或格式不正确,请核对必填项和日期后重试。",
"leave.date_from_invalid": "开始日期不正确,请重新选择。",
"leave.date_to_invalid": "结束日期不正确,请重新选择。",
"leave.date_to_must_be_after_date_from": "结束日期不能早于开始日期,请调整日期范围。",
"leave.department_id_scope_all_only": "当前查询范围不支持指定部门,请调整查询范围。",
"leave.date_invalid": "日期格式不正确,请重新选择日期。",
"leave.file_only_single_file_supported": "每次只能上传一个证明文件,请重新选择。",
"leave.file_file_too_large": "文件超过上传大小限制,请压缩或更换文件后重试。",
"leave.leave_type_id_missing_active_policy": "当前假种尚未配置有效策略,请联系人事维护后重试。",
"leave.file_required": "该假种需要证明材料,请上传后再提交。",
"leave.file_unsupported_mime_type": "文件类型不支持,请选择允许的证明材料格式。",
"leave.status_invalid_edit_status": "当前单据状态不允许编辑,请刷新并确认审批进度。",
"leave.status_invalid_submit_status": "当前单据状态不允许提交,请刷新并确认是否已提交。",
"leave.submit_already_in_review": "该申请已经在审批中,请勿重复提交,可查看审批进度。",
"leave.leave_conflict_range": "该时段已有请假申请,请调整日期或先处理已有申请。",
"leave.leave_insufficient_balance": "可用假期余额不足,请调整天数或联系人事核实。",
"leave.workflow_missing_definition": "尚未配置可用审批流程,请联系管理员维护后重试。",
"leave.workflow_no_actionable_approver": "当前没有可处理该申请的审批人,请联系管理员核实审批配置。",
"leave.status_invalid_withdraw_status": "当前单据状态不允许撤回,请刷新并确认审批进度。",
"leave.withdraw_no_pending_instance": "当前没有可撤回的审批,请刷新并确认审批进度。",
"leave.withdraw_already_processed_by_approver": "审批人已处理该申请,不能再撤回,请联系审批人确认后续处理。",
"leave.session_invalid": "请选择有效的上午或下午时段。",
"leave.leave_invalid_range": "请假结束时间早于开始时间,请调整请假时段。",
"leave.leave_non_working_range": "所选时段没有可请假的工作日,请调整请假日期。",
"leave.leave_missing_reserve_ledger": "请假额度预留记录缺失,请联系人事核实后处理。",
"leave.file_empty": "所选文件为空,请重新选择有效文件。",
"attendance_correction.invalid_input": "提交内容不完整或格式不正确,请核对必填项和日期后重试。",
"attendance_correction.invalid_uuid": "记录标识已失效,请刷新页面后重新选择。",
"attendance_correction.datetime_invalid": "时间格式不正确,请重新选择发生时间。",
"attendance_correction.feature_flag_attendance_unified_v1_disabled": "考勤修正暂未开放,请联系管理员确认处理方式。",
"attendance_correction.workflow_missing_definition": "尚未配置可用审批流程,请联系管理员维护后重试。",
"attendance_correction.workflow_no_actionable_approver": "当前没有可处理该申请的审批人,请联系管理员核实审批配置。",
"attendance_correction.status_invalid_cancel_status": "当前申请已不能取消,请刷新并确认审批结果。",
"payment_batch.legal_entity_required": "报销人尚未维护所属公司,请联系管理员补充后再安排付款。",
"payment_batch.multiple_legal_entities": "所选单据属于不同公司,请按公司分别创建付款安排。",
"payment_batch.payee_bank_required": "收款人尚未维护开户银行,请联系管理员核实真实开户行后补充。",
"payment_batch.payee_account_required": "收款人尚未维护银行卡,请联系管理员补充真实收款资料。",
"payment_batch.payee_bank_unsupported": "当前银行付款仅支持建行收款账户,请停止本次银行付款并联系财务确认处理方式,不要修改真实开户行。",
"payment_batch.payment_account_required": "所属公司尚无可用的建行付款账户,请联系账户管理员维护后重试。",
"payment_batch.payment_account_unavailable": "公司付款账户暂不可用,请联系管理员检查账户配置后重试。",
"payment_batch.active_batch_exists": "所选单据已进入其他付款安排,请刷新列表并查看已有批次。",
"payment_batch.reviewer_required": "当前没有可复核的经理或负责人,制单人不能复核自己的批次,请联系管理员安排独立复核人。"
}
@@ -182,6 +182,7 @@ describe("invoicePool travel-order bind routes", () => {
expect(response.statusCode).toBe(400);
expect(response.json().error.details).toEqual({
linkedItemId: "required_when_invoice_guard_enabled",
reasonCode: "reimbursement.invoice_item_required",
});
expect(state.invoice.status).toBe("available");
expect(prisma.reimbursementItem.create).not.toHaveBeenCalled();
@@ -356,7 +357,7 @@ describe("invoicePool travel-order bind routes", () => {
});
expect(response.statusCode).toBe(400);
expect(response.json().error.details).toEqual({ linkedItemId: "item_not_in_reimbursement" });
expect(response.json().error.details).toEqual({ linkedItemId: "item_not_in_reimbursement", reasonCode: "reimbursement.linked_item_id_item_not_in_reimbursement" });
await app.close();
});
@@ -315,7 +315,7 @@ describe("invoicePool verification routes", () => {
});
expect(response.statusCode).toBe(409);
expect(response.json().error.details).toEqual({ verification: "too_frequent" });
expect(response.json().error.details).toEqual({ verification: "too_frequent", reasonCode: "invoice_verification.too_frequent" });
expect(tx.invoiceVerificationRecord.create).not.toHaveBeenCalled();
} finally {
vi.useRealTimers();
@@ -433,7 +433,7 @@ describe("invoicePool verification routes", () => {
});
expect(response.statusCode).toBe(409);
expect(response.json().error.details).toEqual({ verification: "attempts_exceeded" });
expect(response.json().error.details).toEqual({ verification: "attempts_exceeded", reasonCode: "invoice_verification.attempts_exceeded" });
expect(tx.invoiceVerificationRecord.create).not.toHaveBeenCalled();
expect(app.boss.send).not.toHaveBeenCalled();
} finally {
@@ -293,6 +293,7 @@ function buildPrisma(options = {}) {
const tx = {
paymentAccount: {
findFirst: vi.fn(async () => ({ ...state.paymentAccount })),
findMany: vi.fn(async ({ where }) => where.legal_entity_id.in.includes(state.paymentAccount.legal_entity_id) && state.paymentAccount.enabled && !state.paymentAccount.archived_at ? [{ ...state.paymentAccount }] : []),
},
paymentBatch: {
findUnique: vi.fn(async ({ where }) => {
@@ -431,12 +432,12 @@ function buildPrisma(options = {}) {
count: vi.fn(async () =>
options.candidateCount ?? (state.reservedEntityIds.includes(state.reimbursement.id) ? 0 : 1),
),
findMany: vi.fn(async ({ select } = {}) => {
findMany: vi.fn(async ({ select, where } = {}) => {
if (select?.owner?.select?.employee_no) {
return state.reservedEntityIds.includes(state.reimbursement.id) ? [] : [{ ...state.reimbursement }];
return [state.reimbursement, ...state.additionalReimbursements].filter((item) => item.status === "approved" && !item.paid_at).map((item) => ({ ...item }));
}
const reimbursements = [state.reimbursement, ...state.additionalReimbursements];
return reimbursements.map((item) => ({ ...item }));
return reimbursements.filter((item) => !where?.id?.in || where.id.in.includes(item.id)).map((item) => ({ ...item }));
}),
findUnique: vi.fn(async ({ where }) => {
if (where.id !== state.reimbursement.id) {
@@ -488,6 +489,7 @@ function buildPrisma(options = {}) {
findFirst: vi.fn(async () => null),
updateMany: vi.fn(async () => ({ count: 0 })),
create: vi.fn(async ({ data }) => ({ id: "risk-1", ...data })),
update: vi.fn(async ({ where, data }) => ({ id: where.id, ...data })),
},
auditLog: {
create: vi.fn(async () => ({ id: "audit-1" })),
@@ -648,6 +650,112 @@ afterAll(() => {
});
describe("payment batch reimbursement routes", () => {
it("preserves internal failure summaries while refreshing safe reason metadata on an existing risk", async () => {
const { app, tx, state } = await buildApp({ reimbursement: { status: "reviewing" } });
const risk = {
id: "existing-risk", entity_type: "reimbursement", entity_id: REIMBURSEMENT_ID,
risk_code: "payment_batch_execute_failed", level: "high", title: "支付批次执行失败",
detail: "旧失败摘要", source: "payment_batch", status: "open",
payload_json: { batchId: "previous-batch", itemId: "previous-item", failureCode: "VALID_001" },
};
tx.entityRiskFlag.findFirst.mockResolvedValue(risk);
tx.entityRiskFlag.findMany.mockImplementation(async () => [risk]);
tx.entityRiskFlag.update.mockImplementation(async ({ data }) => Object.assign(risk, data));
const response = await app.inject({ method: "POST", url: `/api/payment-batches/${BATCH_ID}/execute`, headers: { authorization: "Bearer test" }, payload: {} });
expect(response.statusCode).toBe(200);
expect(state.batch.status).toBe("failed");
expect(state.batch.items[0]).toMatchObject({ failure_code: "VALID_001", failure_message: "参数校验失败" });
expect(risk.status).toBe("open");
expect(risk.payload_json).toEqual({ batchId: BATCH_ID, itemId: ITEM_ID, failureCode: "VALID_001", reasonCode: "payment_batch.source_status_invalid" });
expect(risk.detail).toBe("来源单据尚未满足付款条件或已经付款,请刷新并核实审批与付款状态。");
expect(tx.entityRiskFlag.create).not.toHaveBeenCalled();
expect(response.json().data.item.items[0].openRiskFlags[0].payloadJson.reasonCode).toBe("payment_batch.source_status_invalid");
await app.close();
});
it("projects synthetic 0014 omissions in candidates and preflight without writes, and recomputes after repair", async () => {
const { app, tx, state } = await buildApp();
const completeOwner = { ...state.reimbursement.owner };
state.reimbursement.owner = { ...completeOwner, legal_entity_id: null, bank_institution: null, bank_account_encrypted: null };
const candidates = await app.inject({ method: "GET", url: "/api/payment-batches/candidates", headers: { authorization: "Bearer test" } });
expect(candidates.statusCode).toBe(200);
const expected = ["payment_batch.legal_entity_required", "payment_batch.payee_bank_required", "payment_batch.payee_account_required"];
expect(candidates.json().data.items[0].readiness.blockers.map((blocker) => blocker.reasonCode)).toEqual(expected);
const payload = { entityType: "reimbursement", entityIds: [REIMBURSEMENT_ID], defaultPaymentChannel: "bank" };
const preflight = await app.inject({ method: "POST", url: "/api/payment-batches/readiness", headers: { authorization: "Bearer test" }, payload });
expect(preflight.statusCode).toBe(200);
expect(preflight.json().data.blockers.map((blocker) => blocker.reasonCode)).toEqual(expected);
for (const blocker of preflight.json().data.blockers) expect(Object.keys(blocker)).toEqual(["reasonCode", "entityId", "entityCode"]);
expect(tx.paymentBatch.create).not.toHaveBeenCalled();
expect(tx.paymentBatchItem.create).not.toHaveBeenCalled();
expect(tx.auditLog.create).not.toHaveBeenCalled();
expect(createNotificationsWithRecordsMock).not.toHaveBeenCalled();
state.reimbursement.owner = completeOwner;
const repaired = await app.inject({ method: "POST", url: "/api/payment-batches/readiness", headers: { authorization: "Bearer test" }, payload });
expect(repaired.json().data).toEqual({ ready: true, blockers: [] });
await app.close();
});
it.each([
["source_status_invalid", (state) => { state.reimbursement.status = "paid"; }],
["active_batch_exists", (state) => { state.reservedEntityIds.push(REIMBURSEMENT_ID); }],
["legal_entity_required", (state) => { state.reimbursement.owner.legal_entity_id = null; }],
["payee_bank_unsupported", (state) => { state.reimbursement.owner.bank_institution.code = "OTHER"; }],
["payee_bank_required", (state) => { state.reimbursement.owner.bank_institution = null; }],
["payee_account_required", (state) => { state.reimbursement.owner.bank_account_encrypted = null; }],
["payment_account_required", (state) => { state.paymentAccount.enabled = false; }],
["payment_account_unavailable", (state) => { state.paymentAccount.account_number_encrypted = "synthetic-invalid"; }],
["reviewer_required", (state) => { state.employees = []; }],
])("rechecks %s after locking and refuses all creation side effects", async (reason, change) => {
const { app, tx, state } = await buildApp();
const payload = { entityType: "reimbursement", entityIds: [REIMBURSEMENT_ID], defaultPaymentChannel: "bank" };
const preflight = await app.inject({ method: "POST", url: "/api/payment-batches/readiness", headers: { authorization: "Bearer test" }, payload });
expect(preflight.statusCode).toBe(200);
expect(preflight.json().data).toEqual({ ready: true, blockers: [] });
tx.$queryRaw.mockImplementationOnce(async () => { change(state); return []; });
const response = await app.inject({ method: "POST", url: "/api/payment-batches", headers: { authorization: "Bearer test" }, payload });
expect(response.statusCode).toBe(409);
expect(response.json().error.code).toBe("VALID_001");
expect(response.json().error.details.reasonCode).toBe(`payment_batch.${reason}`);
expect(tx.$queryRaw).toHaveBeenCalledTimes(1);
expect(tx.paymentBatch.create).not.toHaveBeenCalled();
expect(tx.paymentBatchItem.create).not.toHaveBeenCalled();
expect(tx.auditLog.create).not.toHaveBeenCalled();
expect(createNotificationsWithRecordsMock).not.toHaveBeenCalled();
expect(flushNotificationDispatchTasksMock).not.toHaveBeenCalled();
await app.close();
});
it("loads candidate account and reviewer facts in batches and rejects a cross-company combination", async () => {
const secondId = "a1111111-1111-4111-8111-111111111111";
const { app, tx, state } = await buildApp({ additionalReimbursements: [{ id: secondId }] });
state.additionalReimbursements[0].owner.legal_entity_id = "second-company";
tx.paymentAccount.findMany.mockResolvedValue([state.paymentAccount, { ...state.paymentAccount, id: "second-account", legal_entity_id: "second-company" }]);
const candidates = await app.inject({ method: "GET", url: "/api/payment-batches/candidates", headers: { authorization: "Bearer test" } });
expect(candidates.statusCode).toBe(200);
expect(candidates.json().data.items.map((item) => item.readiness)).toEqual([{ ready: true, blockers: [] }, { ready: true, blockers: [] }]);
expect(tx.paymentAccount.findMany).toHaveBeenCalledTimes(1);
expect(tx.employee.findMany).toHaveBeenCalledTimes(1);
expect(tx.paymentBatchItem.findMany).toHaveBeenCalledTimes(1);
const payload = { entityType: "reimbursement", entityIds: [REIMBURSEMENT_ID, secondId], defaultPaymentChannel: "bank" };
const preflight = await app.inject({ method: "POST", url: "/api/payment-batches/readiness", headers: { authorization: "Bearer test" }, payload });
expect(preflight.json().data).toEqual({ ready: false, blockers: [{ reasonCode: "payment_batch.multiple_legal_entities" }] });
const create = await app.inject({ method: "POST", url: "/api/payment-batches", headers: { authorization: "Bearer test" }, payload });
expect(create.statusCode).toBe(409);
expect(create.json().error.details).toEqual({ legalEntity: "single_legal_entity_required", reasonCode: "payment_batch.multiple_legal_entities" });
expect(tx.paymentBatch.create).not.toHaveBeenCalled();
await app.close();
});
it("rejects unexpected preflight fields without touching creation state", async () => {
const { app, tx } = await buildApp();
const response = await app.inject({ method: "POST", url: "/api/payment-batches/readiness", headers: { authorization: "Bearer test" }, payload: { entityType: "reimbursement", entityIds: [REIMBURSEMENT_ID], unexpected: true } });
expect(response.statusCode).toBe(400);
expect(response.json().error.details.reasonCode).toBe("payment_batch.invalid_input");
expect(tx.paymentBatch.create).not.toHaveBeenCalled();
await app.close();
});
it("executes reimbursement batch payment and queues downstream follow-ups", async () => {
createNotificationsWithRecordsMock.mockResolvedValueOnce([
{ id: "notif-1", recipient_employee_id: OWNER_ID },
@@ -838,7 +946,7 @@ describe("payment batch reimbursement routes", () => {
await app.close();
});
it("omits reimbursements already reserved by a non-cancelled payment batch", async () => {
it("keeps reserved candidates visible with the shared blocking reason", async () => {
const { app } = await buildApp({ reservedEntityIds: [REIMBURSEMENT_ID] });
const response = await app.inject({
@@ -848,12 +956,12 @@ describe("payment batch reimbursement routes", () => {
});
expect(response.statusCode).toBe(200);
expect(response.json().data).toMatchObject({ total: 0, items: [] });
expect(response.json().data).toMatchObject({ total: 1, items: [{ readiness: { ready: false, blockers: [{ reasonCode: "payment_batch.active_batch_exists" }] } }] });
await app.close();
});
it("omits reimbursement candidates outside the payment rollout department allowlist", async () => {
it("keeps department-blocked candidates visible but not ready", async () => {
const { app } = await buildApp({ paymentBatchAllowedDepartmentIds: ["dept-2"] });
const response = await app.inject({
@@ -863,7 +971,7 @@ describe("payment batch reimbursement routes", () => {
});
expect(response.statusCode).toBe(200);
expect(response.json().data).toMatchObject({ total: 0, items: [] });
expect(response.json().data).toMatchObject({ total: 1, items: [{ readiness: { ready: false, blockers: [{ reasonCode: "payment_batch.department_unavailable" }] } }] });
await app.close();
});
@@ -1933,7 +2041,7 @@ describe("payment batch reimbursement routes", () => {
headers: { authorization: "Bearer test" },
});
expect(missingReasonResponse.statusCode).toBe(400);
expect(missingReasonResponse.json().error.details).toEqual({ reason: "required" });
expect(missingReasonResponse.json().error.details).toEqual({ reason: "required", reasonCode: "payment_batch.download_reason_required" });
const previewResponse = await app.inject({
method: "GET",
@@ -381,7 +381,7 @@ describe("reimbursement attachment routes", () => {
});
expect(response.statusCode).toBe(409);
expect(response.json().error.details).toEqual({ status: "invalid_attachment_edit_status" });
expect(response.json().error.details).toEqual({ status: "invalid_attachment_edit_status", reasonCode: "reimbursement.status_invalid_attachment_edit_status" });
expect(prisma.reimbursementAttachment.create).not.toHaveBeenCalled();
} finally {
await app.close();
@@ -665,9 +665,9 @@ describe("reimbursement attachment routes", () => {
expect(unauthenticated.statusCode).toBe(401);
expect(missingReason.statusCode).toBe(400);
expect(missingReason.json().error.details).toEqual({ reason: "required" });
expect(missingReason.json().error.details).toEqual({ reason: "required", reasonCode: "reimbursement.reason_required" });
expect(missingBundleReason.statusCode).toBe(400);
expect(missingBundleReason.json().error.details).toEqual({ reason: "required" });
expect(missingBundleReason.json().error.details).toEqual({ reason: "required", reasonCode: "reimbursement.reason_required" });
expect(prisma.auditLog.create).not.toHaveBeenCalled();
} finally {
await app.close();
@@ -317,7 +317,7 @@ describe("reimbursements routes expense standard integration", () => {
const response = await app.inject({ method: "POST", url: `/api/reimbursements/${REIMBURSEMENT_ID}/submit` });
expect(response.statusCode).toBe(400);
expect(response.json().error.details).toEqual({ items: "at_least_one_required" });
expect(response.json().error.details).toEqual({ items: "at_least_one_required", reasonCode: "reimbursement.items_at_least_one_required" });
expect(tx.reimbursementForm.update).not.toHaveBeenCalled();
await app.close();
});
@@ -336,6 +336,7 @@ describe("reimbursements routes expense standard integration", () => {
expect(response.json().error.details).toEqual({
items: "positive_amount_required",
itemIds: ["item-zero"],
reasonCode: "reimbursement.items_positive_amount_required",
});
expect(tx.workflowInstance.create).not.toHaveBeenCalled();
await app.close();
@@ -416,7 +417,7 @@ describe("reimbursements routes expense standard integration", () => {
const response = await app.inject({ method: "POST", url: `/api/reimbursements/${REIMBURSEMENT_ID}/submit` });
expect(response.statusCode).toBe(400);
expect(response.json().error.details).toEqual({ invoices: "bound_invoice_required" });
expect(response.json().error.details).toEqual({ invoices: "bound_invoice_required", reasonCode: "reimbursement.invoices_bound_invoice_required" });
expect(tx.workflowInstance.create).not.toHaveBeenCalled();
await app.close();
});
@@ -0,0 +1,66 @@
const fs = require("node:fs");
const path = require("node:path");
const ts = require("../../../../client/node_modules/typescript");
const expectedMessages = require("../fixtures/error-reason-messages.json");
const { getErrorReason } = require("../../../src/constants/errorReasons");
function loadClientModule(relativePath, cache = new Map()) {
const filename = path.resolve(__dirname, "../../../../client/src", relativePath);
if (cache.has(filename)) return cache.get(filename).exports;
const module = { exports: {} };
cache.set(filename, module);
const output = ts.transpileModule(fs.readFileSync(filename, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
}).outputText;
const localRequire = (specifier) => loadClientModule(path.relative(path.resolve(__dirname, "../../../../client/src"), path.resolve(path.dirname(filename), `${specifier}.ts`)), cache);
new Function("require", "module", "exports", output)(localRequire, module, module.exports);
return module.exports;
}
describe("client business error projection", () => {
const cache = new Map();
const { resolveErrorMessage } = loadClientModule("lib/resolveErrorMessage.ts", cache);
const { ApiError } = loadClientModule("api/types.ts", cache);
const { paymentItemFailureMessage, paymentRiskFlagsForDisplay } = loadClientModule("lib/paymentBatchDisplay.ts", cache);
const fallback = "操作未完成,请核对页面提示;如仍无法继续,请联系管理员协助处理。";
it.each(Object.entries(expectedMessages))("keeps exact registered and displayed content for %s", (reasonCode, expected) => {
expect(getErrorReason(reasonCode).defaultMessage).toBe(expected);
expect(resolveErrorMessage(new ApiError({ status: 409, code: "VALID_001", message: "private payload", details: { reasonCode } }))).toBe(expected);
});
it("shows exact actionable payment reason rather than arbitrary server text", () => {
expect(resolveErrorMessage(new ApiError({ status: 409, code: "VALID_001", message: "private payload", details: { reasonCode: "payment_batch.legal_entity_required" } })))
.toBe("报销人尚未维护所属公司,请联系管理员补充后再安排付款。");
});
it.each([undefined, "unknown.reason", "__proto__", "constructor"])("fails safely for unknown reason %s", (reasonCode) => {
expect(resolveErrorMessage(new ApiError({ status: 400, code: "VALID_001", message: "private payload", details: { reasonCode, internal: "/private/secret" } }))).toBe(fallback);
});
it("adds only a valid diagnostic identifier, never arbitrary request text", () => {
const error = new ApiError({ status: 409, code: "VALID_001", message: "参数校验失败", requestId: "req-test-1" });
expect(resolveErrorMessage(error)).toBe(`${fallback} 排障编号:req-test-1`);
error.requestId = "/private/secret";
expect(resolveErrorMessage(error)).toBe(fallback);
});
it("does not surface generic validation or internal identifiers from plain errors", () => {
expect(resolveErrorMessage(new Error("VALID_001: 参数校验失败"))).toBe(fallback);
expect(resolveErrorMessage(new Error("记录失败 a1111111-1111-4111-8111-111111111111"), "请刷新后重试。")).toBe("请刷新后重试。");
});
it("separates persisted failure text from item and risk-card UI projection", () => {
const item = { id: "item", status: "failed", failureCode: "VALID_001", failureMessage: "参数校验失败 /private/hidden", openRiskFlags: [] };
expect(paymentItemFailureMessage(item, "batch")).toBe(fallback);
const risk = { riskCode: "payment_batch_execute_failed", detail: "参数校验失败 /private/hidden", payloadJson: { batchId: "batch", itemId: "item", reasonCode: "payment_batch.source_status_invalid" } };
item.openRiskFlags = [risk];
const expected = "来源单据尚未满足付款条件或已经付款,请刷新并核实审批与付款状态。";
expect(paymentItemFailureMessage(item, "batch")).toBe(expected);
expect(paymentItemFailureMessage(item, "different-batch")).toBe(fallback);
expect(paymentRiskFlagsForDisplay([risk])[0].detail).toBe(expected);
expect(risk.detail).toBe("参数校验失败 /private/hidden");
risk.payloadJson.reasonCode = "unknown.reason";
expect(paymentRiskFlagsForDisplay([risk])[0].detail).toBe(fallback);
});
});
+2 -2
查看文件
@@ -47,7 +47,7 @@ describe("Leave Service", () => {
startSession: "pm",
endSession: "am",
}),
).toThrow(/参数校验失败/);
).toThrow(expect.objectContaining({ code: "VALID_001", statusCode: 422, details: { leave: "invalid_range", reasonCode: "leave.leave_invalid_range" } }));
});
});
@@ -95,7 +95,7 @@ describe("Leave Service", () => {
endSession: "pm",
calendarDays: [],
}),
).toThrow(/参数校验失败/);
).toThrow(expect.objectContaining({ code: "VALID_001", statusCode: 422, details: { leave: "non_working_range", reasonCode: "leave.leave_non_working_range" } }));
});
});
@@ -0,0 +1,65 @@
const { resolvePaymentBatchCreationReadiness, paymentReadinessError } = require("../../../src/services/paymentBatchReadiness");
function facts() {
return {
entityType: "reimbursement", entityIds: ["synthetic-item"], paymentChannel: "bank",
snapshots: [{ entityId: "synthetic-item", entityCode: "SYN-0014", status: "approved", departmentId: "department", legalEntityId: "company", snapshotJson: { payee: { bankInstitutionCode: "CCB", bankAccountEncrypted: "synthetic-present" } } }],
departmentReadiness: new Map([["department", { enabled: true }]]),
accountReadiness: new Map([["company", { paymentAccount: { id: "account" }, paymentAccountSnapshot: { available: true } }]]),
reservedEntityIds: new Set(), reviewerIds: ["independent-reviewer"],
};
}
describe("shared payment creation readiness", () => {
it("accepts a complete synthetic bank payment without success noise", () => {
expect(resolvePaymentBatchCreationReadiness(facts())).toEqual({ ready: true, blockers: [] });
});
it.each([
["source_not_found", (input) => { input.snapshots = []; }],
["source_status_invalid", (input) => { input.snapshots[0].status = "reviewing"; }],
["source_status_invalid", (input) => { input.snapshots[0].snapshotJson.paidAt = "synthetic-paid"; }],
["department_unavailable", (input) => { input.departmentReadiness.clear(); }],
["active_batch_exists", (input) => { input.reservedEntityIds.add("synthetic-item"); }],
["legal_entity_required", (input) => { input.snapshots[0].legalEntityId = null; }],
["payee_bank_required", (input) => { input.snapshots[0].snapshotJson.payee.bankInstitutionCode = null; }],
["payee_account_required", (input) => { input.snapshots[0].snapshotJson.payee.bankAccountEncrypted = null; }],
["payee_bank_unsupported", (input) => { input.snapshots[0].snapshotJson.payee.bankInstitutionCode = "OTHER"; }],
["payee_account_unavailable", (input) => { input.snapshots[0].snapshotJson.payee.bankAccountUnavailable = true; }],
["payment_account_required", (input) => { input.accountReadiness.clear(); }],
["payment_account_unavailable", (input) => { input.accountReadiness.get("company").paymentAccountSnapshot = null; }],
["reviewer_required", (input) => { input.reviewerIds = []; }],
])("returns precise safe blocker for %s", (reason, change) => {
const input = facts();
change(input);
const result = resolvePaymentBatchCreationReadiness(input);
expect(result.ready).toBe(false);
expect(result.blockers.map((blocker) => blocker.reasonCode)).toEqual([`payment_batch.${reason}`]);
for (const blocker of result.blockers) expect(Object.keys(blocker).every((key) => ["reasonCode", "entityId", "entityCode"].includes(key))).toBe(true);
const error = paymentReadinessError(result, input.departmentReadiness);
expect(error.code).toBe("VALID_001");
expect(error.statusCode).toBe(reason === "source_not_found" ? 404 : 409);
expect(error.details.reasonCode).toBe(`payment_batch.${reason}`);
});
it("recomputes all three synthetic 0014 omissions immediately after correction", () => {
const input = facts();
input.snapshots[0].legalEntityId = null;
input.snapshots[0].snapshotJson.payee = {};
expect(resolvePaymentBatchCreationReadiness(input).blockers.map((blocker) => blocker.reasonCode)).toEqual([
"payment_batch.legal_entity_required", "payment_batch.payee_bank_required", "payment_batch.payee_account_required",
]);
input.snapshots = facts().snapshots;
expect(resolvePaymentBatchCreationReadiness(input)).toEqual({ ready: true, blockers: [] });
});
it("does not infer combination readiness from individually valid rows", () => {
const input = facts();
input.snapshots.push({ ...input.snapshots[0], entityId: "second-item", legalEntityId: "second-company" });
input.accountReadiness.set("second-company", input.accountReadiness.get("company"));
expect(resolvePaymentBatchCreationReadiness({ ...input, entityIds: ["second-item"] }).ready).toBe(true);
expect(resolvePaymentBatchCreationReadiness(input).ready).toBe(true);
input.entityIds.push("second-item");
expect(resolvePaymentBatchCreationReadiness(input)).toEqual({ ready: false, blockers: [{ reasonCode: "payment_batch.multiple_legal_entities" }] });
});
});
@@ -0,0 +1,22 @@
const { ERROR_REASONS, getErrorReason } = require("../../../src/constants/errorReasons");
const { buildBusinessValidationError } = require("../../../src/utils/businessValidationError");
describe("business validation reason contract", () => {
it("preserves status, top-level code and legacy fields without allowing reason override", () => {
const error = buildBusinessValidationError("payment_batch.legal_entity_required", {
legalEntity: "single_legal_entity_required", reasonCode: "not_registered",
}, 409);
expect(error).toMatchObject({
code: "VALID_001", statusCode: 409, message: "参数校验失败",
details: { legalEntity: "single_legal_entity_required", reasonCode: "payment_batch.legal_entity_required" },
});
expect(getErrorReason(error.details.reasonCode).defaultMessage).toBe("报销人尚未维护所属公司,请联系管理员补充后再安排付款。");
});
it("rejects unknown reasons and protects registered metadata", () => {
expect(() => buildBusinessValidationError("payment_batch.typo")).toThrow(TypeError);
expect(new Set(ERROR_REASONS.map((reason) => reason.code)).size).toBe(ERROR_REASONS.length);
expect(Object.isFrozen(ERROR_REASONS)).toBe(true);
expect(ERROR_REASONS.every(Object.isFrozen)).toBe(true);
});
});