vault backup: 2026-05-25 22:37:41

This commit is contained in:
Willie
2026-05-25 22:37:41 +08:00
parent 68ea6dfbe9
commit 29938ecbe0
5 changed files with 820 additions and 4 deletions

View File

@@ -0,0 +1,241 @@
---
title: prop-acc · deposit · 场景 - 月度押金账户余额对账
aliases:
- 押金月对账
- 押金账面与银行对账
- audit-monthly-deposit-balance
- 场景-押金月度对账
tags:
- 场景
- prop-acc
- 保证金
- 审计
audience:
- 财务
- 业务人员
status: 已发布
sub_feature: deposit
last_review: 2026-05-25
code_version: 2026-05-22
---
# 场景:月度押金账户余额对账
物业财务**每月底**对所有押金账户做一次余额校验,确保系统账面 = 银行专户余额 = 流水累计净值,三方平衡。是审计内控的标准动作。
## 典型情境
> [!example] 真实情境
> 物业财务王主管每月最后一个工作日做"押金账户余额三方对账":
>
> 1. 系统所有 Active + Closed-retain 账户的 `balance` + `meta.balance_held_amount` 之和
> 2. 银行"装修押金专户"的实际余额
> 3. 全部 `DepositTransaction` 按净值累加 + retain 保留金额
>
> 三者必须**完全相等**,任何差额都要查清。
## 三方对账详解
### 1. 系统账面余额(System Balance)
```sql
-- Active 账户的当前余额
SELECT SUM(balance) FROM acc_deposit_accounts WHERE status = 'active';
-- + Frozen 账户的当前余额(冻结期间余额仍属业户)
SELECT SUM(balance) FROM acc_deposit_accounts WHERE status = 'frozen';
-- + Closed 但保留资金的账户(retain disposition)
SELECT SUM(JSON_EXTRACT(meta, '$.balance_held_amount'))
FROM acc_deposit_accounts
WHERE status = 'closed' AND JSON_EXTRACT(meta, '$.force_closed_disposition') = 'retain';
```
三者之和 = **物业代管负债总额**
### 2. 银行专户余额(Bank Balance)
物业的"装修押金银行专户"(单独开立的隔离账户)的月底余额。
> [!info] 为什么要专户隔离
> 押金是代管资金,**不应与物业自有资金混同**。专户的好处:
> - 内控明确,资金不被挪用
> - 对账简单(银行余额 == 应付业户的钱)
> - 监管合规(部分地区物业法规要求)
### 3. 流水累计净值(Transaction Sum)
```sql
-- 全部 DepositTransaction 的净值(deposit 加,refund/forfeiture 减)
SELECT
SUM(CASE WHEN type = 'deposit' THEN amount ELSE -amount END) AS net_balance
FROM acc_deposit_transactions;
-- 这个值理论上 == 所有 Active + Frozen 账户的 balance 之和
-- (不包括 retain Closed 账户,因为它们的 balance 字段未变,但已不在 Transaction 累加范围内 —— 实际上也累加了,因为 forfeiture/refund 没发生)
```
> [!info] 完整公式
> ```
> SUM(transactions 净值) = SUM(active.balance) + SUM(frozen.balance) + SUM(closed.balance where status=closed and balance>0)
> ```
> 任何不一致 → 有 bug 或人为绕过(详见 [[deposit-account-vs-transaction]] "两者的契约")。
### 三方平衡的预期
```
系统账面余额 == 银行专户余额 == 流水累计净值
```
任何一对不等 → 调查。
## 业务人员视角(月底操作)
### 第 1 步:导出系统账面余额报表
后台 → 保证金 → 列表 → 按"状态 Active / Frozen / Closed-retain"过滤 → 导出 Excel,合计 `balance` 列 + `meta.balance_held_amount` 列。
(或运行 SQL 查询,见上方)
### 第 2 步:取银行专户对账单
- 登录银行网银 / 联系银行客户经理
- 取本月最后一天的余额(精确到分)
### 第 3 步:做对账
| 维度 | 数值 | 来源 |
|---|---|---|
| A. 系统账面余额 | ¥X | 后台导出 |
| B. 银行专户余额 | ¥Y | 银行对账单 |
| C. 流水累计净值 | ¥Z | SQL 查询 |
| **差额(应 0)** | A - B 和 A - C | |
如果三个值不相等,**这才是对账的核心工作**:查清差异。
### 第 4 步:差额排查
常见差异原因:
| 差异 | 可能原因 | 排查方法 |
|---|---|---|
| A > B(系统多于银行) | 账面收了但银行未到账(在途资金) | 查最近 deposit 的支付渠道(POS / 微信)是否回滚 |
| A < B(系统少于银行) | 银行多了钱但系统没记(混账)| 查银行流水那笔多的钱来源 |
| A ≠ C(系统账面与流水不一致)| 系统 bug 或 tinker 改过余额 | `$account->verifyBalance()` 找出哪个账户不一致 |
> [!warning] 任何不一致都是事故
> 即使差额很小(¥1),也必须查清。押金是负债 = 业户的钱,任何不平都是合规风险。
### 第 5 步:出对账报告
格式:
```markdown
# 2026 年 5 月 装修押金账户对账报告
## 三方余额
- 系统账面:¥152,300.00(Active 89 + Frozen 3 + Retain 4)
- 银行专户:¥152,300.00
- 流水净值:¥152,300.00
## 平衡情况:✅ 完全平衡
## 本月新增账户:12
## 本月关账:8(其中 retain 1)
## 本月扣罚 / 退款总额:¥85,400 / ¥73,200
```
如有差异:
```markdown
## ⚠️ 差异
- 系统账面 ¥X 与银行 ¥Y 差 ¥Z
- 已查:第 N 笔 deposit on 2026-05-22 的微信支付实际未到账(支付平台冻结审核)
- 处理:待 5-28 微信渠道放款,再次对账确认
## 本月需复盘
- ID 4567 账户余额异常:`verifyBalance()` 返 false,差 ¥0.5(疑似浮点误差或人为编辑)
- 已 tinker 修复 + 加备注
```
## 系统流程
```mermaid
flowchart TD
A[月底触发] --> B[导出系统账面]
A --> C[取银行专户对账单]
A --> D[查询流水累计]
B --> E{三方相等?}
C --> E
D --> E
E -- 是 --> F[出对账报告 ✅]
E -- 否 --> G[排查差异]
G --> H[追溯交易 / 查 verifyBalance / 排查 tinker 操作]
H --> I[修复 + 备注]
I --> J[重新对账]
J --> E
```
## 报表 SQL 汇总(供财务复用)
```sql
-- 本月新增账户
SELECT COUNT(*) FROM acc_deposit_accounts
WHERE created_at BETWEEN '2026-05-01' AND '2026-05-31 23:59:59';
-- 本月关账(含自动关账 + 主动关账 + ForceClose)
SELECT COUNT(*),
COUNT(*) FILTER (WHERE JSON_EXTRACT(meta, '$.force_closed_disposition') IS NOT NULL) AS force_closed,
COUNT(*) FILTER (WHERE JSON_EXTRACT(meta, '$.force_closed_disposition') = 'retain') AS retain_count
FROM acc_deposit_accounts
WHERE status = 'closed'
AND updated_at BETWEEN '2026-05-01' AND '2026-05-31 23:59:59';
-- 本月扣罚总额
SELECT SUM(amount) FROM acc_deposit_transactions
WHERE type = 'forfeiture'
AND created_at BETWEEN '2026-05-01' AND '2026-05-31 23:59:59';
-- 本月退款总额
SELECT SUM(amount) FROM acc_deposit_transactions
WHERE type = 'refund'
AND created_at BETWEEN '2026-05-01' AND '2026-05-31 23:59:59';
```
## 常见问题
> [!question] 没有银行专户怎么对账?
> 退一步,与物业总账户对账,但只能看"已收 / 已退"流水净值,**无法判断资金是否被挪用**(被混入其他业务用资金池)。**强烈建议设置专户**。
> [!question] retain 账户的资金算物业代管负债吗?
> **算**。即使账户 Closed,只要 `meta.balance_held_amount > 0`,资金法律上仍属业户,会计上仍是"其他应付款"。详见 [[force-close-retain]]。
> [!question] 月度对账要审计师参与吗?
> 内部对账物业财务自己做。**年度审计**时审计师会重点检查这部分(物业的代管资金合规性、是否有挪用迹象)。月度对账留下完整记录是年审顺利的前提。
> [!question] 差额查不出来怎么办?
> 上报物业财务总监 + 法务。可能需要:
> - 委托第三方审计核对
> - 银行流水逐笔重对
> - tinker 操作日志审查(谁改了什么)
>
> **不能放任**,押金对账差异是严重合规问题。
> [!question] 多个物业项目的押金一起对吗?
> 通常每个 `community_id` 独立对账(每个物业项目一个银行专户)。系统层面也可按 community 分组导出。
## 异常分支
- 发现 `verifyBalance()=false` 的账户 → tinker 修复 + 排查根因
- 发现银行专户少于账面 → 资金挪用嫌疑,法务介入
- 发现长期 retain 的资金对应不上 → [[audit-long-pending-accounts|长期账户排查]]
## 相关文档
- [[deposit-account-vs-transaction]]
- [[transaction-types]]
- [[force-close-retain]]
- [[audit-long-pending-accounts]]
- [[deposit-vs-adhoc-vs-prepaid]]

