From ae134c321dd11084061290af2ae90846e34c83ad Mon Sep 17 00:00:00 2001 From: Willie Date: Mon, 25 May 2026 23:27:56 +0800 Subject: [PATCH] vault backup: 2026-05-25 23:27:56 --- .obsidian/workspace.json | 8 +- .../prepaid/consume-batch-auto-monthly.md | 224 ++++++++++++++++++ .../scenarios/prepaid/consume-meter-bill.md | 179 ++++++++++++++ .../consume-multiple-bills-priority.md | 196 +++++++++++++++ .../prepaid/refund-full-resident-moveout.md | 190 +++++++++++++++ 5 files changed, 793 insertions(+), 4 deletions(-) create mode 100644 prop-acc/scenarios/prepaid/consume-batch-auto-monthly.md create mode 100644 prop-acc/scenarios/prepaid/consume-meter-bill.md create mode 100644 prop-acc/scenarios/prepaid/consume-multiple-bills-priority.md create mode 100644 prop-acc/scenarios/prepaid/refund-full-resident-moveout.md diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index 1141f14..00b30a3 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -197,6 +197,10 @@ }, "active": "b06ed69835363258", "lastOpenFiles": [ + "prop-acc/scenarios/prepaid/refund-full-resident-moveout.md", + "prop-acc/scenarios/prepaid/consume-batch-auto-monthly.md", + "prop-acc/scenarios/prepaid/consume-meter-bill.md", + "prop-acc/scenarios/prepaid/consume-multiple-bills-priority.md", "prop-acc/scenarios/prepaid/consume-monthly-property-bill.md", "prop-acc/scenarios/prepaid/deposit-via-miniapp-pending.md", "prop-acc/scenarios/prepaid/deposit-additional-topup.md", @@ -221,10 +225,6 @@ "prop-acc/scenarios/deposit/force-close-retain.md", "prop-acc/scenarios/deposit/force-close-forfeit.md", "prop-acc/scenarios/deposit/force-close-refund.md", - "prop-acc/scenarios/deposit/close-manual-with-zero-balance.md", - "prop-acc/scenarios/deposit/close-after-zero-balance.md", - "prop-acc/scenarios/deposit/unfreeze-after-mediation.md", - "prop-acc/scenarios/deposit/freeze-during-dispute.md", "prop-acc/scenarios/deposit", "prop-acc/concepts/deposit", "prop-acc/scenarios/adhoc", diff --git a/prop-acc/scenarios/prepaid/consume-batch-auto-monthly.md b/prop-acc/scenarios/prepaid/consume-batch-auto-monthly.md new file mode 100644 index 0000000..34e02c4 --- /dev/null +++ b/prop-acc/scenarios/prepaid/consume-batch-auto-monthly.md @@ -0,0 +1,224 @@ +--- +title: prop-acc · prepaid · 场景 - 月初批量自动抵扣 job(待补) +aliases: + - 月初自动抵扣 + - 批量预存款抵账单 + - consume-batch-auto-monthly + - 场景-月初自动抵扣 +tags: + - 场景 + - prop-acc + - 预存款 + - 消费 + - 待补 +audience: + - 业务人员 + - 财务 + - 产品 +status: 草稿 +sub_feature: prepaid +last_review: 2026-05-25 +code_version: 2026-05-22 +--- + +# 场景:月初批量自动抵扣 job(待补) + +> [!warning] 本场景**代码未实现** +> 当前所有 consume 操作都需要业务人员**手动后台触发**。本文档描述自动 job 落地后的目标态。设计意图详见 [[auto-deduction-design]]。 + +预存款的**产品价值核心**。月初 1 日凌晨,Scheduled job 扫所有 Active + 余额>0 的预存款账户,对每个账户找未付账单,按优先级 ([[consume-multiple-bills-priority]]) 自动抵扣。 + +## 典型情境(目标态) + +> [!example] 真实情境(目标态) +> 2026 年 6 月 1 日 00:30,系统自动跑 `PrepaidAutoDeductionJob`: +> +> - 扫描全平台 5 个社区,共 500 个 Active 预存款账户(余额 > 0) +> - 对每个账户找该业户的未付账单(物业费 + 水电费 + ...) +> - 按 due_at 升序抵扣 +> - 全部完成耗时 ~5 分钟 +> +> 早上 8 点,500 个业户陆续收到推送: +> +> | 业户场景 | 推送内容 | +> |---|---| +> | 余额充足,全抵 | "5 月账单已自动抵扣 ¥1,000,余额 ¥4,000" | +> | 余额部分够 | "已抵 ¥800,水电费 ¥200 余额不足,请充值" | +> | 账户冻结 | (不推送)| + +## 业户视角(目标态) + +### 您会感受到什么 + +- 月初某个早晨突然收到推送 +- 小程序"我的账单"批量翻为 ✅ Paid +- 小程序"我的预存款"流水里多几笔 consume +- 收到对应数量的 Receipt(每张账单一张) +- **完全无感**(理想状态),不需要任何操作 + +### 您要做什么 + +只在以下情况要做: + +| 推送内容 | 您要做 | +|---|---| +| "余额不足,X 账单未付" | 充值 → 业务人员手动补抵 / 等下月自动 job | +| "账户冻结无法抵" | 联系物业了解冻结原因 | +| "所有账单已抵,余额还剩 ¥X" | 不用做 | + +## 业务人员视角 + +### Job 执行前 + +业务人员**不需要任何操作**。Scheduled 配置在系统层,运行无需介入。 + +### Job 执行过程 + +后台监控面板(待开发): + +- 实时进度:已处理 N / 总共 M 账户 +- 实时统计:已抵账单数、抵扣总额、失败数、跳过数 +- 失败告警:任何异常立即推送给 ops + +### Job 执行后 + +业务人员看汇总报告(后台 / 邮件): + +```markdown +# 2026 年 6 月 1 日 PrepaidAutoDeductionJob 报告 + +## 统计 +- 候选账户:500 +- 全抵成功:380(76%) +- 部分抵 / 跳过:80(16%) +- 跨社区拦截:0 +- 账户冻结跳过:8(2%) +- 失败(异常):0(0%) +- 其余(余额=0、无未付账单):32(6%) + +## 资金动作 +- 抵扣总额:¥412,300 +- 涉及账单数:835(平均每户 1.6 张) +- 平均抵扣金额:¥513 +- 最大单户抵扣:¥3,200(陈先生家,水电+物业+电梯) + +## 失败明细 +(无) + +## 跳过明细(需关注) +- 80 户余额不足,合计欠款 ¥45,000 + - 已发推送 + - 业务人员可后续手动追缴 + +## 冻结跳过 +- 8 户冻结中 + - 需业务人员核实是否解冻 +``` + +### 异常介入 + +| 场景 | 业务人员动作 | +|---|---| +| 跳过的余额不足业户 | 联系业户充值 + 后续手动 ConsumeAction | +| 冻结跳过业户 | 核实纠纷 / 风控状态 → [[unfreeze-after-verification|解冻]] | +| Job 失败(系统异常) | 立即联系运维查日志 | +| 某账户重复抵扣(理论上不应该) | 查 transactions 表是否有同一 Bill 被抵两次 → 立即停 job 排查 | + +## 系统流程(目标态) + +```mermaid +sequenceDiagram + participant Scheduler + participant Job[PrepaidAutoDeductionJob] + participant Account[PrepaidAccount] + participant Bill + participant Consume[ConsumeFromPrepaidAccountAction] + participant 监听器 + participant 数据库 + participant 业户 + + Note over Scheduler: 2026-06-01 00:30 触发 + + Scheduler->>Job: dispatch + Job->>数据库: SELECT prepaid_accounts WHERE status=Active AND balance>0 + + loop 每个 account + Job->>数据库: SELECT bills WHERE community_id=? AND resident_id=? AND status='unpaid' ORDER BY due_at, amount + + loop 每个 bill + alt balance >= bill.amount + Job->>Consume: handle(account, bill, bill.amount) + Consume->>Account: consume() + Consume->>Bill: recordPayment() → Paid + Consume->>监听器: 触发 CollectionOrderCompleted + 监听器->>数据库: 建 Receipt + Consume->>数据库: 提交事务 + else 余额不够 + Job->>Job: 跳过,记日志 + end + end + end + + Job->>数据库: 写汇总报告 + Job-->>Scheduler: 完成 + Job->>业户: 批量推送通知 +``` + +## Job 的安全设计(目标态) + +| 风险 | 防御 | +|---|---| +| 同一账户被抵两次(job 重跑) | 每笔 consume 关联 Bill,Bill.status=Paid 后跳过 | +| 跨社区误抵 | `ConsumeFromPrepaidAccountAction` 内置守护(consume 模型方法层) | +| Frozen 账户被抵 | `canOperate()` 守护(模型层) | +| 余额为负 | 事务回滚 + amount ≤ balance 守护 | +| Job 长时间运行影响业务 | 分批 chunk(100 / 批);限制最大并发 | +| Job 半夜失败无人发现 | 失败告警(Slack / 钉钉 / 短信)| +| 业户充值后想立即抵(月中)| 业务人员手动 ConsumeAction(不等下月 job)| + +## 与手动 ConsumeAction 的关系 + +| 维度 | 手动 ConsumeAction | 自动 Job | +|---|---|---| +| 触发 | 业务人员后台点击 | Scheduled(月初)| +| 单次范围 | 1 账户 × 1 账单 | 全平台 × 全部账户 × 全部账单 | +| 业务场景 | 个案、运维、补抵 | 月度默认流程 | +| 通知 | 单笔 Receipt | 批量 Receipt + 汇总推送 | +| 复用代码 | `ConsumeFromPrepaidAccountAction` | **同上**(复用,不重写)| + +## 实施前已记录的待讨论项 + +详见 [[auto-deduction-design]] "待讨论 / 决策" 段。简略列表: + +- 触发频率(月度 / 周度 / 实时) +- 触发时点(月初固定 / 账单生成事件触发) +- 优先级排序(due_at / amount / bill_type 组合) +- 部分抵扣支持 +- 失败通知策略 +- 监控指标 + +## 未来扩展 + +job 落地后,后续可演化: + +| 演化方向 | 价值 | +|---|---| +| **小程序"我的账单"显示"将于 X 月 1 日自动扣"** | 业户预期管理,避免临时余额不足惊讶 | +| **预扣预警**:月底前 7 天扫描"下月账单 > 当前余额"的业户 → 主动提醒充值 | 减少跳过率,提升用户体验 | +| **零余额自动通知**:月初 job 后,余额 = 0 的账户主动推送"请充值" | 提升复购率 | +| **跨账户均衡**(若同业户多社区):未来若放开跨社区抵扣 | 提升资金利用率 | + +## 当前替代(job 实现前) + +- 业务人员**月初批量手动**逐户处理(见 [[consume-monthly-property-bill]] + [[consume-multiple-bills-priority]]) +- 工作量大,容易遗漏 / 顺序错乱 +- **这就是 job 紧迫性的来源** + +## 相关文档 + +- [[auto-deduction-design]] +- [[consume-monthly-property-bill]] +- [[consume-multiple-bills-priority]] +- [[consume-meter-bill]] +- [[consume-via-bill-collection-type]] +- [[audit-low-balance-and-overdue]] diff --git a/prop-acc/scenarios/prepaid/consume-meter-bill.md b/prop-acc/scenarios/prepaid/consume-meter-bill.md new file mode 100644 index 0000000..3826849 --- /dev/null +++ b/prop-acc/scenarios/prepaid/consume-meter-bill.md @@ -0,0 +1,179 @@ +--- +title: prop-acc · prepaid · 场景 - 抵扣计量账单(水电费) +aliases: + - 抵水电费 + - 计量账单抵扣 + - consume-meter-bill + - 场景-预存款抵计量账单 +tags: + - 场景 + - prop-acc + - 预存款 + - 消费 + - 计量 +audience: + - 业户 + - 业务人员 +status: 已发布 +sub_feature: prepaid +last_review: 2026-05-25 +code_version: 2026-05-22 +--- + +# 场景:抵扣计量账单(水电费) + +业户的水表 / 电表 / 燃气表抄表后生成账单(由 Meter 模块出账,详见未来的 `meter/` 子模块文档),业务人员从业户的预存款余额抵扣。流程与 [[consume-monthly-property-bill|物业费抵扣]]**几乎相同**,差异在**账单的 bill_type** 和**金额浮动性**。 + +## 典型情境 + +> [!example] 真实情境 +> 张阿姨家 5 月水表抄表:本月用水 12 吨,@ 4.5 元/吨 = ¥54;电表抄表:用电 280 度,@ 0.6 元/度 = ¥168。两张计量账单合计 ¥222,从张阿姨预存款余额 ¥3,400 抵扣。 + +## 业户视角 + +### 您会感受到什么 + +- 抄表数据通过物业 App / 集抄系统进入系统 +- 几天内出账单(可能合并为"水电费"一张,也可能水、电分开两张) +- 业务人员手动 / 自动从预存款抵扣 +- 收到收据:"水费 ¥54" + "电费 ¥168"(或合并"水电费 ¥222") +- 推送:"5 月水电费 ¥222 已抵扣,余额 ¥3,178" + +### 您要做什么 + +什么都不用。看明白即可。**关键差异**:计量账单**金额每月浮动**(取决于用量),业户应: + +- 用量大时确保余额充足(预存款充值要考虑这部分) +- 异常用量(突然翻倍)应自查(可能漏水 / 老人忘关电器) +- 对账单金额有异议 → 走 [[../adhoc/cancel-amount-error-redo|纠错流程]](见 adhoc 模块,通用) + +## 业务人员视角 + +> [!info] 流程基本同物业费抵扣 +> 看 [[consume-monthly-property-bill]] 完整流程。本场景只补充**计量账单特有**的注意点。 + +### 关键差异:bill_type + +| 字段 | 物业费抵扣 | 计量账单抵扣 | +|---|---|---| +| `Bill.bill_type` | `property_fee` | `meter`(或 `water` / `electricity` / `gas`,看 Bill 模块设计)| +| `CollectionOrder.collection_type` | `Bill` | `Bill`(都是 Bill 视角)| +| `meta.fund_source` | `prepaid` | `prepaid` | +| Receipt 文案 | "物业费 ¥800" | "水费 ¥54" / "电费 ¥168"(看 Bill 的 line items)| + +> 注:具体 bill_type 枚举值看 Bill 模块定义。本文按"计量类"统称。 + +### 关键差异:金额来源 + +物业费金额是**固定的**(合同约定,每月不变)。计量账单金额是**计算出来的**: + +``` +本月用量 = 本月抄表 - 上月抄表 +本月金额 = 用量 × 单价(RatePlan) +``` + +数据流:Meter 抄表 → MeterReading 记录 → Bill 生成(按 RatePlan 计算金额) → 业务人员抵扣。 + +详见 Meter 模块文档(待补)。 + +### 关键差异:可能分单或合单 + +各物业财务习惯不同: + +| 方式 | 优 | 缺 | +|---|---|---| +| **分单**(水、电、燃气各一张 Bill)| 业户能看清单项 | 业务人员要抵多张 | +| **合单**(一张"5月水电费 ¥222")| 操作快 | 业户看不清各项 | + +系统两种都支持,看 Meter / Bill 模块的出账配置。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 集抄系统 + participant Meter + participant Bill + participant 财务 + participant Filament + participant Account[PrepaidAccount] + participant 数据库 + + 集抄系统->>Meter: 推送本月抄表数据 + Meter->>数据库: 写 MeterReading + 计算用量 + Meter->>Bill: 生成 Bill(bill_type=meter, amount=222) + Bill->>数据库: status=Unpaid + + Note over 财务: 几天后业务人员处理 + + 财务->>Filament: ConsumeAction(选水电费 Bill) + Filament->>Account: consume(Bill, 222) + Account->>数据库: 建 CO(type=Bill, meta.fund_source=prepaid) + Account->>数据库: 建 PrepaidTransaction(consume, 3400→3178) + Account->>Bill: recordPayment(222) → Paid + Account->>监听器: 触发 CollectionOrderCompleted + 监听器->>数据库: 建 Receipt("水电费 ¥222") + 财务-->>业户: 推送 + 收据 +``` + +## 流水台账(累计) + +| 流水 | type | amount | balance_before | balance_after | related_bill_id | 备注 | +|---|---|---|---|---|---|---| +| ... | (前面省略)| | | | | | +| N | consume | 800 | 4200 | 3400 | Bill #5月物业费 | 5 月物业费抵扣 | +| **N+1** | **consume** | **222** | **3400** | **3178** | **Bill #5月水电费** | **本场景** | + +## 用量异常的处理 + +> [!warning] 用量翻倍 / 异常巨高如何处理 +> +> **场景**:张阿姨家平时月用水 12 吨,5 月用了 80 吨(翻 7 倍)。原因可能是: +> +> | 原因 | 处理 | +> |---|---| +> | 水管漏水 | 业户自查,联系物业维修;账单按事实承担(可能可申请减免)| +> | 抄表录错 | 走 [[../adhoc/cancel-amount-error-redo]] 流程,反向调整账单 | +> | 集抄系统 bug | 运维介入,重新抄表 / 校准 | +> | 业户家用水设备故障 | 业户自担,可向物业申请协助维修 | +> +> 异常账单**不要直接抵扣** —— 先核实再处理,避免业户余额被错误清空。系统不主动识别"用量异常",由业务人员判断。 + +## 常见问题 + +> [!question] 水电费 Bill 是 Meter 模块生成的,跟其他账单有什么区别? +> 唯一差别在 `bill_type` 字段和金额来源(计算 vs 固定)。对预存款 consume 流程**完全透明** —— `ConsumeFromPrepaidAccountAction` 不区分 bill_type,所有 Bill 一视同仁。 + +> [!question] 业户预存款余额不够付水电费怎么办? +> 同 [[consume-monthly-property-bill]] 处理: +> - 跳过该账单 → 推送业户充值 +> - 业户充值后再抵 +> - 不部分抵(避免半付状态) + +> [!question] 水、电、燃气分开还是合并出账? +> 看 Meter 模块配置 + 物业财务习惯。预存款抵扣端**支持两种**。 + +> [!question] 月底 100+ 户的水电费账单要挨个抵,跟物业费一起 100+ 户,业务人员吃得消吗? +> 同样痛点 —— 等 [[auto-deduction-design|自动 job]] 落地一起解决。job 实现后,**月初一次 job 同时抵扣物业费 + 水电费 + 其他账单**。 + +> [!question] 业户对水电费金额有异议(认为抄表错了)? +> 走 Meter / Bill 模块的纠错流程: +> 1. 业户提报 +> 2. 物业核查抄表数据(物理表 vs 录入数据) +> 3. 错了 → 走 Bill 的 reverse + reissue 流程(详见 Meter / Bill 模块文档) +> 4. 没错 → 沟通业户接受(或走司法纠纷) + +## 异常分支 + +- 物业费抵扣 → [[consume-monthly-property-bill]] +- 多账单一起抵 → [[consume-multiple-bills-priority]] +- 异常用量需 Bill 模块介入 → 见 Meter / Bill 模块(待补) +- 月初批量(未来)→ [[consume-batch-auto-monthly]] + +## 相关文档 + +- [[consume-monthly-property-bill]] +- [[consume-multiple-bills-priority]] +- [[consume-via-bill-collection-type]] +- [[transaction-types]] +- [[auto-deduction-design]] diff --git a/prop-acc/scenarios/prepaid/consume-multiple-bills-priority.md b/prop-acc/scenarios/prepaid/consume-multiple-bills-priority.md new file mode 100644 index 0000000..1d751d4 --- /dev/null +++ b/prop-acc/scenarios/prepaid/consume-multiple-bills-priority.md @@ -0,0 +1,196 @@ +--- +title: prop-acc · prepaid · 场景 - 多个未付账单按 due_at 优先级抵扣 +aliases: + - 多账单抵扣优先级 + - 优先抵最早到期账单 + - consume-multiple-bills-priority + - 场景-多账单优先级抵扣 +tags: + - 场景 + - prop-acc + - 预存款 + - 消费 +audience: + - 业户 + - 业务人员 +status: 已发布 +sub_feature: prepaid +last_review: 2026-05-25 +code_version: 2026-05-22 +--- + +# 场景:多个未付账单按 due_at 优先级抵扣 + +业户某月有**多张未付账单**(物业费、水电费、电梯维护费等),余额需抵多张。**优先抵最早到期的账单**(避免逾期罚款)。本场景描述业务人员的批量抵扣逻辑,也是 [[auto-deduction-design|自动 job]] 的核心算法。 + +## 典型情境 + +> [!example] 真实情境 +> 张阿姨家 5 月有 3 张未付账单: +> +> | 账单 | 金额 | due_at | +> |---|---|---| +> | 5 月物业费 | ¥800 | 5 月 15 日 | +> | 5 月水电费 | ¥1,200 | 5 月 20 日 | +> | Q2 电梯维护费 | ¥300 | 5 月 31 日 | +> +> 合计 ¥2,300,张阿姨账户余额 ¥2,500。**全部能抵**,但顺序很重要: +> +> 1. 物业费(5 月 15 日)— 最早到期,先抵 +> 2. 水电费(5 月 20 日) +> 3. 电梯维护费(5 月 31 日) +> +> 抵完余 ¥200。 + +## 业户视角 + +### 您会感受到什么 + +- 5 月底收到 3 张收据(各账单一张),金额对应原账单 +- 小程序"我的预存款"流水按抵扣时间倒序显示 3 笔 consume +- 小程序"我的账单"3 张全部 ✅ Paid +- 推送通知"5 月 3 张账单已抵扣,余额 ¥200" + +### 您要做什么 + +什么都不用。看明白即可。 + +> [!info] 余额够不够全付决定行为 +> - **够全付**:全部抵,余额剩下的留账户 +> - **不够全付**:按优先级先抵最早到期的,后面的留 Unpaid 状态 → 推送"余额不足,请充值" + +## 业务人员视角 + +### 第 1 步:打开账户 + +后台 → 预存款 → 找到张阿姨账户 → 进 `ViewPrepaidAccount`(balance=2500)。 + +### 第 2 步:逐张抵扣(手动模式) + +> [!warning] 当前没有"一键全抵"按钮 +> 业务人员需要**对每张账单各点一次** `ConsumeAction`。这是 [[auto-deduction-design|自动 job]] 要解决的痛点。 + +**正确顺序**(按 due_at 升序): + +| 步骤 | 选 Bill | 抵 amount | 之后余额 | +|---|---|---|---| +| 1 | 5 月物业费(due 5/15) | 800 | 2500 → 1700 | +| 2 | 5 月水电费(due 5/20) | 1200 | 1700 → 500 | +| 3 | Q2 电梯维护费(due 5/31) | 300 | 500 → 200 | + +每张账单走完整 [[consume-monthly-property-bill]] 流程(Modal → 提交 → 触发监听器 → Receipt)。 + +### 第 3 步:核对结果 + +- 3 张账单全 ✅ Paid +- 账户余额 ¥200 +- 3 张 Receipt 已生成(分别 ¥800 ¥1,200 ¥300) + +## 优先级排序逻辑(自动 job 用) + +未来自动 job 实现后,**按以下顺序排序未付账单**: + +| 排序键 | 升序/降序 | 业务理由 | +|---|---|---| +| 1. `due_at` | 升序 | **最早到期的先抵**(避免逾期产生滞纳金 / 影响信用) | +| 2. `bill_type` | 自定义("物业费" → "水电费" → "其他") | 物业费是核心服务费,优先 | +| 3. `amount` | 升序 | 同优先级下,**小额先抵清**(避免余额不够时多张大账单都半抵)| +| 4. `created_at` | 升序 | 兜底:早建的先 | + +伪代码: + +```python +unpaid_bills = sorted( + bills, + key=lambda b: (b.due_at, BILL_TYPE_ORDER[b.bill_type], b.amount, b.created_at) +) + +balance = account.balance +for bill in unpaid_bills: + if balance <= 0: + break + if balance >= bill.amount: + consume(account, bill, bill.amount) + balance -= bill.amount + else: + # 余额不够全付该账单 → 跳过,等业户充值 + notify(account.resident, "余额不足,无法抵 ¥{bill.amount} 的 {bill.bill_type}") + # 或部分抵(看 Bill 是否支持):consume(account, bill, balance); balance = 0; break +``` + +## 余额不够全付的策略对比 + +假设张阿姨账户余额 ¥1,500,3 张账单合计 ¥2,300: + +| 策略 | 行为 | 结果 | +|---|---|---| +| **全部跳过** | 余额不够任何一笔不动 | 3 张全 Unpaid,余额 ¥1,500 闲置 | +| **按优先级抵到不够为止**(推荐) | 物业费 800 → 余 700;水电费 1200 不够跳过;电梯费 300 余 700 ≥ 300 → 抵 → 余 400 | 物业费 + 电梯费 Paid,水电费 Unpaid,余额 ¥400 | +| **按优先级抵 + 部分抵末张** | 物业费 800 → 余 700;水电费 1200 > 700 → 抵 700 部分 → 水电费余 500;电梯费 300 余 0 跳过 | 物业费 Paid + 水电费部分付 + 电梯费 Unpaid + 余额 0 | + +**推荐第二种**(按优先级抵到不够为止,不做部分抵)。理由: + +- 简单,Bill 模块不用支持部分付 +- 业户看到余额还有钱但有账单未付,会主动充值 +- 避免"抵了一半"的复杂状态 + +**第三种**等 Bill 模块支持部分支付后再考虑。 + +## 系统流程(手动模式 3 笔操作) + +```mermaid +sequenceDiagram + participant 财务 + participant Filament + participant Account + participant 数据库 + + Note over 财务: 余额 2500,3 张未付账单 + + 财务->>Filament: ConsumeAction(物业费 800) + Filament->>Account: consume(物业费, 800) + Account->>数据库: balance 2500→1700, Bill Paid + + 财务->>Filament: ConsumeAction(水电费 1200) + Filament->>Account: consume(水电费, 1200) + Account->>数据库: balance 1700→500, Bill Paid + + 财务->>Filament: ConsumeAction(电梯费 300) + Filament->>Account: consume(电梯费, 300) + Account->>数据库: balance 500→200, Bill Paid + + Note over 数据库: 3 张账单 Paid + 3 张 Receipt + 余额 200 +``` + +## 常见问题 + +> [!question] 业务人员漏抵某张账单怎么办? +> 单纯漏抵 → 后续发现再抵一次。如果业户因此被收滞纳金,物业可走 [[../adhoc/cancel-amount-error-redo]] 之类的补救路径。 + +> [!question] 业务人员抵错优先级(先抵晚到期的)? +> 不影响资金正确性(账户余额扣对了,账单状态更新对了)。**业务上**可能让早到期的账单进入逾期。**预防** = 培训业务人员看 due_at,**根治** = 上自动 job。 + +> [!question] 跨多个业户批量抵扣可以吗? +> 当前不行,只能一个账户一个账户操作。**批量是自动 job 的核心需求**。 + +> [!question] 业务人员选错账单(选了别人的)? +> Modal 的账单下拉**已经过滤同业户 + 同社区**,理论上选不到别人的。除非 UI / 数据有 bug,否则不会发生。 + +> [!question] 部分抵扣场景频繁吗? +> 业务上罕见 —— 业户通常一次充够覆盖几个月。如果某业户经常余额不够,业务人员应主动提醒"建议充够 3 个月"。 + +## 异常分支 + +- 单笔抵扣 → [[consume-monthly-property-bill]] +- 计量类账单(水电费)→ [[consume-meter-bill]] +- 余额不够 → 告知业户充值 +- 月初批量(未来)→ [[consume-batch-auto-monthly]] +- 账户冻结 → [[unfreeze-after-verification]] + +## 相关文档 + +- [[consume-monthly-property-bill]] +- [[consume-meter-bill]] +- [[consume-batch-auto-monthly]] +- [[auto-deduction-design]] +- [[transaction-types]] diff --git a/prop-acc/scenarios/prepaid/refund-full-resident-moveout.md b/prop-acc/scenarios/prepaid/refund-full-resident-moveout.md new file mode 100644 index 0000000..b36564c --- /dev/null +++ b/prop-acc/scenarios/prepaid/refund-full-resident-moveout.md @@ -0,0 +1,190 @@ +--- +title: prop-acc · prepaid · 场景 - 业户搬走全额退余 +aliases: + - 业户搬走退预存款 + - 全额退预存款 + - refund-full-resident-moveout + - 场景-业户搬走退预存款 +tags: + - 场景 + - prop-acc + - 预存款 + - 退款 +audience: + - 业户 + - 业务人员 +status: 已发布 +sub_feature: prepaid +last_review: 2026-05-25 +code_version: 2026-05-22 +--- + +# 场景:业户搬走全额退余 + +业户**搬离社区**(卖房 / 退租 / 不再使用本物业),要把预存款账户余额全额退回。退款后**不自动关账**(prepaid 特性),业务人员**主动**走 [[close-resident-moveout|关账]] 流程。 + +## 典型情境 + +> [!example] 真实情境 +> 刘先生把 12-3-501 房子卖了,下周搬走。他在预存款账户里还有 ¥3,200(本月物业费扣完之后的余),要全额退回。 + +## 业户视角 + +### 第 1 步:告知物业搬走 + +- 跟物业管家说"我下周搬走,把预存款里的钱退给我" +- 提供退款渠道(银行卡 / 微信 / 支付宝) + +### 第 2 步:等退款 + +- 业务人员核对账户余额 +- 操作退款(走线下 + 系统) + +### 第 3 步:收到红字收据 + 退款到账 + +- 红字收据"预付款退款 ¥-3,200" +- 银行 / 微信收到 ¥3,200 + +### 第 4 步:账户被关 + +- 后续物业再发账单(若有)→ 不会自动从这个账户扣(已 Closed) +- 业户在小程序"我的预存款" 显示 "🔒 已关闭" + +## 业务人员视角 + +### 第 1 步:核实业户搬走情况 + +- 房屋已过户(看 community_user_profile 状态) +- 业户已结清其他费用(无未付账单) +- 业户提供退款渠道 + +> [!warning] 注意未付账单 +> 如果业户还有未付账单(物业费 / 水电费等),**先抵扣再退余**: +> - 业户余额 ¥3,200,有未付账单 ¥800 → 先 [[consume-monthly-property-bill|抵 800]],余 ¥2,400 → 再退 ¥2,400 +> - 不要直接退全部 → 否则未付账单仍挂业户身上,变成"搬走后还欠物业钱",催收困难 + +### 第 2 步:打开账户做退款 + +后台 → 预存款 → 找到刘先生账户(Active,balance=3200)→ 进 `ViewPrepaidAccount` → 点 `RefundAction`(标签"退款")。 + +> [!warning] 按钮可见性 +> `RefundAction` 守护:`canOperate() && balance > 0` + Policy `->authorize('refund')`。Frozen / Closed / 零余额账户灰化。 + +Modal 表单: + +| 字段 | 填什么 | +|---|---| +| **退款金额** | ¥3,200(默认带入当前余额)| +| **退款渠道(PaymentChannel)** | 选业户指定回款方式 | +| **退款备注** | 必填,如 "业户搬离,12-3-501 已过户,退预存款全额" | + +### 第 3 步:提交 + +系统调 `RefundFromPrepaidAccountAction`,事务内: + +1. 校验 `canOperate()`(Active only) +2. 校验金额 ≤ 当前余额 +3. 建 `CollectionOrder`(`type=Prepaid`,`actual_amount=-3200` 红字,`Completed`) +4. 调 `PrepaidAccount::refund(3200, ...)`: + - 模型层再校验 `canOperate()`(三层防御之模型层兜底) + - 加 `PrepaidTransaction`(`type=refund`,`amount=3200`,`balance_before=3200`,`balance_after=0`,关联红字 CO) + - 更新 `balance=0` +5. **不自动关账**(prepaid 特性,与 deposit 不同 —— 见 [[account-state-machine]] "零余额不自动关账" 段) +6. 触发 `CollectionOrderCompleted` → Listener 建红字 Receipt"预付款退款 ¥-3,200" + +### 第 4 步:走线下退款 + +- 银行转账:导出回款指令 → 银行办理 +- 微信:在物业微信号上做退款 +- 支付宝:同上 + +### 第 5 步:主动关账([[close-resident-moveout]]) + +退完余额后**账户仍是 Active 状态**,需手动走 `CloseAccountAction` 关掉。这是 prepaid 与 deposit 的关键差异。 + +详见 [[close-resident-moveout]]。 + +### 第 6 步:把红字收据给业户 + +后台找 Receipt → 微信 / 邮件发刘先生。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 业户 + participant 财务 + participant Filament + participant RefundFromPrepaidAccountAction + participant 数据库 + participant 监听器 + + Note over 业户,财务: 业户搬走,余额 3200 + + 财务->>财务: 核实无未付账单(若有,先 consume) + 财务->>Filament: ViewPrepaidAccount → RefundAction(3200) + Filament->>RefundFromPrepaidAccountAction: handle(account, 3200, channel) + RefundFromPrepaidAccountAction->>RefundFromPrepaidAccountAction: canOperate()? Active=true + RefundFromPrepaidAccountAction->>RefundFromPrepaidAccountAction: 3200 ≤ 3200 yes + RefundFromPrepaidAccountAction->>数据库: 开启事务 + RefundFromPrepaidAccountAction->>数据库: 1. 建 CO(Prepaid, -3200 红字, Completed) + RefundFromPrepaidAccountAction->>数据库: 2. account.refund(3200) → balance 3200→0 + RefundFromPrepaidAccountAction->>数据库: 3. balance=0, **status 仍 Active** + RefundFromPrepaidAccountAction->>监听器: 4. 触发 CollectionOrderCompleted + 监听器->>数据库: 5. 建 Receipt("预付款退款 ¥-3,200") + RefundFromPrepaidAccountAction->>数据库: 提交事务 + Filament-->>财务: 成功(注:account 仍 Active,需手动关账) + + Note over 财务: 接着走关账 + 财务->>Filament: CloseAccountAction → status=Closed + 财务-->>业户: 银行/微信退 3200 + 红字收据 +``` + +## 与 deposit 退款的关键差异 + +| 维度 | deposit 退款(refund-full-no-damage) | prepaid 退款(本场景) | +|---|---|---| +| 余额清零后状态 | **自动 Closed** | **仍 Active**(可继续充值)| +| 关账操作 | 不需要 | **需手动 CloseAccountAction** | +| 业务背景 | 装修结束等业务节点 | 业户搬走等长期事件 | +| 是否常见 | 高频(每户装修都做) | 低频(业户搬走才做)| + +## 常见问题 + +> [!question] 为什么 prepaid 不自动关账? +> 详见 [[account-state-machine]] "零余额不自动关账" 段。简言之:预存款账户**一户一账**,频繁开关无意义,业户随时可能继续充值。 + +> [!question] 退完不关账户有什么风险? +> 几乎无风险: +> - 业户搬走后再无消费/充值动作 → 账户保持 0 余额 Active +> - 但**长期闲置 Active 账户**会出现在审计扫描里([[audit-low-balance-and-overdue]] 类似),业务上不专业 +> - 推荐**退完立即关**(走 [[close-resident-moveout]]),清爽 + +> [!question] 业户搬走后又租回来或买回来,关了账户怎么办? +> 当前**一户一账约束阻塞**(unique 不允许重开)。详见 [[one-account-per-resident]] "已知设计 gap"。业务上目前用: +> - 业户重新现金 / 微信付账单(不用预存款) +> - 联系运维特殊处理(罕见) + +> [!question] 退款渠道与充值渠道不同可以吗? +> 可以,看 [[../deposit/refund-with-payment-channel-switch]] 介绍的换渠道逻辑(deposit 模块,逻辑相同)。 + +> [!question] 业户失联但要退预存款怎么办? +> 几个选项: +> - **暂留 Active**:不操作,等业户出现(余额对业户仍可用) +> - **freeze 账户**:走 [[freeze-suspected-fraud|风控冻结]] 流程(reason 改"业户失联待联系") +> - **不可走 ForceClose retain**(prepaid 没有 ForceClose,与 deposit 不同) +> - 长期失联(>2 年)走业务流程(类似 deposit 的 retain,但 prepaid 当前没有内建机制,需运维介入) + +## 异常分支 + +- 退一部分余下继续用 → [[refund-partial-after-consume]] +- 账户 Frozen 想退 → 先 [[unfreeze-after-verification|解冻]] 再退(prepaid 没有 ForceClose) +- 关账步骤 → [[close-resident-moveout]] + +## 相关文档 + +- [[refund-partial-after-consume]] +- [[close-resident-moveout]] +- [[account-state-machine]] +- [[transaction-types]] +- [[consume-monthly-property-bill]]