vault backup: 2026-05-25 22:07:35

This commit is contained in:
Willie
2026-05-25 22:07:35 +08:00
parent 39c15cf443
commit 341bbb77d6
6 changed files with 618 additions and 7 deletions

View File

@@ -196,6 +196,12 @@
},
"active": "b06ed69835363258",
"lastOpenFiles": [
"prop-acc/concepts/deposit/red-receipt-design.md",
"prop-acc/concepts/deposit/transaction-types.md",
"prop-acc/concepts/deposit/payer-types.md",
"prop-acc/concepts/deposit/account-state-machine.md",
"prop-acc/concepts/deposit/deposit-account-vs-transaction.md",
"prop-acc/concepts/deposit",
"prop-acc/maps/adhoc-knowledge-map.md",
"prop-acc/index.md",
"prop-acc/scenarios/adhoc/cancel-resident-withdrawal.md",
@@ -217,11 +223,6 @@
"prop-acc/scenarios/adhoc/receipt-miniapp-pdf-download.md",
"prop-acc/scenarios/adhoc/exception-paid-but-cannot-deliver.md",
"prop-acc/scenarios/adhoc/exception-payment-split-failure.md",
"prop-acc/scenarios/adhoc/exception-wechat-callback-delay.md",
"prop-acc/scenarios/adhoc-audit-ic-card-stock-reconciliation.md",
"prop-acc/scenarios/adhoc/exception-cross-community-pending.md",
"prop-acc/scenarios/adhoc/exception-duplicate-order.md",
"prop-acc/scenarios/adhoc/void-after-payment.md",
"prop-acc/scenarios/adhoc",
"prop-acc/concepts/adhoc",
"resident-portal/scenarios",
@@ -230,7 +231,6 @@
"resident-portal/maps",
"resident-portal/glossary",
"resident-portal/features",
"resident-portal/faq",
"resident-portal/decisions"
"resident-portal/faq"
]
}

View File