View File

@@ -0,0 +1,175 @@
---
title: prop-acc · deposit · 场景 - 冻结账户尝试缴款被拦截
aliases:
- 冻结账户缴款被拒
- exception-deposit-on-frozen
- 场景-冻结账户缴款异常
tags:
- 场景
- prop-acc
- 保证金
- 异常
audience:
- 业务人员
status: 已发布
sub_feature: deposit
last_review: 2026-05-25
code_version: 2026-05-22
---
# 场景:冻结账户尝试缴款被拦截
业务人员**误对一个 Frozen 账户**点击"追加缴款"按钮,系统按 [[account-state-machine|状态机]] 守护**直接拦截**,不让任何资金进入。
## 典型情境
> [!example] 真实情境
> 物业职员小李在前台办业户陈先生的押金追加业务。陈先生说"我再补 ¥1,000"。小李没注意到陈先生的账户当前处于 **Frozen 状态**(因之前墙面损坏纠纷),习惯性打开账户点了"追加缴款"按钮。
>
> 系统提示:**"账户处于冻结状态,无法缴款"**,操作被拒绝。
## 业务人员视角
### 您看到什么
| 时刻 | 看到 |
|---|---|
| 进入 `ViewDepositAccount` | 状态栏显示 "🧊 Frozen" |
| 状态管理组里的按钮 | "追加缴款 / 退款 / 扣罚" 全部**灰化**,只有 "Unfreeze / ForceClose" 可点 |
| 如果硬调 API | 系统抛出 `RuntimeException`:"账户处于冻结状态,canDeposit returns false" |
> [!info] 三道防御
> 1. **UI 层**:按钮根据 `canDeposit()` 状态自动灰化
> 2. **Policy 层**:`DepositAccountPolicy::update()` + 状态检查
> 3. **业务 Action 层**:`DepositIntoAccountAction` 入口校验 `canDeposit()`,失败抛异常
>
> 即使前两道被绕过(直接调 API、tinker 操作),第三道仍兜底。
### 第 1 步:看到操作被拒,理解原因
提示框信息:**"账户处于冻结状态,canDeposit returns false"**(系统消息 + 业务消息)。
### 第 2 步:与业户沟通
不要硬要绕过。说明:
> "您的账户当前因纠纷冻结,需先调解完成解冻后才能继续缴款。"
业户的几个可能反应:
| 业户反应 | 处理 |
|---|---|
| "那快帮我解冻" | 看是否有调解结果。无书面凭证不解冻 |
| "我先把钱给你,等解冻再录" | **不要这么做**。物业不能"暂存"业户的钱 —— 没有凭证 = 不合规。让业户先离开,等解冻 |
| "那这账户先不动了" | OK,告诉业户冻结期间状态、预计解冻时间 |
### 第 3 步:正确流程
1. 调解 → 拿到书面调解协议
2. [[unfreeze-after-mediation|解冻]] → 账户变 Active
3. [[deposit-additional-topup|追加缴款]] → 正常缴款流程
## 系统视角:守护链路
```mermaid
sequenceDiagram
participant 职员
participant Filament
participant DepositIntoAccountAction
participant DepositAccount
职员->>Filament: 点击"追加缴款" → DepositAction modal
Note over Filament: UI 守护:Frozen 状态按钮已灰化(防误点)
职员->>Filament: 如果绕过 UI,直接提交
Filament->>DepositIntoAccountAction: handle(account, amount)
DepositIntoAccountAction->>DepositAccount: canDeposit()?
DepositAccount-->>DepositIntoAccountAction: false (status=Frozen)
DepositIntoAccountAction-->>Filament: throw RuntimeException
Filament-->>职员: 显示错误:"账户处于冻结状态,无法缴款"
Note over 职员: 不会建任何 CO/Transaction/Receipt
```
## 为什么这条守护比直觉更严
历史上曾经的代码允许 `[Active, Frozen]` 都能缴款,直觉上"反正多存钱不亏",收紧成只允许 Active 是经过教训的。详见 [[account-state-machine]] "关键守护:Frozen 不允许任何资金动作" 段。
真实风险:
| 反例 | 后果 |
|---|---|
| 装修公司持续往受冻结的押金账户灌钱 | 资金被困、责任更复杂(冻结期间是否构成新债权?) |
| 业户与物业纠纷期间业户继续存钱 | 资金混同,扣罚 / 退款时算不清"哪一笔是原押金、哪一笔是纠纷后存的" |
| 系统给"暂存"开口子 | 业务人员可能用它做不规范操作("先放着,以后调整") |
收紧后**所有冻结期间的资金问题都必须先解冻**,语义清晰、审计可追。
## 测试断言
代码层有专门测试覆盖此异常路径:
```php
test('cannot deposit on frozen account', function () {
$account = DepositAccount::factory()->frozen()->create(['balance' => 5000]);
expect(fn () => app(DepositIntoAccountAction::class)->handle($account, 1000))
->toThrow(RuntimeException::class, '账户处于冻结状态');
expect($account->fresh()->balance)->toBe(5000.0); // 余额未变
expect(DepositTransaction::count())->toBe(0); // 流水未建
});
```
任何对 `canDeposit()` 的修改都会触发此测试,确保守护不被无意中放宽。
## 常见问题
> [!question] 业户坚持要存钱,职员怎么办?
> 解释 + 引导:
> - 解释为什么不能存(冻结期间所有资金动作禁止,无论方向)
> - 引导先完成调解 / 解冻
> - 如果业户着急,加快内部调解流程(联系物业管家)
> [!question] 业户已经把钱转过来了(对公转账已到账)但账户冻结,怎么办?
> 这是**业务问题**,不是系统问题:
> - 物业账户已收到这笔钱,但**不能挂在该业户的 DepositAccount 上**(冻结不允许)
> - 选项:
> - 退回业户(原路退,清晰)
> - 系统外暂留(物业银行账户里的"挂账" / 财务备查)
> - 等解冻后再录入
> - **推荐第一个**(退回业户),最干净
> [!question] 同样的守护对 ForceClose 适用吗?
> ForceClose 走**独立的 Policy 方法** `forceClose()`,守护是 `isFrozen() && hasBalance()` —— 这是**专门**为 Frozen 状态设计的,与 deposit / refund / forfeit 守护刚好"互补":
>
> | 守护 | 适用状态 | 阻止 |
> |---|---|---|
> | `canDeposit()` | Active only | Frozen / Closed 缴款 |
> | `canWithdraw()` | Active only | Frozen / Closed 退款 / 扣罚 |
> | `forceClose()` Policy | Frozen + hasBalance only | Active / Closed / Frozen-zero-balance 强制关账 |
>
> 这条对应详见 [[account-state-machine]]。
> [!question] 这个守护影响小程序业户自助操作吗?
> 当前没有小程序自助缴款 —— Action 假设手工操作。**未来加小程序在线缴款时**:
> - 前端调同一个 `DepositIntoAccountAction`,守护自动生效
> - 业户在小程序操作冻结账户时同样被拦截
> - UI 应在小程序侧显示更友好的错误信息(避免裸抛 RuntimeException 给业户)
> [!question] 怎么手工验证某账户是否 Frozen?
> 后台 → 保证金 → 列表 → 状态列;或 tinker `DepositAccount::find($id)->status`。
## 异常分支
- 想加缴款 → 先 [[unfreeze-after-mediation|解冻]] → [[deposit-additional-topup|追加]]
- 想退款 → 同上,或 [[force-close-refund|强制关账退还]]
- 想扣罚 → 同上,或 [[force-close-forfeit|强制关账扣罚]]
- 长期冻结无解 → [[force-close-retain|资金保留关账]]
## 相关文档
- [[account-state-machine]]
- [[freeze-during-dispute]]
- [[deposit-additional-topup]]
- [[force-close-refund]]
- [[force-close-forfeit]]

