完善报销通知与付款进度(08-01-reimbursement-notification-progress-clarity)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-01 21:06:31 +08:00
父节点 ca24479a44
当前提交 aa6b9bc373
共修改 23 个文件,包含 730 行新增和 88 行删除
+111 -5
查看文件
@@ -138,9 +138,12 @@ if (!hasPermission(actor, "payment.batch.review")
workflow stages use the actual pending assignee when available. Future stages
keep `actorName=null`; never guess a person from today's role holders.
- A reimbursement approved without a batch makes `payment_batch_review` the
current stage with `detail="待财务创建付款批次"`. A draft batch says
`所在付款批次待非制单人复核`; shared-batch wording must not imply that this
reimbursement alone is being reviewed again.
current stage with `detail="待财务安排付款"`. A draft batch says
`待非制单人复核付款`; employee-facing wording does not expose the internal
batch object or imply that this reimbursement alone is being reviewed again.
- The final stage label is `付款完成`, and its pending/current/failed detail
explicitly says it is a finance registration step rather than an approval.
Internal finance pages may continue to use the precise `付款批次` term.
- Missing workflow, batch or receipt facts degrade only their stages. Earlier
known stages remain visible; missing downstream objects are normal, not 5xx.
- `targetHref` is returned only when the viewer already has one of the existing
@@ -163,7 +166,7 @@ if (!hasPermission(actor, "payment.batch.review")
- Batch `draft|reviewed|executed|failed|cancelled` -> map to explicit current,
completed or exception states without changing the batch.
- Executed payment + zero active receipts -> completed payment with
`付款已完成,回单待留存`; positive count -> `付款与回单已留存`.
`付款已完成,回单待留存`; positive count -> `付款和回单已留存`.
- Viewer lacks batch-read permission -> `targetHref=null`; do not return 403 for
the reimbursement detail itself.
- Database/query failure -> existing 5xx behavior and server logging; do not
@@ -171,7 +174,7 @@ if (!hasPermission(actor, "payment.batch.review")
### 5. Good/Base/Bad Cases
- Good: `财务复核=通过` followed by current `待财务创建付款批次`, so the owner
- Good: `财务复核=通过` followed by current `待财务安排付款`, so the owner
can diagnose the exact responsibility without asking three people.
- Base: a newly submitted reimbursement shows known submission, every real
configured approval task, and the two future payment stages with no guessed
@@ -210,3 +213,106 @@ const approvalStages = workflowTasks.map((task) => buildApprovalStage(task));
approvalStages.push(...buildFutureSnapshotStages(workflowSnapshot, workflowTasks));
return [submittedStage, ...approvalStages, batchReviewStage, paymentStage];
```
## Scenario: Payment Responsibility Notifications
### 1. Scope / Trigger
- Trigger: creating a reimbursement payment batch, completing its maker-checker
review, or extending notifications to another payment responsibility handoff.
- These are action-required notifications, not new workflow approval tasks and
not public business-domain events.
### 2. Signatures
- Notification types: `payment_batch.review_requested` and
`payment_batch.execution_requested`.
- Both use `targetType=payment_batch`, `targetId=<batch id>` and
`targetPath=/payment-batches?id=<batch id>`.
- Both resolve to the existing `reimbursement` IM domain while payment batches
contain only reimbursement entities.
### 3. Contracts
- Batch creation resolves active employees whose roles include `manager|ceo`
and whose effective role permissions include `payment.batch.review`; exclude
the maker and never hard-code a person's name or employee ID.
- Review completion first targets the active maker when that employee still has
`payment.batch.manage`. If unavailable, fall back to other active
`finance|finance_mgr` employees with that permission; always exclude the
reviewer.
- Create station notification records inside the same transaction as the batch
creation/review state change. Dispatch through the shared IM dispatcher only
after commit. A dispatch failure is logged and, once a channel attempt starts,
retained as delivery evidence; it never rolls back the committed payment
action.
- Payment execution keeps the existing `reimbursement.paid` result notification
to the owner/submitter. `付款完成` remains a finance action and must not be
represented as another approval.
- PC and mobile approval histories use the shared
`client/src/lib/approvalDisplay.ts#workflowActionLabel`; `start|submit` both
render as `提交`, and pages must not keep local label maps.
### 4. Validation & Error Matrix
- No eligible non-maker reviewer -> create the batch with zero review
notifications; do not weaken maker-checker or notify the maker.
- Maker inactive or missing `payment.batch.manage` after review -> notify the
eligible finance fallback; no eligible fallback -> review remains committed
with zero execution notifications.
- Maker attempts review or a reviewed batch is reviewed again -> existing
deterministic `409 VALID_001`; zero responsibility notifications and zero IM
dispatch.
- IM disabled, recipient push disabled/unbound, or no matching channel -> station
notification remains; dispatcher returns a fixed skipped reason without
rolling back the payment action.
- Notification record creation fails inside the transaction -> the protected
payment state change fails atomically; no partial handoff is reported.
### 5. Good/Base/Bad Cases
- Good: finance creates one weekly batch, the eligible non-maker reviewer gets
one notification, then successful review returns the action to the active
maker for payment and receipt registration.
- Base: no eligible reviewer is configured; the draft batch remains visible for
diagnosis and no unqualified management broadcast is fabricated.
- Bad: notify every finance/manager account, let the maker review their own
batch, send before transaction commit, or label payment completion as an
approval.
### 6. Tests Required
- Route tests assert qualified reviewer resolution, maker exclusion, active
maker preference, inactive-maker finance fallback and reviewer exclusion.
- Negative tests assert maker conflict, repeated review and duplicate batch
creation produce no notification/dispatch.
- Dispatcher tests assert both types are IM-supported and resolve to the
reimbursement domain; dispatch failure must leave the committed batch state.
- PC/mobile E2E asserts `start` renders `提交` from the shared label map and the
five-stage employee projection ends with `付款复核` / `付款完成(非审批)`.
### 7. Wrong vs Correct
#### Wrong
```js
notifyEmployeesByRole(["manager", "finance"]);
await tx.paymentBatch.update({ data: { status: "reviewed" } });
```
#### Correct
```js
const recipients = await resolvePaymentExecutionRecipientIds(tx, {
makerId: batch.created_by,
reviewerId: actor.id,
});
const notifications = await createNotificationsWithRecords(tx, {
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.paymentBatchExecutionRequested,
targetType: "payment_batch",
targetId: batch.id,
});
// after commit
await flushNotificationDispatchTasks({ tasks: [buildDispatchTask(notifications)] });
```
@@ -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,56 @@
# 报销通知与处理进度简化:设计
## 1. 设计目标
用最少改动恢复真实待办通知,并让员工看到职责清晰的报销全流程。工作流审批、付款批次和付款执行仍是三个独立状态源;页面只做汇总,不把它们合成一个状态机。
## 2. 通知只读诊断
按以下顺序诊断目标财务任务,任何一步都只记录布尔状态、计数、固定结果码和脱敏结论:
1. 从目标报销的最新工作流实例定位主管已完成任务与当前财务任务,不保存内部 ID 到仓库证据。
2. 以当前财务任务为 `target_id` 查询 `workflow.task_assigned` 站内通知,并确认收件人是否为预期财务人员。
3. 若通知不存在,回到主管审批后的下一任务创建/收件人解析路径;若存在,继续查询 delivery 状态与固定 `result_code`。
4. 只核对收件员工 active、`notification_push_enabled`、企微绑定是否存在,及启用渠道能否匹配 workflow 域和财务角色;不读取 userid、渠道 Secret、消息正文或响应原文。
5. 只读结论不足以授权配置修改、补发、重启或生产写入,后续动作单独申请。
## 3. 审批动作标签
- `client/src/lib/approvalDisplay.ts` 的 `workflowActionLabel()` 为唯一共享映射。
- PC `WorkflowTaskDetailPage.tsx` 和移动 `MobileApprovalsPage.tsx` 删除局部副本并导入共享函数。
- 保持未知动作的原值兜底用于诊断,但所有当前受支持动作必须有中文映射;测试覆盖 `start` 与 `submit`。
## 4. 员工进度表达
- 后端处理进度阶段键保持兼容,只把普通详情的标签调整为“付款复核”“付款完成”。
- `付款完成` 当前节点/待处理说明明确“由财务登记,非审批环节”;已完成时继续显示付款人、时间及回单是否留存。
- “查看所在付款批次”只对已有批次读取权限的人显示,普通员工不接触内部批次对象名。
- 五阶段继续使用一条竖向进度线,因为它表达全流程进展;文案区分审批与付款,不另加 Tab、折叠或第二张流程图。
## 5. 付款阶段通知
本节实施以第 2 节诊断结论为前置门:若原链路是投递配置问题,付款阶段只新增复用既有 dispatcher 的发送点;若原链路是通知创建或 side-effect flush 的代码问题,先修复并用既有主管→财务通知证明公共路径,再让两个付款发送点复用同一修复。禁止在根因未定时并行新增后再混合排障。
### 5.1 批次创建后
- 新建批次事务内创建“待付款复核”站内通知,事务提交后走现有 IM dispatcher。
- 收件人由 active 员工的真实角色权限计算:必须有 `payment.batch.review` 且角色包含 `manager` 或 `ceo`,并排除批次创建人;不得写死姓名或向所有管理角色泛发。
- 通知目标是 `payment_batch`,deeplink 进入现有付款页面的目标批次。
### 5.2 批次复核后
- 复核事务内创建“待执行付款”通知,收件人优先为仍 active 且具备 `payment.batch.manage` 的批次创建人;创建人不可用时,才解析其他具备管理权限的 active 财务人员,并排除当前复核人。
- 不把付款动作通知给普通申请人;申请人在批次执行成功后继续收到既有 `reimbursement.paid`。
### 5.3 合同与幂等
- 新增明确的付款待办通知类型,不复用“报销已付款”或“工作流待审批”伪装语义。当前付款批次只开放报销实体,因此通知映射到既有 `reimbursement` domain,复用现有渠道配置;未来若重新开放其他实体付款,再按实体拆分 domain,不在本轮新建需要生产改配的 `payment` domain。
- 通知创建与业务状态变更在同一事务内;投递失败不回滚已完成业务动作,但保留 delivery 失败证据。
- 重复创建/复核请求由既有状态锁拒绝,测试证明不会产生重复通知。
## 6. 兼容与回滚
- 不修改数据库 schema、历史审批快照、既有通知记录或付款批次状态。
- 文案改动可直接回滚;共享映射回滚不会影响服务端事实。
- 付款通知若出现收件范围错误,关闭对应通知类型/代码路径即可,不撤销已经创建或复核的付款批次。
- 本轮不合并财务复核与组批;累计 5–10 张真实单后的用户访谈是重新立项条件,不是当前发布门禁。
@@ -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,49 @@
# 报销通知与处理进度简化:实施计划
## 0. 边界与前置
- [x] 获得最终规划批准后再启动任务;生产只读诊断随本计划获批,任何生产写入、补发、配置修改或重启仍需另行批准。
- [x] 不修改审批链、付款 maker-checker、数据库 schema、历史任务或真实付款数据。
- [x] 生产诊断不输出通知正文、完整账号、企微 userid、内部业务 ID、Secret 或原始响应。
## 1. 生产只读通知诊断
- [x] 定位目标报销最新工作流的当前财务任务,先查 `workflow.task_assigned` 站内通知是否存在。
- [x] 有通知则查 delivery 状态/固定结果码、推送开关、绑定存在性和渠道匹配;无通知则核对下一任务与收件人解析事实。
- [x] 形成只含布尔值、计数和错误分类的结论;需要修生产配置、补发或重启时停止并申请批准。
诊断结论(脱敏):当前财务任务及对应站内通知均已生成,收件人 active、开启推送且存在企微绑定;投递尝试为 0,生产无启用且匹配的企微通知渠道。根因位于投递配置层,不是审批推进或通知创建代码。本任务没有修改生产配置、补发、重启或部署。
## 2. 共享动作标签
- [x] PC 与移动审批页移除局部 `workflowActionLabel`,统一导入共享实现。
- [x] 增加 `start`/`submit` 中文映射回归,检查打印及其他调用方没有第三份重复映射。
## 3. 处理进度文案
- [x] 将员工详情的后两阶段改为“付款复核”“付款完成”。
- [x] 付款完成待处理说明明确“由财务登记,非审批环节”,保留权限门控的批次入口和回单完成状态。
- [x] 更新 PC/移动组件测试,断言五阶段顺序与普通员工不暴露内部批次入口。
## 4. 付款责任人通知
- [x] 以前述只读诊断结论为实施门:先证明公共通知创建/投递路径的根因与修复边界,再确定本节只新增发送点还是必须复用同一代码修复。
- [x] 增加付款复核待办与付款执行待办的通知类型、domain 和脱敏文案。
- [x] 批次创建事务内解析有复核权限的 active manager/ceo,排除创建人并创建通知;提交后复用现有 dispatcher。
- [x] 批次复核事务内通知 active 批次创建人/实际付款责任人,排除复核人;付款执行成功沿用现有申请人通知。
- [x] 覆盖无合格收件人、创建人停用、重复请求、投递失败不回滚业务和普通用户不可读批次等边界。
## 5. 验证与审查
- [x] 运行 workflow/reimbursement/payment-batch 聚焦 Vitest、付款并发与 maker-checker 回归。
- [x] 运行客户端 typecheck/build、相关 PC/移动 Playwright 和动作标签单测。
- [x] 运行 Prisma validate、task/governance/process/sensitive/diff 门禁。
- [x] 因涉及 workflow/付款职责和通知收件范围,实施后执行只读 reviewer 检查,不允许 reviewer 改代码。
- [ ] 部署与公网检查另行批准;未部署前不通知用户可以测试,企微实际到达必须真机确认。
只读审查结论:首次检查发现站内通知中心缺少 `payment_batch` 目标链接,已补 PC/移动统一直达付款页并增加回归;复查未发现阻断项。通知记录与付款动作同事务,IM 投递发生在提交后且失败不回滚;复核人解析排除制单人,付款责任人解析排除复核人;重复创建/复核由既有锁和状态校验拒绝,不重复通知。生产仍无启用且匹配的企微渠道,属于已知部署后配置门,不得表述为企微通知已恢复。
## 6. 运行观察
- [x] 运行约定为每周集中建立一次付款批次,不建设排程功能。
- [ ] 王芝玲累计处理 5–10 张真实报销后触发一次访谈,决定财务复核与加入批次是否值得另立合并任务(未来触发条件,当前尚未发生)。
@@ -0,0 +1,61 @@
# 报销通知与处理进度简化
## Goal
让报销单在 5–10 人公司的真实流程中做到两件事:新的责任人能及时收到待办通知;申请人能看懂“审批”和“付款处理”的区别,不再把四个动作误认为四轮审批。
## Background
- 马建勇已在生产完成杨俊报销的主管审批,但下一节点王芝玲没有收到预期审批通知;王芝玲此前已通过企业微信登录,不能把问题归因于“从未登录”。
- 代码在报销提交和工作流流转时会创建 `workflow.task_assigned`,主管通过后也会生成下一任务并排队通知,因此应先区分“站内通知未生成”和“已生成但企微未投递”。
- 当前员工详情展示“付款批次复核”“付款与回单”,容易被理解为两轮额外审批;实际前者是非制单人付款复核,后者是财务线下付款后的回单与状态登记,不是审批。
- 当前职责基线:马建勇负责主管审批和非制单付款复核;王芝玲负责财务复核、创建付款批次、线下付款、上传回单和登记付款完成。一个付款批次可以覆盖多张报销单。
- `client/src/lib/approvalDisplay.ts` 已把 `start` 映射为“提交”,但 PC `WorkflowTaskDetailPage.tsx` 与移动 `MobileApprovalsPage.tsx` 各自复制了缺少 `start` 的局部映射,导致显示 `start · 杨俊`。
## Scope
- 先做一次获批的生产只读诊断:只核对目标工作流任务、王芝玲对应站内通知是否存在、投递记录状态/固定错误分类、通知开关、企微绑定状态和渠道匹配状态;不读取 Secret、消息正文或原始企微 payload。
- 删除 PC/移动审批页重复的动作标签映射,统一使用共享 `workflowActionLabel()`;`start` 和 `submit` 均显示“提交”。
- 员工报销进度保留五个业务阶段,但改为:申请提交、主管审批、财务复核、付款复核、付款完成;付款完成明确为“财务登记,非审批环节”。
- 保留付款 maker-checker:付款批次创建人与复核人不得相同,当前只需一次非制单复核,不增加更多复核层级。
- 通知以“新的责任人需要行动”为准:提交后通知主管、主管通过后通知财务、创建付款批次后通知非制单复核人、付款复核后通知财务/批次创建人继续付款、付款完成后通知申请人。
- 每周集中建立一次付款批次作为当前运行约定,不在本任务建设定时排程、自动组批或强制付款日。
## Requirements
- 通知诊断必须先判断站内通知是否生成,再进入企微投递层,不能直接假设缺少通知代码或重复创建通知。
- 若本次缺失由生产配置、人员推送开关、企微绑定或渠道配置造成,先给出脱敏根因和最小修复建议;任何生产写入、配置修改、补发消息或重启均需再次批准。
- 新增付款阶段通知时必须复用现有通知、投递和审计服务;通知只给真实需要行动的人,不向所有 finance/manager 角色泛发重复消息。
- 普通员工页面不突出“付款批次”内部对象名;有付款批次权限的人员仍可通过现有权限门控入口查看批次。
- 共享动作标签是唯一事实源;PC、移动和打印页不得继续维护互相漂移的局部映射。
- 不删除主管审批、财务复核或付款复核,不放宽自审,不把付款完成伪装成审批通过。
## Acceptance Criteria
- [ ] 脱敏证据能明确回答本次财务通知属于“未生成”还是“已生成未送达”,并定位到单一失败层或明确下一步,不包含通知正文、账号、Secret 或原始 payload。
- [ ] 马建勇通过主管审批后,王芝玲能够收到对应的新待办站内通知;企微投递恢复需另有真实投递证据后才能标为通过。
- [ ] PC 与移动审批处理历史均显示“提交 · 杨俊”,不再出现原始 `start`;动作标签映射只保留一个共享实现。
- [ ] 普通报销详情依次显示“申请提交、主管审批、财务复核、付款复核、付款完成”,并明确付款完成不是审批。
- [ ] 创建付款批次后通知实际可复核人员;复核通过后通知实际付款责任人;付款完成继续通知申请人,且同一事件不会重复创建/投递。
- [ ] 付款复核仍强制非制单人且仅一轮;既有付款批次、审批快照和历史记录不迁移、不改写。
- [ ] PC/移动相关组件测试、通知路由/服务测试、付款 maker-checker 回归、前端 typecheck/build 与 Trellis 门禁通过。
## Out of Scope
- 不在本任务删除付款批次、付款复核或银行回单要求。
- 不接银行接口,不让 OA 自动执行真实付款。
- 不建设每周付款日排程、自动组批、批次自动复核或定时通知。
- 不在未获批准时修改生产通知渠道、员工推送开关、企微绑定、环境变量或服务状态。
- 不顺带重构全部审批页面或统一所有业务状态标签。
## Deferred Decision With Trigger
- 本轮不合并“财务复核通过”和“加入/创建本周付款批次”。决定性依据是王芝玲尚未实际执行过财务复核,无法判断她会随到随审还是与周付款批次一起处理;提前合并可能迫使她延迟复核或频繁建批次。
- 触发条件:王芝玲累计处理 5–10 张真实报销单后,由负责人直接询问“财务复核与加入付款批次通常一次完成,还是分开完成?”若一次完成,再立小任务评估合并;若随到随审、定期组批,则永久保持两个动作分离。
## Test Plan
- 聚焦测试动作标签、下一审批任务通知、付款批次创建/复核/执行通知和幂等性。
- 运行相关 server Vitest、付款并发/maker-checker 回归、客户端 typecheck/build 与相关 Playwright。
- 运行 task validate、Trellis governance、process contract、sensitive-content scan 与 `git diff --check`。
- 生产只读诊断和任何后续真机通知验证分开记录;没有实际企微到达证据时不得宣称通知已恢复。
@@ -0,0 +1,26 @@
{
"id": "reimbursement-notification-progress-clarity",
"name": "reimbursement-notification-progress-clarity",
"title": "报销通知与处理进度简化",
"description": "诊断报销审批通知投递,统一审批动作中文映射,并按小团队实际职责简化付款阶段展示。",
"status": "in_progress",
"dev_type": null,
"scope": "报销审批通知只读诊断、审批动作标签去重、付款阶段文案与必要通知;不削弱付款 maker-checker",
"package": null,
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-08-01",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}