@@ -0,0 +1,133 @@
---
title: prop-acc · deposit · 押金账户状态机
aliases:
- 押金账户状态机
- DepositAccount 状态机
- Active / Frozen / Closed
tags:
- 概念
- prop-acc
- 保证金
- 状态机
audience:
- 业户
- 业务人员
status: 已发布
sub_feature: deposit
last_review: 2026-05-25
code_version: 2026-05-22
---
# 押金账户状态机
押金账户三种状态:**Active(在押)** / **Frozen(冻结)** / **Closed(已结清)**
> [!warning] 重要原则
> 一旦 Closed,永远 Closed。**不允许重开**。新业务一律开新账户。理由见本文末"为什么不允许 reopen"。
## 三状态速查
| 状态 | 中文 | 何时进入 | 能做什么 |
|---|---|---|---|
| `Active` | 在押 | 新账户首次缴款后 | 缴款 / 退款 / 扣罚 / 冻结 / 关账(余额 0) |
| `Frozen` | 冻结 | 发生纠纷、内审等 | 看流水(只读)/ 解冻 / 强制关账 |
| `Closed` | 已结清 | 余额清零正常关账 OR ForceClose | 看流水(只读),没有任何可写操作 |
## 状态机图
```mermaid
stateDiagram-v2
[*] --> Active : 开户 + 首次缴款
Active --> Active : 追加缴款 / 部分退款 / 扣罚
Active --> Frozen : freeze() 纠纷/审计
Frozen --> Active : unfreeze() 调解完成
Active --> Closed : close() 余额=0
Frozen --> Closed : forceClose() 强制结账
Closed --> [*]
note right of Frozen
冻结期间禁止任何资金进出
canDeposit = false
canWithdraw = false
end note
note right of Closed
永久终态
canBeReopened = false
新业务请开新账户
end note
```
## 守护方法(代码层)
`DepositAccount` 模型上有一组 `can*()` 守护方法,**所有写入 Action 必须先调用**:
| 方法 | 返回 true 的状态 | 用途 |
|---|---|---|
| `canDeposit()` | Active **only** | DepositAction 准入 |
| `canWithdraw()` | Active **only** | RefundAction / ForfeitureAction 准入 |
| `canBeFreezed()` | Active | FreezeAction 准入 |
| `canBeUnfreezed()` | Frozen | UnfreezeAction 准入 |
| `canBeClosed()` | balance=0 且 ≠Closed | CloseAction 准入 |
| `canBeReopened()` | **永远 false** | 占位,刻意禁止 |
| `canOperate()` | Active | 复合判断:既不在 Frozen 也不在 Closed |
| `hasBalance()` | balance>0 | 配合判定能否 Close / 是否需 ForceClose |
| `isAvailable()` | Active | UI 显示"可用"或灰化 |
> [!info] 关键守护:Frozen 不允许任何资金动作
> `canDeposit()` 与 `canWithdraw()` **都只允许 Active**,Frozen 一律拒绝。这条规则比直觉更严:
>
> - 原本曾允许 `[Active, Frozen]` 都能缴款(看着"反正多存钱不亏")
> - 但与"冻结 = 暂停所有交易"的语义矛盾
> - 真实风险:纠纷期间装修公司继续往受冻结账户灌钱 → 资金被困、责任更复杂
>
> 现在两个方向严格一致:Frozen = 完全冻结,只能解冻或 ForceClose。
## 业务人员视角
后台账户列表的"状态"列对应这三个值。
- 看到 `Active`:绿色,可点开操作
- 看到 `Frozen`:橙色,所有按钮变灰,只剩 `Unfreeze` / `ForceClose`
- 看到 `Closed`:灰色,完全只读,只能看流水
## 业户视角
业户**通常感受不到状态机**,只感受到结果:
- 余额能正常用 → Active
- 申请退款被拒,前台告知"账户冻结中,等纠纷处理完才能动" → Frozen
- 账户被关 → 收到一张红字收据 + 短信告知"您的押金账户已结清"
## 为什么不允许 Reopen
`canBeReopened()` 永远返回 `false`,是**刻意的设计**。
| 假设允许 reopen | 风险 |
|---|---|
| Closed → Active 又开放 | 流水台账"已结清"的语义被破坏,审计难追责 |
| 业务方:"业户搬回来了就 reopen 老账户" | 鼓励混账;两次业务关系应有清晰边界 |
| 系统级:"误操作 close 了能反悔" | close 已经守护"余额 0",误操作不会丢钱;新业务开新账户即可 |
替代做法:业户搬回来续约,**开新账户**。旧账户保留作为历史台账,与"曾经的业务关系结束"语义一致。
## 异常路径:Frozen + 有余额 + 想关账?
矛盾:
- `Frozen` 不允许 `withdraw`(包括退款 / 扣罚)
- `canBeClosed()` 要求余额=0
- 又不能 unfreeze 直接关
这种困境通过 **ForceClose** 解决,见 [[force-close-refund]] / [[force-close-forfeit]] / [[force-close-retain]] 三种 disposition。
ForceClose 是**唯一**能合法从 Frozen 直接到 Closed 的路径,通过专门的 `DepositAccountPolicy::forceClose()` 守护(`update` 权限 + `isFrozen() && hasBalance()`)。
## 相关文档
- [[deposit-account-vs-transaction]]
- [[transaction-types]]
- [[freeze-during-dispute]]
- [[unfreeze-after-mediation]]
- [[close-after-zero-balance]]
- [[force-close-refund]]

View File