View File

@@ -0,0 +1,170 @@
---
title: prop-acc · deposit · 场景 - 冻结状态强制全扣并关账
aliases:
- 强制全扣关账
- ForceClose forfeit
- force-close-forfeit
- 场景-冻结强制扣罚关账
tags:
- 场景
- prop-acc
- 保证金
- 强制关账
audience:
- 业务人员
status: 已发布
sub_feature: deposit
last_review: 2026-05-25
code_version: 2026-05-22
---
# 场景:冻结状态强制全扣并关账
账户处于 **Frozen** 状态、**还有余额**,**纠纷结果明确利向物业**(损坏归责确认业户全责、违约严重、司法判决物业胜诉等)。物业用 `ForceCloseAction``forfeit` disposition,**一步完成"解冻 + 全额扣罚 + 关账"**。
## 典型情境
> [!example] 真实情境
> 刘先生家装修时**多次违反装修管理协议**(深夜施工、随意丢弃建筑垃圾、损坏 3 处公共设施),物业累计要扣 ¥6,000,而账户只有 ¥5,000。其间刘先生拒绝补缴,账户被冻结进入仲裁。仲裁裁决"业户全责,押金 ¥5,000 全部用于罚款,差额 ¥1,000 业户另行支付"。
>
> 物业财务依据仲裁书走 ForceClose forfeit,把账户里的 ¥5,000 全扣转入物业收入,账户关闭。差额 ¥1,000 通过司法执行另案处理(不在本系统内)。
## 业户视角
### 您会感受到什么
- 收到通知:"您的押金账户已结清,余额 ¥5,000 全额扣罚"
- 收到**红字收据**:"装修保证金扣罚 ¥-5,000(强制关账扣罚,事由:仲裁裁决业户全责)"
- 银行 / 微信**没有退款**(钱已转入物业收入)
- 小程序"我的押金账户"显示 "🔒 已结清,余额 0"
### 您要做什么
- 接受裁决结果(钱已转出账户,无法追回)
- 保管红字收据(税务凭证,有时报税)
- 如有异议走司法上诉(不在本系统内)
## 业务人员视角
### 第 1 步:确认全扣判定
- 仲裁裁决书 / 司法判决书 / 物业内部决议(必须书面)
- 物业内部已审批,具备实施权限
> [!warning] 比 ForceClose refund 更敏感
> 全扣是把业户的钱全部转入物业,**法律风险**比"全退"更高。书面凭证必须完整,内部多层审批,任何疏漏未来纠纷物业败诉概率大。
### 第 2 步:打开 Frozen 账户
后台 → 保证金 → 账户列表 → 找 Frozen 账户 → 进 `ViewDepositAccount`
### 第 3 步:点击 `ForceCloseAction`(标签"强制关账")
Modal 表单:
| 字段 | 填什么 |
|---|---|
| **处置方式 (disposition)** | 选 ✅ **`forfeit`(扣罚)** |
| **扣罚事由(memo)** | 必填且详细。例: "2026-XX-XX 仲裁案号 XXX 裁决:业户多次违约,押金 5000 全额作为罚款。" |
> [!info] forfeit 不需要选退款渠道
> 扣罚的钱**不退给业户**,直接转入物业收入,无需 PaymentChannel。Modal 表单选 forfeit 后退款渠道字段隐藏。
> [!warning] Policy 守护
> 同 [[force-close-refund]]:`update` 权限 + `isFrozen() && hasBalance()`。
### 第 4 步:提交
系统调 `ForceCloseDepositAccountAction(disposition=forfeit)`,事务内:
1. 校验 `isFrozen() && hasBalance()`
2.`CollectionOrder`(`actual_amount=-5000` 红字,`status=Completed`)—— 表达"从代管负债转出"
3.`DepositTransaction`(`type=forfeiture`,`amount=5000`,`balance_before=5000`,`balance_after=0`,关联红字 CO)
4. 更新 `balance=0`
5. 直接 `status=Closed`(Frozen → Closed)
6.`meta.force_closed_disposition=forfeit``meta.force_closed_memo=...``meta.force_closed_at=...` 记审计字段
7. 触发 `CollectionOrderCompleted` → Listener 建红字 Receipt"装修保证金扣罚 ¥-5,000(强制关账扣罚,事由 XXX)"
### 第 5 步:无线下退款,只给红字凭证
后台找到红字 Receipt → 发业户。**不要走任何转账操作** —— 资金已通过 forfeiture 流水转入物业维修收入科目,账面已完成。
## 系统流程
```mermaid
sequenceDiagram
participant 业户
participant 财务
participant Filament
participant ForceCloseDepositAccountAction
participant 数据库
participant 监听器
Note over 业户,财务: 账户 Frozen + balance=5000,仲裁裁决全扣
财务->>Filament: ViewDepositAccount → ForceCloseAction (disposition=forfeit)
Filament->>ForceCloseDepositAccountAction: handle(account, disposition=forfeit, memo)
ForceCloseDepositAccountAction->>ForceCloseDepositAccountAction: isFrozen() && hasBalance()? yes
ForceCloseDepositAccountAction->>数据库: 开启事务
ForceCloseDepositAccountAction->>数据库: 1. 建 CO (-5000 红字, Completed)
ForceCloseDepositAccountAction->>数据库: 2. 建 DepositTransaction (forfeiture, 5000→0)
ForceCloseDepositAccountAction->>数据库: 3. balance=0, status=Closed
ForceCloseDepositAccountAction->>数据库: 4. meta.force_closed_disposition=forfeit + memo + at
ForceCloseDepositAccountAction->>监听器: 5. 触发 CollectionOrderCompleted
监听器->>数据库: 6. 建 Receipt (强制关账扣罚 ¥-5,000)
ForceCloseDepositAccountAction->>数据库: 提交事务
Filament-->>财务: 成功通知
财务-->>业户: 红字凭证(无退款)
```
## 资金流意义
```mermaid
flowchart LR
A[业户押金 5000<br/>物业代管负债] -->|forfeit| B[物业维修/罚没收入<br/>5000]
```
会计上:**其他应付款 → 装修维修收入**,资金从未离开物业账户,只是科目变化。
## 常见问题
> [!question] forfeit 的钱进什么科目?
> 通常进"装修维修收入"或"罚没收入"科目(视物业财务核算细则)。账面通过 `Receipt` 上的 line item 描述触发科目映射(Listener `generateDepositReceiptItems` 按 `DepositTransaction.type=forfeiture` 选词)。
> [!question] 业户能反悔追讨吗?
> 已扣的钱要回去**只能走司法**:
> - 业户起诉物业不当扣罚
> - 法院判决物业败诉 → 物业按判决退款(可能需开新账户做反向 deposit + refund 操作记账,审计完整)
> - 法院判决物业胜诉 → 维持原状
>
> 系统层面的 ForceClose 不可逆,司法判决物业败诉走"补偿"路径,不"撤销"原 ForceClose。
> [!question] 仲裁裁决有补充条款(例如要求物业方做某些工作)怎么办?
> 系统只处理资金,补充条款(如修复方案、整改要求)需物业线下执行 + 留档。系统不强制关联。
> [!question] 业户已经搬走,联系不上怎么给红字收据?
> 系统层面凭证已生成,业务上若联系不上业户:
> - 用挂号信寄到登记地址
> - 物业内部档案保留
> - 等业户联系时再补发
>
> 如果业户失联前提下连扣罚都不应做(可能未让业户充分应诉),走 [[force-close-retain|资金保留]] 等业户出现更稳妥。
> [!question] 比 forfeit 多扣怎么办(扣 5000 还不够,要扣 6000)?
> 账户最多扣到余额清零(`amount ≤ balance` 守护)。**差额追偿不在本系统内**:
> - 单独的 [[adhoc-flow-a-vs-flow-b|adhoc 一次性收费]] 流(开违约金账单)
> - 司法执行(系统不参与)
## 异常分支
- 部分扣部分退 → 走 [[unfreeze-after-mediation|解冻]] + 普通 [[refund-partial-after-forfeit|扣罚后退余]]
- 全退给业户 → [[force-close-refund]]
- 资金保留待定 → [[force-close-retain]]
## 相关文档
- [[force-close-refund]]
- [[force-close-retain]]
- [[freeze-during-dispute]]
- [[account-state-machine]]
- [[forfeit-damage-public-area]]

