From 669d3b4400496c95f58828245dfbf645d593e922 Mon Sep 17 00:00:00 2001 From: Willie Date: Tue, 26 May 2026 01:13:17 +0800 Subject: [PATCH] vault backup: 2026-05-26 01:13:17 --- .obsidian/workspace.json | 6 +- .../billing/bulk-delete-batch-mistake.md | 267 +++++++++++++++++ .../billing/exception-partial-payment.md | 279 ++++++++++++++++++ prop-acc/scenarios/billing/void-paid-bill.md | 269 +++++++++++++++++ 4 files changed, 818 insertions(+), 3 deletions(-) create mode 100644 prop-acc/scenarios/billing/bulk-delete-batch-mistake.md create mode 100644 prop-acc/scenarios/billing/exception-partial-payment.md create mode 100644 prop-acc/scenarios/billing/void-paid-bill.md diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index b40c929..c867517 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -197,6 +197,9 @@ }, "active": "849c5ff8936a2b67", "lastOpenFiles": [ + "prop-acc/scenarios/billing/exception-partial-payment.md", + "prop-acc/scenarios/billing/bulk-delete-batch-mistake.md", + "prop-acc/scenarios/billing/void-paid-bill.md", "prop-acc/scenarios/billing/delete-bill-unpaid.md", "prop-acc/scenarios/billing/resume-bill.md", "prop-acc/scenarios/billing/suspend-bill.md", @@ -222,9 +225,6 @@ "prop-acc/scenarios/meter/generate-bill-min-max-cap.md", "prop-acc/scenarios/meter/generate-bill-with-multiplier.md", "prop-acc/scenarios/meter/generate-bill-tiered-pricing.md", - "prop-acc/scenarios/meter/read-with-photo-proof.md", - "prop-acc/scenarios/meter/read-via-iot-remote-source.md", - "prop-acc/scenarios/meter/read-batch-via-excel-import.md", "prop-acc/scenarios/meter", "prop-acc/concepts/meter", "prop-acc/scenarios/prepaid", diff --git a/prop-acc/scenarios/billing/bulk-delete-batch-mistake.md b/prop-acc/scenarios/billing/bulk-delete-batch-mistake.md new file mode 100644 index 0000000..520ea83 --- /dev/null +++ b/prop-acc/scenarios/billing/bulk-delete-batch-mistake.md @@ -0,0 +1,267 @@ +--- +title: prop-acc · billing · 场景 - 批量误建,智能 Modal 三档分类清理 +aliases: + - 批量删除账单 + - BulkDeleteBillsAction 实战 + - 智能批删 + - bulk-delete-batch-mistake + - 场景-批量删除账单 +tags: + - 场景 + - prop-acc + - 账单 + - 删除 + - 批量 +audience: + - 业务人员 + - 财务 +status: 已发布 +sub_feature: billing +last_review: 2026-05-26 +code_version: 2026-05-22 +--- + +# 场景:批量误建,智能 Modal 三档分类清理 + +业务人员**误触发批量生成**(例如点了一次"生成 5 月物业费"后忘了又点了一次,导致每户有两张同样账单),需要清理。走 `BulkDeleteBillsAction` 的智能 Modal,自动按状态分三档处理。 + +## 典型情境 + +> [!example] 真实情境 +> 王主管 5 月 1 日上午点了"生成 5 月物业费"批量(已生成 300 张)。下午接电话被打断,**忘了之前已经生成**,又点了一次 → 系统按 `SkipExisting` 策略**全部跳过**(因为已存在)。 +> +> **但**:王主管 Modal 没看清,选了 `Replace` 策略 → 系统 **作废原 300 张 + 重新生成 300 张** → 同业户有重复账单(状态 Void 的旧账单 + Unpaid 的新账单)。 +> +> 业务人员发现后需清理 300 张 Void 状态的旧账单。 + +或者: + +> [!example] 真实情境(更典型) +> 业务人员手工建了 100 张维修分摊账单,分摊金额算错,需要全部清理重建。 + +## 业务人员视角 + +### 第 1 步:筛选要清理的账单 + +后台 → 账单 → 列表 → 用过滤(按期次 / 状态 / 费用类型 / 生成时间)选出要清理的批。 + +例:5 月物业费 + Void 状态 + 创建时间 today → 300 张 Void 旧账单。 + +### 第 2 步:选中 + +Table 上**勾选**这 300 张(全选 / 多选)。 + +### 第 3 步:触发批量删除 + +顶部 **"批量删除"** 按钮(`BulkDeleteBillsAction`)。 + +> [!warning] 权限守护 +> 需要 `bill.bulkDelete` **独立高敏权限**(`Policy::deleteAny`)。普通业务人员可能没此权限,需主管 / 财务总监操作。 +> +> 详见 [[smart-bulk-delete-design]]"权限设计"段。 + +### 第 4 步:智能 Modal(关键) + +Modal 自动**预检查**所选账单 + 分三档: + +``` +批量删除账单 (选中 300 张) + +预检查统计: + ✅ 可删: 200 张 (Unpaid + 无付款关联) + ⚠️ 需作废: 50 张 (Void 已经 / Partial 已经 = 状态) + ❌ 跳过: 50 张 (Paid 已付 / 其他异常) + +请选择处理模式: + ⚪ 仅删除可删的(200 张物理删,100 张跳过) + ⚫ 删可删 + 作废需作废的(200 删 + 50 作废,50 跳过) + +批量操作原因(必填,审计留痕): +[多行输入框: + 5 月 1 日批量生成时误选 Replace 策略, + 导致 300 张旧账单 Void。现批量清理。 +] + +⚠️ 注意: + - 物理删除不可恢复(但 activitylog 保留 bill_no) + - 作废不可撤销 + - 跳过的账单不动 + +[取消] [确认执行] +``` + +### 第 5 步:选模式 + 提交 + +业务人员看到统计,做决定: + +| 选 | 业务效果 | +|---|---| +| 仅删可删的 | 200 物理删,100 暂留(包括 50 已作废的 + 50 跳过的)| +| 删可删 + 作废需作废 | 200 物理删,50 翻 Void(变作废),50 跳过 | + +填原因 → 提交。 + +### 第 6 步:看执行结果 + +系统返回: + +``` +批量操作完成: +- 物理删除:200 张 +- 作废:50 张(若选 DeleteAndVoid) +- 跳过:50 张 + +详细 bill_no 见 activitylog(本次操作)。 +``` + +## 系统流程 + +```mermaid +sequenceDiagram + participant 业务 + participant Filament + participant Modal + participant Action[BulkDeleteBillsAction 业务层] + participant Activity + participant DB + + 业务->>Filament: BillsList → 选 300 张 → 批量删除 + Filament->>Modal: 预检查(分类三档) + Modal->>Filament: 显示 ✅200 ⚠️50 ❌50 + + 业务->>Modal: 选模式 + 填原因 + 提交 + Modal->>Action: handle(bills, mode, reason, user) + + Action->>Action: 再次分类(防 UI 缓存过期) + + Action->>DB: 开启事务 + + loop 200 可删 + Action->>DB: bill.delete() + end + + alt mode = DeleteAndVoid + loop 50 可作废 + Action->>Action: VoidBillAction.handle(bill, reason, user) + Action->>Activity: log voided(单条) + end + end + + Action->>Activity: log bulk_deleted(总体 + affected_bill_nos 数组) + Action->>DB: 提交 + + Action-->>Filament: 结果 + 通知 + Filament-->>业务: 显示"200 删 / 50 作废 / 50 跳过" +``` + +## activitylog 设计 + +详见 [[smart-bulk-delete-design]]"activitylog 设计"段。 + +**单条 bulk_deleted log**(批量操作只一条): + +```json +{ + "log_name": "default", + "subject_type": null, + "subject_id": null, + "event": "bulk_deleted", + "causer_id": 42, // 王主管 ID + "properties": { + "mode": "DeleteAndVoid", + "reason": "5 月 1 日批量生成时误选 Replace,300 张 Void 清理", + "total_selected": 300, + "deleted_count": 200, + "voided_count": 50, + "blocked_count": 50, + "affected_bill_nos": [ + "B-202605-501-001 [DELETED]", + "B-202605-502-001 [DELETED]", + ... + "B-202605-501-002 [VOIDED]", + ... + "B-202605-501-003 [SKIPPED]", + ... + ] + } +} +``` + +`affected_bill_nos` 是数组,每条标记处理结果。事后可完整还原。 + +**同时,每张被作废的 Bill 还各有一条 voided log**(VoidBillAction 内部触发,subject=bill)。 + +## 业户视角 + +业户**通常不感知**: + +- 被物理删的 200 张:业户未付款,通常未通知 → 无感 +- 被作废的 50 张:业户**可能**收到通知(看物业策略),或无感 +- 跳过的 50 张:不动,不影响 + +**理想**:批量误建及时清理 → 业户全程无感 → 物业体面化解错误。 + +**不理想**:清理前业户已经收到推送 + 已付款 → 跳过(Paid 不可删 / 不可作废)→ 需要走单独的"已付作废 + 退款"流程([[void-paid-bill]])。 + +## 与单删 / 单作废的对比 + +| 维度 | [[delete-bill-unpaid|单删]] | [[void-paid-bill|单作废]] | **智能批删(本)** | +|---|---|---|---| +| 一次操作多少张 | 1 | 1 | **多张(N)** | +| 权限 | `bill.delete` | `bill.void` | **`bill.bulkDelete`(独立高敏)** | +| 处理混合状态 | 无(只删 1 张)| 无 | **✅ 三档分类自动** | +| activitylog | 1 条单条 | 1 条单条 | **1 条总体 + N 条单条** | +| 业务人员效率 | 低(逐张)| 低 | **高(一次)** | +| 业务人员风险 | 低 | 低 | **中(影响面大)** | + +## 常见问题 + +> [!question] 批删的"跳过"档具体包括哪些? +> `canBeDeleted=false && canBeVoided=false`: +> +> - **Paid**(已付清,无法走任何"消除"路径,需走专门退款流程) +> - **Void**(已经作废,无需重复) +> - **Processing**(罕见,处理中,等回调) +> +> 跳过的账单不动,业务人员需单独处理。 + +> [!question] 实际执行时与 Modal 预检查不一致(其他人在中间改了)? +> Action 内部**再次分类**(防 UI 缓存过期),实际按最新状态分。可能与 Modal 显示统计**略有差异**(但极少)。 + +> [!question] 批删后想撤销? +> - **物理删的**:不可恢复(数据没了) +> - **作废的**:不可撤销(Void 终态) +> - 唯一办法:重新创建(走 [[create-periodic-property-fee]] 或 [[create-single-bill-manual]]) +> +> 信息可从 activitylog 还原(bill_no / amount 等)。 + +> [!question] 批删的影响面太大会有审批吗? +> 系统层面**无审批流**。靠**权限控制**(只主管 / 财务总监能批删)+ **必填原因**(留审计)+ **Modal 预检查统计**(让操作者看到影响面后决策)。 +> +> 业务上若需"超过 100 张需双签",需扩展加审批 workflow。 + +> [!question] 批删失败怎么办? +> 事务内执行,某张失败 → 整批回滚。事务边界是**全成功或全失败**。 +> +> 例外:`Action` 实现可能优化为"个别失败不影响其他",但破坏原子性 → 通常不推荐。 + +> [!question] activitylog 表太多 bulk_deleted 怎么管? +> 每次批删一条 log(总体 + affected_bill_nos)。即使 100 次批删 = 100 条 log。**不会爆表**。 +> +> 但 voided 的子 log(每张作废一条) = 大量。批删 50 张作废 = 50 条 voided log。归档策略详见 [[smart-bulk-delete-design]]。 + +## 异常分支 + +- 单张删 → [[delete-bill-unpaid]] +- 单张作废 → [[void-paid-bill]] +- 跳过的 Paid 账单要消除 → [[void-paid-bill|Paid 作废 + 退款]](当前手工) +- 批删后想审计 → [[audit-activitylog-trace]] + +## 相关文档 + +- [[smart-bulk-delete-design]](核心概念) +- [[delete-vs-void-dual-track]] +- [[delete-bill-unpaid]] +- [[void-paid-bill]] +- [[bill-six-state-machine]] +- [[audit-activitylog-trace]] diff --git a/prop-acc/scenarios/billing/exception-partial-payment.md b/prop-acc/scenarios/billing/exception-partial-payment.md new file mode 100644 index 0000000..7c4c021 --- /dev/null +++ b/prop-acc/scenarios/billing/exception-partial-payment.md @@ -0,0 +1,279 @@ +--- +title: prop-acc · billing · 场景 - 部分付状态处理(Partial) +aliases: + - 部分付 + - Partial 状态 + - exception-partial-payment + - 场景-部分付账单 +tags: + - 场景 + - prop-acc + - 账单 + - 部分付 +audience: + - 业务人员 + - 财务 +status: 已发布 +sub_feature: billing +last_review: 2026-05-26 +code_version: 2026-05-22 +--- + +# 场景:部分付状态处理(Partial) + +业户**只付了一部分**账单金额,系统状态从 Unpaid 进入 **Partial**(部分付)。后续业务人员需要跟进:**继续催收剩余款** / 提醒业户 / 或视情况调整账单。 + +## 典型情境 + +> [!example] 真实情境 +> 王先生(15-7-203)5 月物业费 ¥800,他到前台说"现在只能付 ¥300,剩下 ¥500 月底再补"。业务人员小李录入收款 ¥300: +> +> - Bill.paid_amount = 300 +> - Bill.status:Unpaid → **Partial** +> - 建 CollectionOrderBill(allocated=300)+ CollectionOrder(+300)+ Receipt(¥300) +> +> 后续:王先生 5 月底再付 ¥500 → Bill.paid_amount = 800 → Bill.status: Partial → Paid。 + +## 业户视角 + +### 部分付场景 + +| 业户原因 | 频率 | +|---|---| +| 现金不够 | 中 | +| 短期资金紧张 | 中 | +| 对账单部分有异议(剩余部分协商中)| 低 | +| 拆分多次付方便记账(罕见)| 低 | +| 业户搬走前部分清账 | 低 | + +### 您会感受到什么 + +- 收据上显示**实付金额**(¥300,不是账单总额 ¥800) +- 推送 / 小程序:"已付 ¥300,剩余 ¥500 待付" +- 余额显示 Partial 状态(账单未完全清) +- 月底前 reminder(若有催收机制) + +### 您要做什么 + +- 在到期日前补齐剩余款 +- 走 [[collect-payment-single|继续收款]](第 2 笔 ¥500) +- 若有困难 → 与物业沟通(协商分期 / 减免 / 挂起) + +## 业务人员视角 + +### 部分付的触发 + +走 [[collect-payment-single|单张收款]] Modal,**手动改"收款金额"小于全额**: + +``` +收款金额:300(改成 300,不是默认的 800) +支付方式:现金 +``` + +提交后: + +- Bill.paid_amount: 0 → 300 +- Bill.status: Unpaid → **Partial** + +### Partial 状态的能力 + +| 操作 | Partial 状态 | +|---|---| +| `CollectPaymentAction`(继续收款)| ✅(canBePaid=true)| +| `SuspendBillAction`(挂起)| ✅ | +| `VoidBillAction`(作废)| ✅(canBeVoided=true,但需配套退款已付部分)| +| `DeleteAction`(物理删)| ❌(canBeDeleted 要求 Unpaid + 无付款)| +| `SplitBillAction`(拆账单)| 可能不允许(已付款拆分复杂,见 [[split-bill]])| + +### 第 2 笔收款 + +业户来补付时: + +1. 找到 Partial 账单 +2. 走 `CollectPaymentAction`(状态守护 canBePaid=true,Partial 也允许) +3. 收款金额 = ¥500(剩余应付,Modal 应默认带入 remaining) +4. 提交 → Bill.paid_amount = 800,status: Partial → Paid + +### 监控 Partial 账单 + +业务人员**定期查看** Partial 状态账单: + +```sql +SELECT bill_no, resident_id, amount, paid_amount, (amount - paid_amount) AS remaining +FROM acc_bills +WHERE status = 'partial' + AND community_id = ? +ORDER BY due_at ASC; +``` + +或后台 → 账单 → 过滤"状态=Partial"列表。 + +业务人员对 Partial 业户: + +- 临近 due_at:发提醒 +- 已逾期:走 [[exception-overdue-bills|催收]] +- 业户失联:走 [[suspend-bill|挂起]] + +## 系统流程 + +```mermaid +sequenceDiagram + participant 业户 + participant 业务 + participant Filament + participant Action[CollectPaymentAction] + participant DB + + 业户->>业务: 我只付 300(账单 800) + 业务->>Filament: ViewBill → CollectPayment(modal,改金额=300) + Filament->>Action: handle(bill, 300, channel) + Action->>DB: 1. 建 CO(+300)+ COBill(allocated=300) + Action->>DB: 2. Bill.paid_amount = 300 + Action->>DB: 3. Bill.status: Unpaid → Partial(因 300 < 800) + + Note over Filament: 几周后业户补付 + + 业户->>业务: 现在付剩下 500 + 业务->>Filament: ViewBill(Partial)→ CollectPayment(默认带 500) + Filament->>Action: handle(bill, 500, channel) + Action->>DB: 4. 建 CO(+500)+ COBill(allocated=500) + Action->>DB: 5. Bill.paid_amount = 800 + Action->>DB: 6. Bill.status: Partial → Paid +``` + +## 数据示例(完整流水) + +业户付 ¥300 后: + +### Bill 表 + +``` +id: 12345 +amount: 800 +paid_amount: 300 +status: Partial +``` + +### CollectionOrderBill(1 条) + +``` +bill_id: 12345 +collection_order_id: 67890 +allocated_amount: 300 +``` + +### CollectionOrder(1 条) + +``` +id: 67890 +actual_amount: +300 +payment_channel: 现金 +status: Completed +``` + +### Receipt(1 条) + +``` +amount: +300 +line_items: [{ 物业费(5月)分付, 300 }] +``` + +业户再付 ¥500 后: + +### Bill 表(更新) + +``` +paid_amount: 800 +status: Paid ← 收齐 +``` + +### CollectionOrderBill(2 条总) + +``` +1: bill_id=12345, collection_order_id=67890, allocated=300 +2: bill_id=12345, collection_order_id=67891, allocated=500 +``` + +业户两次付,流水**完整保留**。 + +## 部分付的几种异常 + +### 异常 1:业户长期不补付 + +业户付了 ¥300 后**消失**,剩余 ¥500 长期不付: + +| 处置 | 路径 | +|---|---| +| 临时不催(可能有困难)| 不动,等业户主动 | +| 走逾期催收 | [[exception-overdue-bills]] | +| 业户失联 | [[suspend-bill|挂起]] | +| 协议放弃剩余 | [[void-paid-bill|作废]](但已付 ¥300 不退,业务上协商决定)| + +### 异常 2:业户付错金额 + +业户原本想付 ¥300 给物业费,误付到了水费: + +- 物业费 Bill 仍 Unpaid +- 水费 Bill 多付了(超过 ¥54) + +处置:看实施细节,可能需手工调整 CollectionOrderBill(危险,破坏审计)。**预防**:Modal 提交前确认 Bill ID。 + +### 异常 3:业务人员录错 paid_amount + +业务人员收 ¥500 现金,误录入 ¥300: + +- Bill.paid_amount = 300(应该 500) +- 物业账面少记 ¥200 → 账上 vs 银行不一致 + +处置:走 [[void-paid-bill|作废]] 原 CollectionOrder + 重录,或运维 tinker 修字段。 + +## Partial 的边界 + +> [!info] 严格部分付 vs 宽松部分付 +> +> 严格实现(本系统倾向): +> - Modal `amount` 校验 `<= remaining` +> - 不允许超付(避免凭空多记 paid_amount) +> +> 宽松实现: +> - 允许超付 → 多余部分转入业户预存款 / 留作"未分配收款" +> +> 当前实现看 `CollectPaymentAction` 代码。 + +## 常见问题 + +> [!question] 多次部分付的 Receipt 是合并还是分开? +> **分开**(每次收款一张 Receipt)。业户拿到的是两张: +> +> - Receipt 1:¥300(5/15 现金付) +> - Receipt 2:¥500(5/30 现金付) +> +> 不合并(合并破坏每次收款的独立凭证)。 + +> [!question] Partial 账单挂起后能恢复继续收吗? +> 可以。走 [[suspend-bill|挂起]] → [[resume-bill|恢复]] → 状态智能判定为 Partial(因有付款)→ 继续收款。 + +> [!question] 部分付能预存款抵吗? +> 看实施。理论上业户预存款余额 ¥200 + 账单剩余 ¥500 = 预存款抵 ¥200 → 仍 Partial(还差 ¥300)。需要部分抵扣功能(见 [[../prepaid/consume-multiple-bills-priority]] 部分抵讨论)。当前不一定支持。 + +> [!question] 业户付的钱比账单总额还多(超付)? +> 见上方"严格 vs 宽松"段。严格实现拒绝;宽松实现允许多付转入预存款。 + +> [!question] activitylog 怎么追踪 Partial 状态历史? +> 每次 CollectPayment 都有 log(event=collected),可看到状态从 Unpaid → Partial 的转变。 + +## 异常分支 + +- 单张全付 → [[collect-payment-single]](标准路径) +- 批量付(多张一次)→ [[collect-payment-batch]] +- 长期 Partial 无解 → [[suspend-bill]] / [[void-paid-bill]] +- 部分付逾期催收 → [[exception-overdue-bills]] + +## 相关文档 + +- [[bill-six-state-machine]] +- [[bill-vs-collection-order]] +- [[collect-payment-single]] +- [[suspend-bill]] +- [[exception-overdue-bills]] +- [[void-paid-bill]] diff --git a/prop-acc/scenarios/billing/void-paid-bill.md b/prop-acc/scenarios/billing/void-paid-bill.md new file mode 100644 index 0000000..cbe6ec0 --- /dev/null +++ b/prop-acc/scenarios/billing/void-paid-bill.md @@ -0,0 +1,269 @@ +--- +title: prop-acc · billing · 场景 - 作废已付账单(走作废 + 退款) +aliases: + - 作废账单 + - VoidBillAction + - void-paid-bill + - 作废加退款 + - 场景-作废已付账单 +tags: + - 场景 + - prop-acc + - 账单 + - 作废 +audience: + - 业务人员 + - 财务 +status: 已发布 +sub_feature: billing +last_review: 2026-05-26 +code_version: 2026-05-22 +--- + +# 场景:作废已付账单(走作废 + 退款) + +业户**已付**的账单需要消除(误开账单业户付了 / 业户事后投诉成功 / 调解结果物业认错)。走**作废 + 退款**组合,留状态留审计 + 退还业户。 + +> [!warning] 当前实施状态 +> `VoidBillAction` 本身**只翻状态 + 留 meta + 写 activitylog**(`canBeVoided=true` for **非 Paid**)。 +> +> **Paid 状态 `canBeVoided=false`**,意味着 `VoidBillAction` 不直接处理 Paid 账单的作废。需要走**类似 meter 的修正流程**: +> +> - 当前手工 / tinker(运维操作) +> - 未来扩展 `VoidPaidBillAction` 自动化(包含退款 + 红字 CO 等) +> +> 详见 [[delete-vs-void-dual-track]]"`canBeVoided` 的微妙之处"段。 + +## 典型情境 + +### 情境 1:Partial 状态作废(系统支持) + +> [!example] 真实情境 +> 陈先生 5 月物业费 ¥800,部分付了 ¥300(状态 Partial)。后续物业承认服务有问题,**双方协议作废账单**,退 ¥300 给业户。 +> +> 业务流程: +> 1. 走 `VoidBillAction`(Partial → Void) +> 2. **手工配套退款**(给陈先生退 ¥300 现金 / 微信) +> 3. (理想)系统自动建红字 CollectionOrder(待扩展) + +### 情境 2:Paid 状态作废(当前需手工) + +> [!example] 真实情境 +> 张阿姨 5 月物业费 ¥800 已付清(Paid)。物业发现金额算错了(应该 ¥600),**多收 ¥200**。要全额作废 + 退还 ¥800 + 重新建一张 ¥600 账单。 +> +> 当前流程(因 canBeVoided=false for Paid,VoidBillAction 不允许): +> 1. **运维 tinker 操作**:把 Bill 状态强制改为 Void + 记 meta +> 2. **手工建红字 CollectionOrder**(¥-800)+ Receipt(红字) +> 3. **物业线下退款** ¥800 给业户 +> 4. **手工建新 Bill** ¥600 +> 5. 业户付 ¥600 +> +> 整个流程**复杂、易出错、无 UI**。issue.md Q6 未明确实施时间,标"待业务方明确"。 + +## 业务人员视角(Partial 作废) + +### 第 1 步:确认作废 + +- 协议作废(业户与物业书面达成) +- 调解 / 司法判决物业方有责 +- 业务上的特殊情况 + +### 第 2 步:打开账单 + +后台 → 账单 → 找到 Partial Bill → 进 `ViewBill`。 + +### 第 3 步:点击 `VoidBillAction`(标签"作废") + +> [!warning] 按钮可见性 +> 守护:`canBeVoided()` = 非 Paid 非 Void + `->authorize('void')`(`bill.void` 权限)。 + +Modal 表单: + +| 字段 | 填什么 | +|---|---| +| **作废原因(reason)** | **必填**,如 "调解结果:物业服务质量有问题,账单作废 + 退还已付款" | + +### 第 4 步:提交 + +`VoidBillAction` 业务逻辑(详见 [[delete-vs-void-dual-track]]): + +```php +$bill->update([ + 'status' => BillStatus::Void, + 'meta' => array_merge($bill->meta ?? [], [ + 'voided_reason' => $reason, + 'voided_at' => now(), + 'voided_by' => $user->id, + ]), +]); + +activity()->performedOn($bill)->causedBy($user) + ->withProperties([ + 'reason' => $reason, + 'from_status' => $bill->getOriginal('status'), + 'to_status' => 'Void', + 'bill_no' => $bill->bill_no, + 'amount' => $bill->amount, + 'paid_amount' => $bill->paid_amount, // 关键:有付款需要后续退 + ]) + ->event('voided') + ->log('账单已作废'); +``` + +### 第 5 步:手工退款(若 paid_amount > 0) + +VoidBillAction **本身不退钱**。业务人员后续: + +1. 看 activitylog 的 `paid_amount` 字段 = ¥300 +2. 联系业户确认退款方式(微信 / 现金 / 银行转账) +3. 走线下退款(物业财务实际打钱) +4. **理想**:建红字 CollectionOrder(`actual_amount=-300`,`type=Bill`,关联到该 Bill)+ Receipt 红字(待扩展自动化) + +> [!info] 当前简化做法 +> Bill 作废后: +> - Bill 状态 = Void +> - CollectionOrderBill 关联**不动**(审计需要) +> - 业务人员**线下手工退款**给业户 +> - 系统中没有红字 CO / 红字 Receipt(待 `VoidPaidBillAction` 自动化) +> +> 财务账面**临时不一致**(Bill 是 Void 但 CO 仍是 Completed),需事后人工对账修复。 +> +> **业务方提需求时,优先级会上来。** + +### 第 6 步:通知业户 + +> 陈先生您好,您的 5 月物业费账单已作废,已付的 ¥300 我们将退还您。请确认收款方式。 + +## 业务人员视角(Paid 作废,当前 tinker 流程) + +详见上方"情境 2"。当前**没自动化**: + +1. 评估业务必要性(确认无误后操作) +2. 联系运维 tinker: + ```php + $bill->update([ + 'status' => BillStatus::Void, + 'meta' => array_merge($bill->meta ?? [], [ + 'voided_reason' => $reason, + 'voided_at' => now(), + 'voided_by' => $user->id, + 'manual_void' => true, // 标记是 tinker 手工作废 + ]), + ]); + ``` +3. 走完整退款流程(线下退 + 系统记一笔红字 CO/Receipt 若可能) +4. 重建账单(走 [[create-single-bill-manual]]) +5. 业户付新账单 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 业务 + participant Filament + participant Action[VoidBillAction] + participant DB + participant Activity + + 业务->>Filament: ViewBill(Partial)→ VoidBillAction + Filament->>Action: handle(bill, reason, user) + Action->>Action: 校验 canBeVoided(非 Paid 非 Void) + Action->>DB: 1. bill.status = Void + meta + Action->>Activity: 2. log(event=voided, paid_amount=300) + Filament-->>业务: 成功 + + 业务->>业务: 看 activitylog paid_amount=300 + 业务->>业户: 线下退款 + 通知 + + Note over Action: 注:不自动建红字 CO/Receipt + Note over Action: 待 VoidPaidBillAction 实现 +``` + +## 业户视角 + +### 您会感受到什么 + +- 收到通知"您的账单 #XXX 已作废,理由 XXX" +- 已付的钱**未来几天**收到退款 +- 收到收据备注(若系统支持) +- 小程序账单状态:Partial → Void + +### 您要做什么 + +- 确认收款方式 +- 接收退款(银行 / 微信 / 现金) +- 如有疑问联系物业 + +## 与 prop-acc 其他作废的对比 + +| 模块 | 作废 / void 机制 | +|---|---| +| **billing(本)** | `VoidBillAction`(非 Paid 非 Void)+ 手工退款配套(Paid 待扩展) | +| deposit | 无单独 void;用 [[../deposit/force-close-refund|ForceClose refund]] 等机制 | +| prepaid | 无 void;走 [[../prepaid/refund-partial-after-consume|refund]] | +| meter | 无 void(reading 不可改,见 [[../meter/exception-readings-locked-after-bill]])| +| adhoc | 走 [[../adhoc/cancel-amount-error-redo|VoidAction]](类似 billing 设计)| + +bill 的 void 是**最完整的"账单作废"设计**,但 Paid 的作废**仍待补**(整个 prop-acc 通用问题:已收款的"反向"流程都不够成熟)。 + +## 已知限制(issue.md Q6 待补) + +- **VoidBillAction 不处理 Paid 状态**:`canBeVoided=false`,需走专门流程(未实现) +- **作废后红字 CO + Receipt 自动生成**:未实现,需手工 +- **退款金额自动算 / 自动建红字凭证**:未实现 +- **BulkRefundBillsAction**(批量退款):未实现,issue.md 标优先级低 + +## 常见问题 + +> [!question] 作废后业户已付款怎么记账? +> 当前(简版): +> - Bill 状态 = Void +> - CollectionOrderBill 关联保留(`allocated_amount=300`) +> - 物业账面"已收 ¥300"(从 CO 角度)= 实际"应退给业户" +> +> 财务月度对账时需**人工识别这种情况**(账上有钱但 Bill 已 void = 待退款)。 +> +> 未来自动化后:作废 + 同时建红字 CO(¥-300)抵消原 CO,账面归零。 + +> [!question] 作废能撤销吗? +> 不能(Void 是终态,详见 [[bill-six-state-machine]])。如需"恢复":新建一张同信息 Bill。 + +> [!question] 已付 + 已被预存款抵的 Bill 作废,怎么退到预存款? +> 自动化未实现。手工流程: +> +> 1. tinker 作废 Bill +> 2. 给业户预存款手工 deposit(走 [[../prepaid/deposit-additional-topup]] 把 ¥800 充回预存款) +> 3. 备注"账单作废退还" +> +> 系统层面:理想是自动反向(`PrepaidAccount::reverseConsume` 之类),未实现。 + +> [!question] activitylog 怎么查作废历史? +> ```sql +> SELECT * FROM activity_log +> WHERE event = 'voided' +> AND properties->>'$.bill_no' = ? +> ORDER BY created_at DESC; +> ``` + +> [!question] 业户对作废结果不满意? +> 已作废不可逆。业户: +> - 走司法 / 仲裁 +> - 业务方重新协商(可能再建新账单) + +## 异常分支 + +- Unpaid 无付款 → [[delete-bill-unpaid|物理删]] 更干净 +- 批量作废 → [[bulk-delete-batch-mistake]] 选 DeleteAndVoid 模式 +- 业户拒绝接受作废 → 协商 / 司法 +- Paid 作废自动化 → 待业务方明确 + 实施 VoidPaidBillAction + +## 相关文档 + +- [[delete-vs-void-dual-track]] +- [[smart-bulk-delete-design]] +- [[bill-six-state-machine]] +- [[delete-bill-unpaid]] +- [[bulk-delete-batch-mistake]] +- [[audit-activitylog-trace]] +- [[../meter/exception-readings-locked-after-bill]](类似 已落账修正) +- [[../adhoc/cancel-amount-error-redo]](adhoc 同类对比)