@@ -0,0 +1,125 @@
---
title: prop-acc · deposit · 押金账户与押金流水
aliases:
- 押金账户与押金流水
- DepositAccount 与 DepositTransaction
- 押金的双对象模式
tags:
- 概念
- prop-acc
- 保证金
- 核心概念
audience:
- 业户
- 业务人员
status: 已发布
sub_feature: deposit
last_review: 2026-05-25
code_version: 2026-05-22
---
# 押金账户与押金流水
保证金模块底层是**两个对象**配合的:**账户**(`DepositAccount`)只记**当前状态**,**流水**(`DepositTransaction`)记**每一笔变动**。
## 为什么要两个对象
> [!info] 类比:银行存折
> - **账户** = 存折封面那一行"当前余额 ¥5,000"
> - **流水** = 翻开存折每一页:几号存了多少、几号取了多少、每笔的"前余额→后余额"
如果只有账户没有流水,查到余额是 ¥3,000 你不知道**钱从哪里来的、被扣了什么**。审计、业户对账、纠纷复盘全都失据。
如果只有流水没有账户,每次想知道当前余额都要重新累加全部历史,慢且容易脏。
所以两个都要:**账户给你"现在长什么样"**,**流水给你"如何变成现在这样"**。
## 字段速查
### DepositAccount(账户)
| 字段 | 含义 |
|---|---|
| `id` | 账户 ID |
| `community_id` | 所属物业项目 |
| `fee_type_id` | 押金种类(装修保证金 / 入驻押金 / ...) |
| `payer_type` | 缴款人类型(详见 [[payer-types]]) |
| `payer_name` | 缴款人姓名(冗余存,免关联 user 表) |
| `payer_contact` | 缴款人联系方式 |
| `community_user_profile_id` | 业户档案 ID(若缴款人是平台业户) |
| `asset_id` | 关联房屋单元(若与具体房屋相关) |
| **`balance`** | **当前余额(单一事实来源)** |
| `status` | 账户状态(详见 [[account-state-machine]]) |
| `opened_at` | 开户时间 |
| `meta` | JSON 扩展字段(`force_closed_*` 等审计标记) |
### DepositTransaction(流水)
| 字段 | 含义 |
|---|---|
| `id` | 流水 ID |
| `deposit_account_id` | 归属账户 |
| `type` | 流水类型(详见 [[transaction-types]]) |
| `amount` | 本笔金额(正数;退款/扣罚也是正数,方向由 type 表达) |
| `balance_before` | 本笔之前账户余额 |
| `balance_after` | 本笔之后账户余额 |
| `related_collection_order_id` | 关联收款单(deposit / refund / forfeiture 都关联) |
| `memo` | 备注 |
| `operated_by` | 操作员 ID |
| 创建后**不可变** | 一旦生成就只读,任何"撤销"都建新流水反向冲 |
## 两者的契约
**账户.balance 必须等于流水按时间累加的净值**
```php
$account->verifyBalance(); // bool,看是否一致
$account->getBalanceDifference(); // float,差额(0 才对)
$account->calculateBalanceFromTransactions(); // 现场重算
```
这是审计的核心校验。日常运行中两者必须一致;若出现不一致只可能是 bug 或人为绕过(已通过 [[account-state-machine]] 守护和 Policy 双重防御)。
## 与一次性收费的 CollectionOrder + Receipt 关系
保证金的每笔流水**也会建一张 CollectionOrder 和 Receipt**(详见 [[collection-order-and-receipt]]),退款 / 扣罚走红字 CollectionOrder(详见 [[red-receipt-design]])。
```mermaid
flowchart LR
A[业户缴款 5000] --> B[DepositTransaction<br/>type=deposit, amount=5000]
A --> C[CollectionOrder<br/>actual=5000]
C --> D[Receipt<br/>amount=5000]
B -.关联.-> C
E[退款 5000] --> F[DepositTransaction<br/>type=refund, amount=5000]
E --> G[CollectionOrder<br/>actual=-5000 红字]
G --> H[Receipt<br/>amount=-5000 红字]
F -.关联.-> G
```
为什么不只用 `DepositTransaction`?因为业户要拿到一张可下载、可打印的**收据凭证** —— 那是 `Receipt` 的职责。`DepositTransaction` 是内部台账,`Receipt` 是对外凭证。
## 业户视角(您看到什么)
业户**通常不直接接触账户/流水概念**。您看到的是:
- 物业前台告诉您"您还有 ¥5,000 装修保证金在账"
- 装修完了物业给您**一张红字收据**:"装修保证金退还 ¥-5,000"
- 微信小程序"我的账户"里能查到余额和历次变动
底下的两个对象都是后台的事。
## 业务人员视角
后台 → 保证金 → 账户列表,你看到的每行就是一个 `DepositAccount`。点开"查看"进入 `ViewDepositAccount`,右侧的"流水"标签就是该账户的 `DepositTransaction` 列表(只读、按时间倒序)。
所有写入操作(`DepositAction` / `RefundAction` / `ForfeitureAction` 等)都**同时写账户余额 + 流水**,事务内完成。任何只写一边的代码都是 bug。
## 相关文档
- [[account-state-machine]]
- [[transaction-types]]
- [[payer-types]]
- [[red-receipt-design]]
- [[collection-order-and-receipt]]
- [[deposit-vs-adhoc-vs-prepaid]]