View File

@@ -0,0 +1,230 @@
---
title: prop-acc · deposit · 场景 - 资金保留并关账(法律保留/业户失联)
aliases:
- 强制保留关账
- ForceClose retain
- 资金保留归档
- force-close-retain
- 场景-押金资金保留关账
tags:
- 场景
- prop-acc
- 保证金
- 强制关账
audience:
- 业务人员
status: 已发布
sub_feature: deposit
last_review: 2026-05-25
code_version: 2026-05-22
---
# 场景:资金保留并关账(法律保留 / 业户失联)
账户 **Frozen** 状态、**有余额**,但**不确定该退还还是扣罚** —— 业户失联、遗产分配延迟、法律保留期未满、案件审理中等。物业用 `ForceCloseAction``retain` disposition,**关闭账户但保留余额** —— 资金留在物业账上,等业户回来或法律决定。
## 典型情境
> [!example] 真实情境(一)
> 装修公司倒闭,法人失联。"王装修有限公司"账户里还有 ¥15,000(为 3 户业主代缴的押金)。3 户业主中 2 户已自费维修,1 户仍在装修。物业不知该退给装修公司(已失联)、退给业主(账面缴款人不是业主)、还是扣罚作维修款。决定:**保留资金,关账户,等法律或公司清算结果**。
> [!example] 真实情境(二)
> 张阿姨因家中变故住进养老院,装修保证金账户里还有 ¥4,000。家属正在办继承公证,需 6 个月。物业不能擅自退给"自称是子女的人",也不能扣罚(无任何违约)。**保留资金,关账户,等继承公证结果**。
## 业户 / 业户家属视角
### 您(或亲属)会感受到什么
- 账户被关闭,但**没有任何退款**或扣罚通知
- 系统通知:"您的押金账户已结清,资金 ¥X 暂留物业代管,事由 XXX"
- 小程序"我的押金账户"显示 "🔒 已结清,余额保留中"
- **资金仍属于您 / 您家属 / 法定继承人**,物业不可挪用
### 您要做什么
- 在法律 / 继承 / 公司清算等程序结束后,**主动联系物业**
- 出示身份证明(本人 / 继承证明 / 司法授权文件)
- 物业核实后**重新进入退款流程**(走线下操作,系统层面**不重启账户**)
## 业务人员视角
### 第 1 步:确认 retain 判定
**业务上要符合**以下任一情境:
- 业户失联(无法核实身份,无法退款)
- 业户已故,继承未定
- 装修公司清算 / 倒闭,无明确债权人
- 法律保留期内(司法要求资金不动)
- 任何**不该退、不该扣**的临时归档需求
> [!warning] 不要把 retain 当万能选项
> retain 是为**长期不确定**的情境设计的。短期纠纷应走 [[freeze-during-dispute|冻结]] 等结果。retain 是终态,关账后**永久无法在本账户继续操作**。
### 第 2 步:打开 Frozen 账户
后台 → 保证金 → 账户列表 → 找 Frozen 账户 → 进 `ViewDepositAccount`
### 第 3 步:点击 `ForceCloseAction`(标签"强制关账")
Modal 表单:
| 字段 | 填什么 |
|---|---|
| **处置方式 (disposition)** | 选 ✅ **`retain`(保留归档)** |
| **保留事由(retain_reason)** | **必填**(关键!),如 "装修公司倒闭,3 户业主代缴款,清算未结" |
| **关账事由(memo)** | 必填,如 "ForceClose retain,等待清算结果" |
> [!info] retain_reason 与 memo 的区别
> - `retain_reason`:**业务背景**,为什么保留(写给将来要追溯的人看)
> - `memo`:**操作意图**,这次操作做了什么
>
> 两个字段都进 `meta`,审计时一起查。
> [!warning] retain 不写 retain_reason 会被守护拦截
> `ForceCloseDepositAccountAction` 对 disposition=retain 时强制要求 `retain_reason` 非空。这是刻意设计 —— 防止"无理由保留"导致后续追溯困难。
### 第 4 步:提交
系统调 `ForceCloseDepositAccountAction(disposition=retain)`,事务内:
1. 校验 `isFrozen() && hasBalance()`
2. 校验 `retain_reason` 非空
3. **不建 CollectionOrder**(没有资金动作,余额不变)
4. **不建 DepositTransaction**(同上)
5. **不建 Receipt**(同上)
6. 更新 `status=Closed`(从 Frozen)
7.`meta` 写入审计字段:
- `force_closed_disposition: 'retain'`
- `force_closed_memo: ...`
- `force_closed_at: now`
- **`balance_held_amount`**: 保留的金额
- **`balance_held_reason`**: retain_reason 的副本
8. 账户 `balance` 字段保持原值(例如 ¥5,000)—— **不清零**
### 第 5 步:线下记录归档
- 物业财务把这种"已 Closed 但有余额"账户列入**待处理代管资金清单**
- 银行账户里对应的资金做**专项隔离**(账面与物业自有资金分开)
- 每月 / 每季对账时核对
### 第 6 步:业户出现时
业户(或家属 / 继承人 / 清算人)出现并出示合法身份后:
| 选项 | 操作 |
|---|---|
| 退还给业户 | **开新账户** → 走 `Deposit` 把 retain 余额转入新账户(系统层 deposit + 备注"原 retain 账户转入")→ 立刻 [[refund-full-no-damage|退款]] |
| 扣罚处理 | **开新账户** → 同上流程 → [[forfeit-damage-public-area|扣罚]] |
| 物业内部决议无主资金 | 走法律程序(物业法务 / 街道办)→ 司法判决归属 → 按判决处理 |
> [!warning] 不要试图"重启" Closed 账户
> Closed 账户永久关闭。`canBeReopened` 永远 false。所有"业户回来了"的处理都通过**新账户**走。
## 系统流程
```mermaid
sequenceDiagram
participant 业户家属
participant 财务
participant Filament
participant ForceCloseDepositAccountAction
participant 数据库
Note over 业户家属,财务: 业户失联,余额 5000 留 Frozen 状态
财务->>Filament: ViewDepositAccount → ForceCloseAction(disposition=retain, retain_reason)
Filament->>ForceCloseDepositAccountAction: handle(account, retain, retain_reason, memo)
ForceCloseDepositAccountAction->>ForceCloseDepositAccountAction: isFrozen() && hasBalance()? yes
ForceCloseDepositAccountAction->>ForceCloseDepositAccountAction: retain_reason 非空? yes
ForceCloseDepositAccountAction->>数据库: 开启事务
ForceCloseDepositAccountAction->>数据库: 1. balance 不变(5000)
ForceCloseDepositAccountAction->>数据库: 2. status=Closed
ForceCloseDepositAccountAction->>数据库: 3. meta.force_closed_disposition=retain<br/>+ balance_held_amount=5000<br/>+ balance_held_reason
Note over 数据库: 不建 CO/Transaction/Receipt
ForceCloseDepositAccountAction->>数据库: 提交事务
Filament-->>财务: 成功通知
Note over 业户家属,财务: 数年后家属持继承公证出现
业户家属->>财务: 我是继承人,要领回 5000
财务->>财务: 核验继承文件
财务->>Filament: 开新 DepositAccount + 缴款 5000 (备注"原 retain 转入")
财务->>Filament: 立即 RefundAction (5000) → 自动 Closed
财务-->>业户家属: 退款 + 红字收据
```
## 流水台账(本场景完整记录)
| 流水 | 说明 |
|---|---|
| (无)| 整个 retain 关账过程**没有任何 DepositTransaction**,余额未变,只有账户状态变更 + meta 审计字段 |
后续如果业户家属出现:
| 流水 | 说明 |
|---|---|
| (新账户的)deposit | 把 retain 余额转入新账户 |
| (新账户的)refund | 退给业户家属 |
## meta 字段示例(retain 关账后)
```json
{
"force_closed_disposition": "retain",
"force_closed_memo": "ForceClose retain,等待清算结果",
"force_closed_at": "2026-05-25T14:32:01+08:00",
"force_closed_by": 42,
"balance_held_amount": 5000.00,
"balance_held_reason": "装修公司倒闭,3 户业主代缴款,清算未结"
}
```
## 常见问题
> [!question] retain 后账户有余额但 status=Closed,这不是矛盾吗?
> 不矛盾。Closed 表示"业务终结",`balance` 字段表示"账面余额"。两者**独立**:
>
> - Closed + balance=0:正常关账(refund / forfeit 后)
> - Closed + balance>0:retain 关账,业务终结但资金仍归业户
>
> 系统设计上,`canBeClosed()` 要求 `balance==0` 是给**正常路径**用的;ForceClose 走专用 Policy 不受此限制。
> [!question] retain 之后报表上余额怎么算?
> 物业代管资金报表:
> - 全 Active 账户 `balance` 之和
> - **加上** Closed 账户中 `meta.balance_held_amount` 之和
>
> 后者是"待处置代管资金",银行账户里对应的钱仍属业户。详见 [[audit-monthly-deposit-balance]]。
> [!question] retain 的资金物业能挪用吗?
> **不能,法律上是业户的钱**。物业的会计科目仍为"其他应付款",银行账户里这部分资金做隔离专户最规范。任何挪用都是违法。
> [!question] 长期 retain(10 年以上)的资金怎么处理?
> 根据各地民法 / 物业管理条例,长期无主资金可能转入:
> - 街道办无主资金账户
> - 公益基金
> - 财政
>
> 具体走流程视司法管辖,**系统不主动处理** —— 物业法务发起,系统配合记录(可能在该账户 meta 加一笔 `transferred_to_*` 备注,或者依照流程做反向 forfeit 然后转出)。
> [!question] retain 的金额能在已关账户上"动一下"吗(例如部分释放、部分继续保留)?
> 不能。账户 Closed 后任何操作都不允许。如果业务需要"部分释放":开新账户 → 把保留余额按 X 元转入(deposit)→ 退给业户(refund)→ 剩余在新账户继续 retain(再 ForceClose retain 一次)。
> [!question] retain 不写 retain_reason 系统会怎样?
> `ForceCloseDepositAccountAction` 会校验失败,抛出错误。Modal 表单也会前端校验阻止提交。这是**刻意设计**:无理由 retain = 后续无法追溯 = 不合规。
## 异常分支
- 业户出现配合退款 → 开新账户 + deposit + refund(详见上方"业户出现时"步骤)
- 业户最终被司法判定无主 → 物业法务发起转出流程
- 短期纠纷不适用 retain → 走 [[freeze-during-dispute]] 然后 [[unfreeze-after-mediation]]
## 相关文档
- [[force-close-refund]]
- [[force-close-forfeit]]
- [[account-state-machine]]
- [[freeze-during-dispute]]
- [[audit-monthly-deposit-balance]]
- [[audit-long-pending-accounts]]