From 68ea6dfbe980f6fc52fe37c1374220d8eb54a504 Mon Sep 17 00:00:00 2001 From: Willie Date: Mon, 25 May 2026 22:32:40 +0800 Subject: [PATCH] vault backup: 2026-05-25 22:32:40 --- .obsidian/workspace.json | 10 +- .../deposit/close-after-zero-balance.md | 165 +++++++++++++++ .../deposit/close-manual-with-zero-balance.md | 142 +++++++++++++ .../scenarios/deposit/force-close-refund.md | 188 ++++++++++++++++++ .../deposit/freeze-during-dispute.md | 163 +++++++++++++++ .../deposit/unfreeze-after-mediation.md | 170 ++++++++++++++++ 6 files changed, 833 insertions(+), 5 deletions(-) create mode 100644 prop-acc/scenarios/deposit/close-after-zero-balance.md create mode 100644 prop-acc/scenarios/deposit/close-manual-with-zero-balance.md create mode 100644 prop-acc/scenarios/deposit/force-close-refund.md create mode 100644 prop-acc/scenarios/deposit/freeze-during-dispute.md create mode 100644 prop-acc/scenarios/deposit/unfreeze-after-mediation.md diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index acdc62f..e9981a0 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -196,6 +196,11 @@ }, "active": "b06ed69835363258", "lastOpenFiles": [ + "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/forfeit-violation-no-permit.md", "prop-acc/scenarios/deposit/forfeit-damage-public-area.md", "prop-acc/scenarios/deposit/deposit-first-time-renovation.md", @@ -219,11 +224,6 @@ "prop-acc/scenarios/adhoc/cancel-resident-withdrawal.md", "prop-acc/scenarios/adhoc/flow-a-counter-buy-ic-card.md", "概念-CollectionOrder与Receipt.md", - "概念-A流与B流.md", - "prop-acc · 物业财务(域首页).md", - "prop-acc/maps/knowledge-map.md", - "prop-acc · 知识地图.md", - "index.md", "prop-acc/scenarios/adhoc", "prop-acc/concepts/adhoc", "resident-portal/scenarios", diff --git a/prop-acc/scenarios/deposit/close-after-zero-balance.md b/prop-acc/scenarios/deposit/close-after-zero-balance.md new file mode 100644 index 0000000..e485da0 --- /dev/null +++ b/prop-acc/scenarios/deposit/close-after-zero-balance.md @@ -0,0 +1,165 @@ +--- +title: prop-acc · deposit · 场景 - 余额清零自动关闭 +aliases: + - 押金账户自动关账 + - 余额 0 自动 Closed + - close-after-zero-balance + - 场景-押金账户自动关闭 +tags: + - 场景 + - prop-acc + - 保证金 + - 结清 +audience: + - 业户 + - 业务人员 +status: 已发布 +sub_feature: deposit +last_review: 2026-05-25 +code_version: 2026-05-22 +--- + +# 场景:余额清零自动关闭 + +押金账户在**最后一笔操作**(退款 / 扣罚)使余额变 0 的瞬间,系统**自动**把账户状态翻为 `Closed`,无需手工再点"关账"。 + +## 典型情境 + +> [!example] 真实情境(三种触发) +> +> **情境 A**:张阿姨家装修无损坏,物业全额退 ¥5,000 → 余额 0 → 账户**自动 Closed**。 +> +> **情境 B**:陈先生家装修部分损坏,先扣 ¥800,再退余下 ¥4,200 → 余额 0 → 账户**自动 Closed**。 +> +> **情境 C**:刘先生违约严重,全部 ¥5,000 全扣无退 → 余额 0 → 账户**自动 Closed**。 + +## 业户视角 + +### 您会感受到什么 + +- 收到**最后一张红字收据**(可能是退款 / 也可能是扣罚) +- 同时收到通知:"您的押金账户已结清,余额 0" +- 小程序"我的押金账户"显示 "🔒 已结清" +- 流水台账仍可查(只读),整张账户的历史可追溯 + +### 您要做什么 + +- 妥善保管所有收据(缴款蓝字 + 历次退款 / 扣罚红字) +- **不要试图重启账户** —— 系统设计上不允许;新业务请联系物业开新账户 +- 如有任何关于过往流水的疑问 → 联系物业核对(凭收据 + 流水台账) + +## 业务人员视角 + +### 关键认知:不需要手工关账 + +> [!info] 自动机制 +> 任何 `RefundAction` / `ForfeitureAction` 提交后,系统在事务尾部检查 `balance == 0`,**自动**把 `status` 从 `Active` 翻到 `Closed`。 +> +> 你不需要(也不应该)在余额变 0 后还跑一次 `CloseAction` —— 那个按钮 UI 上会被守护 `canBeClosed()`(已是 Closed → false),即使强行调也会被 Policy 拦截。 + +### 触发链路 + +最常见的几条路径: + +| 序号 | 场景 | 触发动作 | 关账方式 | +|---|---|---|---| +| 1 | 无损全退 | `RefundAction(全额)` | 自动 | +| 2 | 部分扣罚 + 退余 | `RefundAction(剩余)` | 自动(退完瞬间)| +| 3 | 全扣无退 | `ForfeitureAction(余额全)` | 自动(扣完瞬间)| +| 4 | 多次扣罚直到清零 | 最后一笔 `ForfeitureAction` | 自动 | +| 5 | 主动关账(罕见,见 [[close-manual-with-zero-balance]]) | `CloseAction` | 手工(账户余额本来就 0) | + +### 实现细节(`canBeClosed` + 自动逻辑) + +`DepositAccount` 模型上的相关方法: + +```php +public function canBeClosed(): bool +{ + return $this->balance == 0 && $this->status !== DepositAccountStatus::Closed; +} +``` + +业务层(`RefundFromDepositAccountAction` / `ForfeitFromDepositAccountAction`)在事务最后会检查并自动调用 `close()`: + +```php +// 伪代码 +if ($account->canBeClosed()) { + $account->update(['status' => DepositAccountStatus::Closed]); +} +``` + +### 通知 + +后台日志记录"账户已自动关账",业户侧通过最后一张 Receipt 同时收到关账信息(Receipt 内容 + 短信)。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 财务 + participant Filament + participant RefundFromDepositAccountAction + participant 数据库 + + Note over 财务: 余额 4200,退最后 4200 + + 财务->>Filament: RefundAction (4200) + Filament->>RefundFromDepositAccountAction: handle(account, 4200, channel) + RefundFromDepositAccountAction->>数据库: 开启事务 + RefundFromDepositAccountAction->>数据库: 建 CO(-4200) + Transaction(refund, 4200→0) + RefundFromDepositAccountAction->>数据库: balance=0 + Note over RefundFromDepositAccountAction: 检查 canBeClosed() → balance==0 → true + RefundFromDepositAccountAction->>数据库: 自动 status=Closed + RefundFromDepositAccountAction->>数据库: 触发 CollectionOrderCompleted + RefundFromDepositAccountAction->>数据库: 提交事务 +``` + +## 常见问题 + +> [!question] 余额 0 但 status 还是 Active 怎么办? +> 这是 bug。可能原因: +> - 业务 Action 没调 `canBeClosed()` 检查 +> - 事务回滚后状态不一致 +> +> 修复:tinker 手工执行 `$account->update(['status' => DepositAccountStatus::Closed])`,然后修复 Action 代码。审计上可写一笔说明备注。 + +> [!question] 没有任何流水(从未缴款)的账户能关账吗? +> 技术上可以(`balance == 0`)。但**业务上不正常**(开了账户没缴款),应: +> - 查清原因(为什么开了不缴?录错?) +> - 走 `CloseAction` 手工关([[close-manual-with-zero-balance]] 场景) +> - 备注清楚"无缴款记录,关闭" + +> [!question] Frozen 账户余额是 0,自动关账吗? +> **不会**。Frozen 状态下不会自动关账,因为冻结期间任何状态变更都需要审慎处理。Frozen 且余额 0 → 先 [[unfreeze-after-mediation|解冻]] → 自动变 Active → 再走 `CloseAction` 手工关(或重启业务再继续操作)。 + +> [!question] 已关账户能"重新开张"继续用吗? +> 不能。`canBeReopened()` 永远 false。**新业务开新账户**。详见 [[account-state-machine]] "为什么不允许 Reopen" 段。 + +> [!question] 关账时业户能查看流水吗? +> 能。Closed 账户**只读模式**保留全部历史 —— 流水台账、所有 Receipt、状态变更记录(`meta`)都可查询。这是账户的"历史档案",随时可调出对账。 + +## 与 ForceClose 的区别 + +| 维度 | 本场景(自动关账) | [[force-close-refund]] 等 | +|---|---|---| +| 触发条件 | balance==0 时的最后一笔 Refund / Forfeiture | 账户 Frozen + 有余额,但要终结此账户 | +| 状态前置 | Active(Frozen 不会自动) | Frozen + hasBalance | +| Action | 任何减余额到 0 的 Action | 专用 `ForceCloseAction` | +| 资金处置 | 已有的 Refund / Forfeiture 决定 | 由 disposition 选(refund / forfeit / retain) | +| 关账理由 | "余额清零正常关账" | 强制结清,有专门 audit 字段 | + +## 异常分支 + +- 关账时业户提出反悔 → 不可逆;走线下沟通 / 司法 +- 余额非 0 想强制关 → 走 [[force-close-refund]] / [[force-close-forfeit]] / [[force-close-retain]] +- 主动关账(无业务往来)→ [[close-manual-with-zero-balance]] + +## 相关文档 + +- [[account-state-machine]] +- [[refund-full-no-damage]] +- [[refund-partial-after-forfeit]] +- [[forfeit-damage-public-area]] +- [[close-manual-with-zero-balance]] +- [[force-close-refund]] diff --git a/prop-acc/scenarios/deposit/close-manual-with-zero-balance.md b/prop-acc/scenarios/deposit/close-manual-with-zero-balance.md new file mode 100644 index 0000000..de4d603 --- /dev/null +++ b/prop-acc/scenarios/deposit/close-manual-with-zero-balance.md @@ -0,0 +1,142 @@ +--- +title: prop-acc · deposit · 场景 - 主动关账(已无业务往来) +aliases: + - 主动关账 + - 手工关押金账户 + - close-manual-with-zero-balance + - 场景-押金账户主动关账 +tags: + - 场景 + - prop-acc + - 保证金 + - 结清 +audience: + - 业务人员 +status: 已发布 +sub_feature: deposit +last_review: 2026-05-25 +code_version: 2026-05-22 +--- + +# 场景:主动关账(已无业务往来) + +账户**余额本来就是 0**(从未缴款、或之前已通过其他流程退完),物业财务想把账户**主动归档**,走 `CloseAction` 手工关账。比 [[close-after-zero-balance|自动关账]] 罕见,但 UI 仍提供。 + +## 典型情境 + +> [!example] 真实情境 +> 一年前物业为某业户开了押金账户,但**业户最终没装修**(改主意了),账户始终余额 0、状态 Active。一年后清理账户列表时,物业财务想把这种"挂着但没用过"的账户关掉,避免长期占列。 + +## 业务人员视角(本场景**业户无感**) + +> [!info] 业户视角 +> 业户从未缴款、也没动账户,**完全感受不到**这个操作。不会收到通知,也不会拿到收据(没有资金流水,Listener 不触发)。 + +### 适用前提 + +- 账户 `status = Active` +- 账户 `balance == 0` +- 业务上已确认**确实无后续业务**(联系业户确认或长期无动静) + +### 第 1 步:打开账户 + +后台 → 保证金 → 账户列表 → 按"余额=0 + 状态=Active"过滤 → 找到目标账户。 + +### 第 2 步:点击 `CloseAction`(标签"关账") + +右上角状态管理组。Modal 表单: + +| 字段 | 填什么 | +|---|---| +| **关账事由(memo)** | 必填,如 "账户未缴款,业户已确认无装修计划" | + +> [!warning] Policy 守护 +> `CloseAction` 调 `canBeClosed()`: +> - `balance == 0` ✅ +> - `status !== Closed` ✅ +> 满足这两条即可关,**不要求 Active**(理论上 Frozen + balance==0 也行,但 Frozen 状态下通常先解冻再关)。 + +### 第 3 步:提交 + +系统调 `DepositAccount::close($memo)`,事务内: + +1. 校验 `canBeClosed()` +2. 更新 `status=Closed` +3. 在 `meta.close_memo` 记关账事由 +4. 在 `meta.closed_at` 记关账时间 + +**不产生** `DepositTransaction`(没有资金流水)、**不产生** `CollectionOrder`、**不产生** `Receipt`。 + +### 第 4 步:无需通知业户 + +业务上无变化,业户无感知。如果业务上认为需要(例如告知业户"我们这边把账户归档了,以后要装修请重新申请开户"),走线下沟通,不走系统通知。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 财务 + participant Filament + participant 数据库 + + Note over 财务: 找到一年前开的空账户 + + 财务->>Filament: ViewDepositAccount → CloseAction(modal, memo) + Filament->>数据库: 校验 canBeClosed() (balance==0, status!=Closed) → true + Filament->>数据库: 更新 status=Closed + meta.close_memo + meta.closed_at + 数据库-->>Filament: ok + Filament-->>财务: 成功通知 + + Note over 数据库: 无 Transaction,无 CO,无 Receipt +``` + +## 流水台账(本场景完整记录) + +| 流水 | 说明 | +|---|---| +| (无)| 账户从开户到关账,无任何 DepositTransaction | + +只有账户本身的 `status` 字段从 `Active` 变 `Closed`,`meta` 加几个审计字段。 + +## 常见问题 + +> [!question] 没有流水的账户也算有效记录吗? +> 算。`DepositAccount` 本身是审计对象,记录了开户时间、缴款人信息、关账事由,具备完整的"开-关"生命周期文档,即使中间没有资金往来。 + +> [!question] 业务上常见这种"开了不缴"的账户吗? +> 不应常见。如果一个物业项目积累了很多空账户,说明**开户流程**有问题(业户开始装修前就开账户,但有相当比例改主意)。建议改流程为"业户决定装修当天才开账户",降低空账户产生率。 + +> [!question] 主动关账后能反悔重开吗? +> **不能**。`canBeReopened()` 永远 false。如果业户后来真的要装修,**开新账户**。 + +> [!question] 余额非 0 主动关账可以吗? +> **不能**。`canBeClosed()` 要求 `balance==0`。如果账户有余额但要终结: +> - Active + 有余额 → 走 [[refund-full-no-damage|退款]] 或 [[forfeit-damage-public-area|扣罚]] 清零后自动关 +> - Frozen + 有余额 → 走 [[force-close-refund]] / [[force-close-forfeit]] / [[force-close-retain]] 强制关账 + +> [!question] 主动关账和自动关账的区别? +> 主要区别在**触发方式**和**业务背景**: +> +> | 维度 | 自动关账([[close-after-zero-balance]]) | 主动关账(本场景) | +> |---|---|---| +> | 触发 | 最后一笔 Refund / Forfeiture 使余额变 0 | 手工 `CloseAction` | +> | 业务背景 | 有缴款也有退/扣的完整生命周期 | 通常无缴款,只是清理 | +> | 流水台账 | 有 | 无 | +> | 业户感知 | 收到最后一张红字收据 | 无感 | +> | 频率 | 大量(每个正常退还都触发)| 罕见(清理用)| + +> [!question] 批量关账(同时关多个空账户)有功能吗? +> 当前没有。如果要清理 100+ 空账户,需要在 List 页加批量操作,或运维 tinker。 + +## 异常分支 + +- 余额非 0 想强制关 → [[force-close-refund]] / [[force-close-forfeit]] / [[force-close-retain]] +- 已关账户想恢复使用 → 不可逆,开新账户 +- Frozen 账户余额 0 想关 → 先 [[unfreeze-after-mediation]] 再走本流程,或直接走 [[force-close-retain]] + +## 相关文档 + +- [[close-after-zero-balance]] +- [[account-state-machine]] +- [[deposit-first-time-renovation]] +- [[audit-long-pending-accounts]] diff --git a/prop-acc/scenarios/deposit/force-close-refund.md b/prop-acc/scenarios/deposit/force-close-refund.md new file mode 100644 index 0000000..7a6a365 --- /dev/null +++ b/prop-acc/scenarios/deposit/force-close-refund.md @@ -0,0 +1,188 @@ +--- +title: prop-acc · deposit · 场景 - 冻结状态强制全退并关账 +aliases: + - 强制全退关账 + - ForceClose refund + - force-close-refund + - 场景-冻结强制退还关账 +tags: + - 场景 + - prop-acc + - 保证金 + - 强制关账 +audience: + - 业务人员 +status: 已发布 +sub_feature: deposit +last_review: 2026-05-25 +code_version: 2026-05-22 +--- + +# 场景:冻结状态强制全退并关账 + +账户处于 **Frozen** 状态、**还有余额**,但**纠纷结果明确利向业户**(损坏归责不在业户、调解结果全退、司法判决业户胜诉等)。物业用 `ForceCloseAction` 选 `refund` disposition,**一步完成"解冻 + 全额退还 + 关账"**。 + +## 典型情境 + +> [!example] 真实情境 +> 陈先生家押金账户因墙面损坏归责争议被冻结。1 个月后第三方机构鉴定结论 —— **损坏与陈先生装修无关**(是先前住户造成的)。物业认可结论,要把 ¥5,000 全额退给陈先生并关账户。 +> +> 不能走普通 [[refund-full-no-damage|RefundAction]],因为账户 Frozen 状态下 `canWithdraw=false` 守护会拦截。需要 ForceClose 走 `refund` disposition。 + +## 业户视角 + +### 您会感受到什么 + +- 收到通知:"您的押金账户已结清,全额 ¥5,000 退还" +- 收到**红字收据**:"装修保证金退还 ¥-5,000(强制关账退还,事由:鉴定结论无责)" +- 银行 / 微信收到退款 +- 小程序"我的押金账户"显示 "🔒 已结清" + +### 您要做什么 + +- 配合提供退款渠道 +- 保管红字收据(税务凭证) + +## 业务人员视角 + +### 第 1 步:确认全退判定 + +- 第三方鉴定 / 司法判决 / 调解协议(书面) +- 物业内部决定"按全退处理" + +> [!warning] 这是不可逆操作 +> ForceClose 提交后账户立即 Closed,**无法撤销**。务必在书面凭证就位后再操作。 + +### 第 2 步:打开 Frozen 账户 + +后台 → 保证金 → 账户列表 → 找 Frozen 账户(状态显示 🧊)→ 进 `ViewDepositAccount`。 + +> 右上角状态管理组只有 `UnfreezeAction` 和 `ForceCloseAction` 可点。 + +### 第 3 步:点击 `ForceCloseAction`(标签"强制关账") + +Modal 表单(关键是 **disposition** Radio): + +| 字段 | 填什么 | +|---|---| +| **处置方式 (disposition)** | 选 ✅ **`refund`(退还)** | +| 退款渠道(动态出现)| 选业户指定渠道 | +| **关账事由(memo)** | 必填,如 "鉴定结论:墙面损坏与业户装修无关,全额退还" | + +> [!info] 不复用 RefundFromDepositAccountAction +> ForceClose 的 refund 逻辑**单独写**(`ForceCloseDepositAccountAction` ~230 行),不复用 `RefundFromDepositAccountAction`。理由: +> +> - 后者的 `canWithdraw` 守护刚好挡住 Frozen +> - 如果复用,普通调用方意外绕过冻结检查,语义边界模糊 +> - 重复约 60 行核心代码换语义边界清晰,值得 +> +> 这条权衡详见 issue.md Q3 "第二轮已落地"段。 + +> [!warning] Policy 守护 +> `DepositAccountPolicy::forceClose()`: +> - `update` 权限 +> - `isFrozen() && hasBalance()`(只允许 Frozen + 有余额) +> +> Active 账户 / Closed 账户 / Frozen 但 balance=0 的账户,按钮都灰化。 + +### 第 4 步:提交 + +系统调 `ForceCloseDepositAccountAction(disposition=refund)`,事务内: + +1. 校验 `isFrozen() && hasBalance()` +2. 建 `CollectionOrder`(`actual_amount=-5000` 红字,`status=Completed`) +3. 加 `DepositTransaction`(`type=refund`,`amount=5000`,`balance_before=5000`,`balance_after=0`,关联红字 CO) +4. 更新 `balance=0` +5. 直接 `status=Closed`(从 Frozen) +6. 在 `meta.force_closed_disposition=refund`、`meta.force_closed_memo=...`、`meta.force_closed_at=...` 记审计字段 +7. 触发 `CollectionOrderCompleted` → Listener 建红字 Receipt + +### 第 5 步:走线下退款 + 给收据 + +银行 / 微信退 ¥5,000;红字收据交业户。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 业户 + participant 财务 + participant Filament + participant ForceCloseDepositAccountAction + participant 数据库 + participant 监听器 + + Note over 业户,财务: 账户 Frozen + balance=5000,鉴定结论全退 + + 财务->>Filament: ViewDepositAccount → ForceCloseAction (disposition=refund) + Filament->>ForceCloseDepositAccountAction: handle(account, disposition=refund, channel, memo) + ForceCloseDepositAccountAction->>ForceCloseDepositAccountAction: isFrozen() && hasBalance()? yes + ForceCloseDepositAccountAction->>数据库: 开启事务 + ForceCloseDepositAccountAction->>数据库: 1. 建 CO (-5000 红字, Completed) + ForceCloseDepositAccountAction->>数据库: 2. 建 DepositTransaction (refund, 5000→0) + ForceCloseDepositAccountAction->>数据库: 3. balance=0, status=Closed(直接 Frozen→Closed) + ForceCloseDepositAccountAction->>数据库: 4. meta.force_closed_disposition=refund 等审计字段 + ForceCloseDepositAccountAction->>监听器: 5. 触发 CollectionOrderCompleted + 监听器->>数据库: 6. 建 Receipt (强制关账退还 ¥-5,000) + ForceCloseDepositAccountAction->>数据库: 提交事务 + Filament-->>财务: 成功通知 + 财务-->>业户: 银行/微信退 5000 + 红字收据 +``` + +## 状态转换 + +```mermaid +stateDiagram-v2 + [*] --> Active + Active --> Frozen : freeze() 纠纷 + Frozen --> Closed : ForceClose(refund)
+建红字流水
+全额退款 + Closed --> [*] +``` + +ForceClose 是**唯一**能从 Frozen 直接到 Closed(不经过 Active)的路径。 + +## 常见问题 + +> [!question] 为什么不先解冻再退? +> 可以走"解冻 + 普通 Refund"两步组合,效果一样。但 ForceClose 一步到位有几个好处: +> +> - **审计标记**:`meta.force_closed_*` 字段明确记录"这次关账是强制的、什么 disposition、什么事由",未来追溯一眼明白 +> - **避免中间态**:Active 状态下账户能被其他人误操作(再缴款 / 再扣罚),ForceClose 跳过这个窗口 +> - **业务语义清晰**:这次关账不是"正常退完关",是"特殊处置关",标志性强 + +> [!question] disposition 选错(原本应该 retain 选了 refund)能改吗? +> 不能。`DepositTransaction` 和 `CollectionOrder` 都不可变。如果选错: +> - 已实际退款给业户 → 找业户协商再缴回,系统记一笔 deposit;然后开新账户继续(原账户已 Closed 永久) +> - 还没实际线下退款 → 系统记录已成事实但线下不操作,业务上需在审计备注解释"系统记退款实际未执行";然后在新账户上做正确处置 +> +> **预防胜于补救**:Modal 表单提交前务必再三确认 disposition。 + +> [!question] 退款金额能改成部分吗? +> **不能**。ForceClose 是"一步终结",退款金额自动 = 当前全部余额。如果需要部分退、部分扣、部分保留,**不应该用 ForceClose**,应该: +> 1. 先 [[unfreeze-after-mediation|解冻]] 回 Active +> 2. 按比例操作:部分 [[refund-full-no-damage|退]] + 部分 [[forfeit-damage-public-area|扣]] +> 3. 余额清零时自动 Closed + +> [!question] 业户拒绝接受 ForceClose refund 的结果怎么办? +> 一旦 ForceClose 提交,账户 Closed 不可逆。**业户接受与否**只影响线下退款流程的执行: +> - 业户接受 → 收到退款 +> - 业户拒收(罕见,觉得"应该是物业全责加罚款") → 走司法 +> +> 系统层面的账面已平,后续纠纷与账户无关。 + +> [!question] ForceClose 后能反悔(Active 重启)吗? +> 不能。Closed 永久。详见 [[account-state-machine]]。 + +## 异常分支 + +- 全扣不退 → [[force-close-forfeit]] +- 资金保留待领 → [[force-close-retain]] +- 业户没失联且配合 → 优先 [[unfreeze-after-mediation]] + 普通 refund(更标准) + +## 相关文档 + +- [[account-state-machine]] +- [[freeze-during-dispute]] +- [[force-close-forfeit]] +- [[force-close-retain]] +- [[red-receipt-design]] diff --git a/prop-acc/scenarios/deposit/freeze-during-dispute.md b/prop-acc/scenarios/deposit/freeze-during-dispute.md new file mode 100644 index 0000000..636ca8e --- /dev/null +++ b/prop-acc/scenarios/deposit/freeze-during-dispute.md @@ -0,0 +1,163 @@ +--- +title: prop-acc · deposit · 场景 - 业户纠纷期间冻结账户 +aliases: + - 冻结押金账户 + - 纠纷冻结 + - freeze-during-dispute + - 场景-押金账户冻结 +tags: + - 场景 + - prop-acc + - 保证金 + - 冻结 +audience: + - 业户 + - 业务人员 +status: 已发布 +sub_feature: deposit +last_review: 2026-05-25 +code_version: 2026-05-22 +--- + +# 场景:业户纠纷期间冻结账户 + +业户和物业**就扣罚 / 退款 / 损坏归责发生纠纷**,沟通陷入僵局。物业先把账户**冻结**(任何资金动作禁止),等调解结果。 + +## 典型情境 + +> [!example] 真实情境 +> 陈先生家装修完了,物业验收发现公共走道墙面损坏要扣 ¥800。陈先生坚持"墙是装修前就坏的,跟我没关系,拒绝扣罚"。物业找施工方核实暂无定论,**预计要 1-2 周走调解**。这段时间不能让账户继续动 —— 不能让陈先生提退款,也不能强行扣罚。物业先冻结账户。 + +## 业户视角 + +### 您会感受到什么 + +- 想申请退押金 → 物业说"账户冻结中,等纠纷处理完才能退" +- 微信小程序"我的押金账户"显示"⏸ 冻结" +- 不会被强行扣钱(冻结期间扣罚也不允许) +- 不会丢钱(余额冻结,只是不让动) +- 收到通知:"您的押金账户已冻结,事由:XXX" + +> [!info] 冻结不等于扣钱 +> 冻结只是**暂停所有资金动作**,余额仍是您的钱,等纠纷处理完会做正确处置(退 / 扣 / 部分扣余下退)。 + +### 您要做什么 + +- 配合物业调解(提供装修前后照片、施工合同等证据) +- 不要单方面追讨(系统层面已挡住,纠纷不在系统层面解决) +- 调解有结果后通知物业操作(走 [[unfreeze-after-mediation|解冻]] 或 [[force-close-refund|强制关账]] 等) + +## 业务人员视角(物业财务 + 物业管理) + +### 第 1 步:决定冻结 + +触发场景: + +- 业户对扣罚归责有异议 +- 装修方与业户之间纠纷,押金归属不明 +- 涉及司法 / 仲裁,资金需保留待裁决 +- 业户失联但有未决业务(暂留 Active 不合适) + +### 第 2 步:打开账户 + +后台 → 保证金 → 账户列表 → 找到陈先生账户(Active,balance=5000)→ 进入 `ViewDepositAccount`。 + +### 第 3 步:点击 `FreezeAction`(标签"冻结") + +右上角状态管理组。Modal 表单: + +| 字段 | 填什么 | +|---|---| +| **冻结事由(reason)** | 必填,如 "业户对墙面损坏归责有异议,等待第三方核查" | +| **预计冻结时长** | 选填,如 "2 周" | + +> [!warning] Policy 守护 +> `FreezeAction` 调 `DepositAccount::canBeFreezed()`:仅 Active 状态可冻。Frozen / Closed 状态下按钮灰化。 + +### 第 4 步:提交 + +系统调 `DepositAccount::freeze(reason)`,事务内: + +1. 校验 `canBeFreezed()`(Active only) +2. 更新 `status=Frozen` +3. 在 `meta.freeze_reason` 记冻结事由(可选) +4. 在 `meta.frozen_at` 记冻结时间 + +**注意:不产生 DepositTransaction**(冻结不是资金动作,只是状态变更)。 + +### 第 5 步:通知业户 + +后台 → 短信 / 微信通知陈先生"账户已冻结,事由 XXX,预计解冻时间 XXX"。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 业户 + participant 财务 + participant Filament + participant 数据库 + + Note over 业户,财务: 业户与物业就 800 扣罚分歧,调解中 + + 财务->>Filament: ViewDepositAccount → FreezeAction(modal, reason) + Filament->>数据库: 校验 canBeFreezed() (Active=true, ok) + Filament->>数据库: 更新 status=Frozen + meta.freeze_reason + 数据库-->>Filament: ok + Filament-->>财务: 成功通知 + + Note over 数据库: 冻结期间所有 deposit/refund/forfeit 调用都会被守护拦截 +``` + +## 冻结状态下的能力对照 + +| 操作 | Active 状态 | Frozen 状态 | +|---|---|---| +| `DepositAction`(缴款)| ✅ | ❌(`canDeposit=false`)| +| `RefundAction`(退款)| ✅ | ❌(`canWithdraw=false`)| +| `ForfeitureAction`(扣罚)| ✅ | ❌(`canWithdraw=false`)| +| `FreezeAction`(冻结)| ✅ | ❌(已是 Frozen)| +| `UnfreezeAction`(解冻)| ❌(已是 Active)| ✅ | +| `CloseAction`(关账)| ✅(余额 0) | ❌(必须先解冻再关)| +| `ForceCloseAction`(强制关账)| ❌ | ✅(`isFrozen && hasBalance`)| +| 看账户 / 看流水 | ✅ | ✅(只读)| + +> [!info] Frozen + 有余额 + 想关账 = 用 ForceClose +> Frozen 状态下既不能退也不能扣,但万一就是要终结此账户(纠纷长期无果、业户失联多年、法律保留期满等),走 [[force-close-refund]] / [[force-close-forfeit]] / [[force-close-retain]]。详见 [[account-state-machine]]。 + +## 常见问题 + +> [!question] 冻结后能解冻吗? +> 当然能,走 [[unfreeze-after-mediation]]。冻结是临时状态,设计上就是"暂停"。 + +> [!question] 冻结期间业户能查询余额吗? +> 能。**只读**,业户看得到余额、流水台账、状态(Frozen)、冻结事由。 + +> [!question] 冻结期间业户能在小程序申请退款吗? +> 看小程序设计。当前 Action 假设手工触发,业户的小程序申请只是一个**业务申请单**,系统不会自动执行退款。物业人工处理时仍会被 `canWithdraw=false` 守护拦截,自然走流程。 + +> [!question] 冻结理由必填吗? +> 系统层面不强制(`reason` 参数可选)。**业务层面强烈推荐填**,审计追溯、调解复盘、与业户沟通都需要。 + +> [!question] 冻结时间长了系统会自动解冻吗? +> **不会**。冻结是手工操作,解冻也是手工。如果业务上需要"30 天自动解冻"之类的策略,需要单独写定时任务,但不推荐 —— 调解周期不固定,自动解冻容易在错误的时机解开。 + +> [!question] 多次冻结会有多个冻结理由吗? +> 当前只有 1 个 `meta.freeze_reason` 字段(后写覆盖前写)。如果想保留历史,可以在解冻时把当前 reason 抄进 `meta.freeze_history[]` 数组,再清空 `freeze_reason`。当前业务量未必需要,先简单实现。 + +> [!question] 冻结期间能改账户的其他字段吗? +> 看 Policy。当前 Policy 允许 update(改 payer 信息、备注等),但不允许 delete。如果有更严的"冻结期间什么都不能改"需求,在 `DepositAccountPolicy::update()` 加 `!isFrozen()` 检查。 + +## 异常分支 + +- 误冻结 → [[unfreeze-after-mediation]] 解开(reason 改 "误操作解除") +- 长期冻结无解 → [[force-close-refund]] / [[force-close-forfeit]] / [[force-close-retain]] +- 冻结期间业户失联 → 留 Frozen 到法律保留期满 → [[force-close-retain]] + +## 相关文档 + +- [[account-state-machine]] +- [[unfreeze-after-mediation]] +- [[force-close-refund]] +- [[exception-deposit-on-frozen]] +- [[forfeit-damage-public-area]] diff --git a/prop-acc/scenarios/deposit/unfreeze-after-mediation.md b/prop-acc/scenarios/deposit/unfreeze-after-mediation.md new file mode 100644 index 0000000..4dd711a --- /dev/null +++ b/prop-acc/scenarios/deposit/unfreeze-after-mediation.md @@ -0,0 +1,170 @@ +--- +title: prop-acc · deposit · 场景 - 调解结束后解冻 +aliases: + - 解冻押金账户 + - 调解完成解冻 + - unfreeze-after-mediation + - 场景-押金账户解冻 +tags: + - 场景 + - prop-acc + - 保证金 + - 冻结 +audience: + - 业户 + - 业务人员 +status: 已发布 +sub_feature: deposit +last_review: 2026-05-25 +code_version: 2026-05-22 +--- + +# 场景:调解结束后解冻 + +[[freeze-during-dispute|冻结]] 期间纠纷调解出结果,业户与物业达成一致,账户解冻、回到 Active,然后按调解结果执行后续(退 / 扣 / 部分扣余下退)。 + +## 典型情境 + +> [!example] 真实情境 +> 陈先生家押金账户因墙面损坏归责纠纷被冻结 2 周。期间物业找施工方核实,发现损坏**确实是装修期间造成的**(有施工照片为证)。陈先生最终同意:扣 ¥500(原本物业要扣 ¥800,部分妥协),剩余 ¥4,500 退还。 +> +> 物业财务先解冻,再按 [[refund-partial-after-forfeit]] 走"扣 500 + 退 4500"两步。 + +## 业户视角 + +### 您会感受到什么 + +- 收到通知:"您的押金账户已解冻" +- 小程序"我的押金账户"显示 "✅ Active" +- 后续按调解结果走 —— 收到红字扣罚收据 + 退款 + 退款收据 + +### 您要做什么 + +- 配合提供退款渠道(银行卡 / 微信) +- 接收红字收据并妥善保管 +- 任何后续意见在系统外沟通(账户已按调解结果处置,不可逆) + +## 业务人员视角(物业财务) + +### 第 1 步:确认调解结果有书面凭证 + +- 调解协议书(双方签字) +- 微信 / 录音确认(辅助) + +> [!warning] 没有凭证不要解冻 +> 解冻后立即可做扣 / 退,如果调解没书面凭证、业户事后反悔,物业很被动。 + +### 第 2 步:打开账户 + +后台 → 保证金 → 账户列表 → 找到 Frozen 账户 → 进入 `ViewDepositAccount`。 + +> 状态显示 "🧊 Frozen",右上角只有 `UnfreezeAction` 和 `ForceCloseAction` 可点(其他都灰)。 + +### 第 3 步:点击 `UnfreezeAction`(标签"解冻") + +Modal 表单: + +| 字段 | 填什么 | +|---|---| +| **解冻事由(reason)** | 必填,如 "调解协议:扣 ¥500,退 ¥4,500" | + +> [!warning] Policy 守护 +> `UnfreezeAction` 调 `canBeUnfreezed()`:仅 Frozen 状态可解。Active / Closed 状态下按钮灰化。 + +### 第 4 步:提交 + +系统调 `DepositAccount::unfreeze(reason)`,事务内: + +1. 校验 `canBeUnfreezed()`(Frozen only) +2. 更新 `status=Active` +3. 在 `meta.unfreeze_reason` 记解冻事由 +4. 在 `meta.unfrozen_at` 记解冻时间 +5. (可选)在 `meta.freeze_history[]` 追加这次冻结-解冻的完整记录 + +**不产生 DepositTransaction**(同冻结,只是状态变更)。 + +### 第 5 步:按调解结果做后续 + +- 扣 ¥500 → `ForfeitureAction`(详见 [[forfeit-damage-public-area]]) +- 退 ¥4,500 → `RefundAction`(详见 [[refund-full-no-damage]]) +- 余额清零自动 Closed + +两步顺序:**先扣再退**,因为扣罚事由要清楚,退款是处理剩余款。 + +### 第 6 步:通知业户 + +短信 / 微信告知调解结果已执行 + 退款已到账 + 收据已发。 + +## 系统流程 + +```mermaid +sequenceDiagram + participant 业户 + participant 调解 + participant 财务 + participant Filament + participant 数据库 + + Note over 业户,财务: 冻结 2 周后调解出结果:扣 500 + 退 4500 + + 调解-->>财务: 提供书面调解协议 + 财务->>Filament: ViewDepositAccount → UnfreezeAction(modal, reason) + Filament->>数据库: 校验 canBeUnfreezed() (Frozen=true, ok) + Filament->>数据库: 更新 status=Active + meta.unfreeze_reason + 数据库-->>财务: 已解冻 + + 财务->>Filament: ForfeitureAction(500, "墙面损坏调解结果") + Filament->>数据库: 建 CO(-500) + Transaction(forfeiture, 5000→4500) + Receipt + + 财务->>Filament: RefundAction(4500, 业户银行卡) + Filament->>数据库: 建 CO(-4500) + Transaction(refund, 4500→0) + Receipt + 数据库-->>数据库: balance=0, 自动 status=Closed + + 财务-->>业户: 2 张红字收据 + 退款到账 +``` + +## 流水台账(本场景完整记录) + +| 流水 | type | amount | balance_before | balance_after | 备注 | +|---|---|---|---|---|---| +| 1 | deposit | 5000 | 0 | 5000 | 首次缴款 | +| 2 | forfeiture | 500 | 5000 | 4500 | 调解结果扣罚(墙面损坏)| +| 3 | refund | 4500 | 4500 | 0 | 调解结果退还 | + +`status` 变化:`Active → Frozen → Active → Closed`(自动)。 + +## 常见问题 + +> [!question] 解冻后能立即做扣罚 / 退款吗? +> 能。解冻是事务内同步操作,提交后立即生效。后续 `ForfeitureAction` / `RefundAction` 可立即触发。 + +> [!question] 解冻后忘了执行后续(扣 / 退)怎么办? +> 账户停留 Active 状态,余额仍是冻结前的数。可任何时候继续操作。**业务上不应该**长期停留 Active 不处理 —— 应在解冻后立即完成调解结果。 + +> [!question] 解冻后业户反悔调解结果怎么办? +> 解冻 + 已执行扣 / 退 → 已执行的不可逆。如果业户反悔: +> - 走司法程序(系统不参与) +> - 物业可选择补退一笔(走 deposit 反向冲)以化解纠纷 +> +> **预防**:解冻前务必拿到书面调解协议。 + +> [!question] 解冻和重新冻结(同一账户多次冻结)可以吗? +> 可以。账户可以在 `Active ↔ Frozen` 之间多次来回。如果业务上常发生,`meta.freeze_history[]` 数组就是用来记历次冻结的事由和时间。 + +> [!question] 调解时间过长,业户失联怎么办? +> 走 [[force-close-retain]] —— 不解冻,直接从 Frozen 强制关账并保留余额,等业户出现再处理。 + +## 异常分支 + +- 调解失败长期僵持 → [[force-close-refund]] / [[force-close-forfeit]] / [[force-close-retain]] +- 业户失联 → [[force-close-retain]] +- 误冻结(其实没纠纷)→ UnfreezeAction(reason "误操作解除") +- 解冻后业户立即提退 → 走 [[refund-full-no-damage]] + +## 相关文档 + +- [[freeze-during-dispute]] +- [[account-state-machine]] +- [[forfeit-damage-public-area]] +- [[refund-partial-after-forfeit]] +- [[force-close-retain]]