View File

@@ -0,0 +1,102 @@
---
title: prop-acc · deposit · 缴款人类型
aliases:
- 押金缴款人类型
- DepositPayerType
- 谁交的押金
tags:
- 概念
- prop-acc
- 保证金
- 业务字典
audience:
- 业户
- 业务人员
status: 已发布
sub_feature: deposit
last_review: 2026-05-25
code_version: 2026-05-22
---
# 缴款人类型(DepositPayerType)
押金账户必须记录**谁交的钱**,因为退款时要原路退回。系统支持 6 种缴款人,比常见物业系统的"业主/租户"二分细得多。
## 6 种类型速查
| 枚举 | 中文 | 典型场景 | 是否绑业户档案 |
|---|---|---|---|
| `Owner` | 业主 | 自住业主交装修押金 | 通常绑(`community_user_profile_id`) |
| `Tenant` | 租户 | 出租房租客装修需要押金 | 通常绑(若租户已注册) |
| `Contractor` | 装修承包商 | 自然人装修工头交押金 | 通常**不绑**(临时关系) |
| `Company` | 装修公司 | 法人装修公司交批量押金 | **不绑**(法人非业户) |
| `Supplier` | 供应商 | 进场施工的供货方押金(罕见) | 不绑 |
| `Other` | 其他 | 兜底 | 视情况 |
## 为什么不只用"业主/租户"二分
常见物业系统只有"业主"或"租户"两类,会带来这些问题:
> [!warning] 真实坑
> - 装修公司代多个业户交押金,挂在哪个业户名下都不对
> - 工头是自然人但不是业户,系统逼着你建假业户
> - 供应商进场施工要交押金,业户表里根本没他
本系统拆得更细:
- **Owner / Tenant** 是平台业户(`community_user_profile_id` 必有)
- **Contractor / Company / Supplier / Other** 是"三方"(`community_user_profile_id` 可空,靠 `payer_name` + `payer_contact` 记)
## 业户账户 vs 三方账户
代码层有两个辅助方法:
```php
$account->isOwnerAccount(); // payer_type == Owner
$account->isThirdPartyAccount(); // payer_type ∈ {Contractor, Company, Supplier, Other}
```
差异:
| 维度 | 业户账户 | 三方账户 |
|---|---|---|
| `community_user_profile_id` | 必填 | 可空 |
| 业户能否在小程序查询 | 能(通过业户登录) | 不能(无业户账号) |
| 退款方式 | 通常原路退回业户绑定支付方式 | 需手工指定退款渠道 |
| 找人的难度 | 通过业户档案找 | 靠 `payer_contact` 联系 |
## 业户视角(您是哪种)
> [!example] 张阿姨(业主)
> 自住业主张阿姨家请人装修,自己交了 ¥5,000 押金。
> → `payer_type = Owner`,账户绑张阿姨的业户档案。退款直接退到她微信。
> [!example] 王装修(装修公司)
> 王老板的装修公司本月承接小区 3 户业主的装修,公司账户一次性垫付 3 张押金 ¥15,000。
> → 3 个独立账户,每个 `payer_type = Company`,`payer_name = "王装修有限公司"`。
> 退款时退到公司对公账户。每户业主对自己那 ¥5,000 知情但不直接拿到。
> [!example] 李工头(承包商)
> 自然人装修工头,无公司主体,自己出名字交押金。
> → `payer_type = Contractor`,`payer_name = "李某某"`。退款退给个人。
## 业务人员视角
后台开账户时**第一个必填字段**就是 `payer_type`,选完才决定后续字段是否显示业户档案选择器。
> [!tip] 选错会怎样?
> `payer_type` 只影响 UI 显示和退款找人的便利性,**不影响**账户能不能用、能不能退。万一开账户时选错,可以联系运维通过 tinker 改字段;不影响资金流水。
## 与其他模块的关系
- **业户(Resident)**:Owner / Tenant 类型必填 `community_user_profile_id`,关联 [[业户]] 跨域概念
- **房屋单元(Housing Unit)**:可选 `asset_id`,关联 [[房屋单元]] 跨域概念 —— 装修押金通常与具体房屋关联;入驻押金可不绑
- **费用类型(FeeType)**:必填 `fee_type_id`,区分"装修保证金 / 入驻押金 / 设备押金 / ..."
## 相关文档
- [[deposit-account-vs-transaction]]
- [[deposit-first-time-renovation]]
- [[deposit-on-behalf-by-company]]
- [[业户]]
- [[房屋单元]]

View File

@@ -0,0 +1,130 @@
---
title: prop-acc · deposit · 红字凭证设计
aliases:
- 红字凭证设计
- 退款扣罚的金额正负
- 红字 CollectionOrder
tags:
- 概念
- prop-acc
- 保证金
- 架构决策
audience:
- 业务人员
- 架构师
status: 已发布
sub_feature: deposit
last_review: 2026-05-25
code_version: 2026-05-22
---
# 红字凭证设计
押金的退款和扣罚**也走 `CollectionOrder` + `Receipt` 链路**,通过**金额正负**表达方向 —— 这就是"红字凭证"。本文说明为什么这么设计,以及它对凭证文案、报表、未来扩展的影响。
## 核心结论(一句话)
> [!tip] 本系统采用「金额正负」而非「新增枚举 case」表达退款/扣罚方向。
> 缴款 `actual_amount = +5000`,退款 `actual_amount = -3000`,扣罚 `actual_amount = -2000`。
> `Receipt.amount` 同样可负。
## 设计决策表
| 维度 | 金额正负(已采用) | 加 `CollectionType` 枚举 case |
|---|---|---|
| **Schema 改动** | 零迁移 — `actual_amount``decimal` 无 CHECK 约束 | 改 enum + 改 listener match 分支 |
| **报表聚合** | `SUM(actual_amount)` 一行算净值(收入 - 退款) | 需 `GROUP BY type` 再相减 |
| **中国会计实践** | ✅「红字凭证」就是负数,会计师熟悉 | 不通用,需另解释 |
| **未来扩展** | 同模式可直接复用(账单退款 / 预存款退款) | 枚举持续膨胀,每加一个业务加一个 case |
| **类型分类细粒度** | `DepositTransaction.type` 已有 3 档 | 双重表达,信息冗余 |
## 资金流示意
```mermaid
flowchart TD
subgraph "缴款(蓝字)"
A1[业户缴 5000] --> A2[DepositTransaction<br/>type=deposit, amount=5000]
A1 --> A3[CollectionOrder<br/>actual=+5000]
A3 --> A4[Receipt<br/>amount=+5000<br/>「装修保证金缴纳 ¥5,000」]
end
subgraph "退款(红字)"
B1[业户退 3000] --> B2[DepositTransaction<br/>type=refund, amount=3000]
B1 --> B3[CollectionOrder<br/>actual=-3000 红字]
B3 --> B4[Receipt<br/>amount=-3000 红字<br/>「装修保证金退还 ¥-3,000」]
end
subgraph "扣罚(红字)"
C1[物业扣 2000] --> C2[DepositTransaction<br/>type=forfeiture, amount=2000]
C1 --> C3[CollectionOrder<br/>actual=-2000 红字]
C3 --> C4[Receipt<br/>amount=-2000 红字<br/>「装修保证金扣罚 ¥-2,000」]
end
```
## 凭证文案如何区分退款/扣罚
`Receipt.amount` 都是负数,但**文案不同**,业户一眼能看明白方向。
| `DepositTransaction.type` | Receipt 文案 | 业户感受 |
|---|---|---|
| `refund` | 装修保证金退还 ¥-3,000 | 拿回自己的钱 |
| `forfeiture` | 装修保证金扣罚 ¥-2,000 | 钱被扣了 |
文案由 Listener `generateDepositReceiptItems``DepositTransaction.type` 选词,自动注入 Receipt 的 line items。
> [!info] 未来增强:PDF 红色字样
> Receipt PDF 模板对负数金额**当前是普通样式**,优先级排期低。可加红字效果(中国习惯)等业务方反馈再上。
## CollectionStatus 仍用 `Completed` 而不是 `Disbursed`
退款的 CollectionOrder 同样进入 `Completed` 状态,**不新增**"Disbursed(已支出)"枚举值。
理由:
- `Completed` 语义 = **"事务完成"**,不局限"收到钱"
- 不加新 case 减少枚举膨胀,符合"最简单实用"
- 报表里"已完成的 CO" `SUM(actual_amount)` 就是净值,无需关心方向
`refund_status` 字段也**保持 `None`**,因为退款的 CollectionOrder 是**独立的红字单**,不是"原单的退款标记"。这与传统电商"在原订单上挂退款"模式不同 —— 那种模式适合一次性商品交易,不适合押金的"账户 + 流水"长期账本。
## 为什么不在原 CollectionOrder 上挂退款标记?
替代方案:在缴款单上加 `refund_amount``refund_status`,退款时改这两个字段。
为什么我们没选:
| 问题 | 后果 |
|---|---|
| 原单状态会被频繁更新 | 缴款时建立的"已完成快照"被破坏,审计追溯困难 |
| 多次部分退款怎么办 | 原单要记一个数组?变成事实上的子流水,等于在错位置实现 |
| 跨期对账 | 4 月缴的 5000 在 6 月退 3000 → 4 月的报表数据会变,跨期数字不稳定 |
| 与新业务复用 | 账单 / 预存款的退款不能复用此模式,要各自重复实现 |
红字独立单解决以上全部问题:每张单是不可变快照,跨期数字不变,各业务模块通过相同的"红字 CO + Receipt"模式表达退款。
## 与一次性收费(adhoc)的对比
| 维度 | adhoc 一次性收费 | deposit 保证金 |
|---|---|---|
| 主对象 | `AdHocEvent` | `DepositAccount` |
| 流水/凭证 | `CollectionOrder` + `Receipt`(单笔) | `DepositTransaction` + 关联 `CollectionOrder` + `Receipt`(长期账本) |
| 退款方式 | 走 [[adhoc-void-after-payment]] 作废 + 退款 | **红字 CollectionOrder**(独立单)|
| 是否进入收入 | ✅ 一次性收入 | ❌ 负债科目(代管款) |
详见 [[deposit-vs-adhoc-vs-prepaid]]。
## 待补 / 已知限制
| 项 | 状态 |
|---|---|
| Receipt PDF 红色字体样式 | 待补,优先级低 |
| 小程序在线申请退款(走支付网关 webhook) | 待补,等业务有需求再加 |
| `CollectionOrders/Actions/RecordRefundAction`(从原单视角的退款入口) | **已知重叠**,未来要统一为单一退款流程 |
## 相关文档
- [[deposit-account-vs-transaction]]
- [[transaction-types]]
- [[collection-order-and-receipt]]
- [[refund-full-no-damage]]
- [[forfeit-damage-public-area]]

View File

@@ -0,0 +1,121 @@
---
title: prop-acc · deposit · 押金流水类型
aliases:
- 押金流水类型
- DepositTransactionType
- 三种流水
tags:
- 概念
- prop-acc
- 保证金
- 业务字典
audience:
- 业户
- 业务人员
status: 已发布
sub_feature: deposit
last_review: 2026-05-25
code_version: 2026-05-22
---
# 押金流水类型(DepositTransactionType)
押金账户的所有资金变动**只有 3 种合法流水**:**deposit(存入)** / **refund(退款)** / **forfeiture(扣罚)**
> [!warning] 第 4 种 `adjustment(调整)`在代码里仍然存在,但**已弃用**。本文末解释为什么。
## 3 种合法流水速查
| 类型 | 中文 | 余额方向 | 触发 Action | 是否建 CollectionOrder | 凭证 Receipt 金额 |
|---|---|---|---|---|---|
| `Deposit` | 存入 | + 增加 | [[deposit-first-time-renovation]] / [[deposit-additional-topup]] 等 | ✅ 正数 | 正数(蓝字) |
| `Refund` | 退款 | 减少 | [[refund-full-no-damage]] / [[refund-partial-after-forfeit]] | ✅ 负数(红字) | 负数(红字) |
| `Forfeiture` | 扣罚 | 减少 | [[forfeit-damage-public-area]] / [[forfeit-violation-no-permit]] | ✅ 负数(红字) | 负数(红字) |
详见 [[red-receipt-design]] 关于"红字凭证"的设计。
## amount 字段一律为正数
不要被"退款扣罚"的方向误导:
- `DepositTransaction.amount` **永远是正数**
- 方向由 `type` 表达,不由 amount 符号
- 关联的 `CollectionOrder.actual_amount` 才是负数(红字凭证)
举例(业户首次缴 ¥5,000,后退 ¥3,000,再扣 ¥2,000):
| 流水 ID | type | amount | balance_before | balance_after |
|---|---|---|---|---|
| 1 | `Deposit` | 5000 | 0 | 5000 |
| 2 | `Refund` | 3000 | 5000 | 2000 |
| 3 | `Forfeiture` | 2000 | 2000 | 0 |
最后账户余额 0,可走 [[close-after-zero-balance]] 关账。
## 三种流水的语义边界
> [!info] Refund vs Forfeiture
> 都是从账户里减钱,业户都拿到红字凭证。差别在**业务定性**:
>
> | 维度 | Refund(退款) | Forfeiture(扣罚) |
> |---|---|---|
> | 业户是否应得 | ✅ 是,本来该退回 | ❌ 不,作为违约/损坏赔偿 |
> | 是否需举证 | 否 | 需(损坏证据、违约事实) |
> | 凭证表述 | "装修保证金退还 ¥-X" | "装修保证金扣罚 ¥-X" |
> | 业户感知 | 拿回自己的钱 | 钱被扣了 |
凭证文案由 Listener `generateDepositReceiptItems``type` 选词,业户拿到 PDF 一眼能看明白方向(详见 [[red-receipt-design]])。
## 为什么 `Adjustment`(调整)弃用
代码里 `DepositTransactionType::Adjustment` 仍存在,但**对应的 Action 已删除**,UI 完全禁用。
### 设计取舍(摘自 issue.md Q3)
| 维度 | 保留 adjustment 的代价 | 不保留的解法 |
|---|---|---|
| **审计风险** | 余额修正给前台开后门,任何余额都能"改对" | 任何错误用 deposit + refund/forfeiture 组合补正,**留下完整修正记录** |
| **责任追溯** | adjustment 没有方向/凭证,事后查不清当时改了什么 | 每笔补正都是合规流水 + 对应凭证 |
| **业户感知** | 业户不知道余额为什么突然变了 | 业户拿到对应凭证,知道每分钱的来去 |
### 真实修正场景示例
"业户王先生缴款时被收银员录错,把 ¥5,000 录成 ¥50,000(多录 10 倍)。"
**错误做法**(adjustment):
- 直接把账户余额改成 ¥5,000
**正确做法**:
- 建一笔 `Refund` ¥45,000(对应红字 CollectionOrder)
- 备注"录错金额修正"
- 业户拿到一张红字收据 "装修保证金退还 ¥-45,000",事后审计完全可追
这样虽然多一笔流水,但**每一分钱都有凭证**。
## 业户视角
业户在小程序"我的押金账户"里能看到流水列表,每行就是一笔 `DepositTransaction`:
```
2026-03-01 +5,000.00 装修保证金缴纳
2026-04-15 -3,000.00 装修保证金退还
2026-04-15 -2,000.00 装修保证金扣罚(公共走道墙面损坏)
余额 0.00 ✅ 已结清
```
## 业务人员视角
后台 → 保证金 → 账户详情 → 流水标签。
- 点击账户上方的"缴纳 / 退款 / 扣罚"按钮分别触发 `DepositAction` / `RefundAction` / `ForfeitureAction`
- 三个按钮的可见性由 [[account-state-machine]] 守护:Frozen 状态下都灰化
- 不存在"修改流水""删除流水"按钮 —— 任何错误走补充流水
## 相关文档
- [[deposit-account-vs-transaction]]
- [[account-state-machine]]
- [[red-receipt-design]]
- [[deposit-first-time-renovation]]
- [[refund-full-no-damage]]
- [[forfeit-damage-public-area]]