From 341bbb77d6a25720f18ca0cc6b4cd46583dfe849 Mon Sep 17 00:00:00 2001 From: Willie Date: Mon, 25 May 2026 22:07:35 +0800 Subject: [PATCH] vault backup: 2026-05-25 22:07:35 --- .obsidian/workspace.json | 14 +- .../concepts/deposit/account-state-machine.md | 133 ++++++++++++++++++ .../deposit/deposit-account-vs-transaction.md | 125 ++++++++++++++++ prop-acc/concepts/deposit/payer-types.md | 102 ++++++++++++++ .../concepts/deposit/red-receipt-design.md | 130 +++++++++++++++++ .../concepts/deposit/transaction-types.md | 121 ++++++++++++++++ 6 files changed, 618 insertions(+), 7 deletions(-) create mode 100644 prop-acc/concepts/deposit/account-state-machine.md create mode 100644 prop-acc/concepts/deposit/deposit-account-vs-transaction.md create mode 100644 prop-acc/concepts/deposit/payer-types.md create mode 100644 prop-acc/concepts/deposit/red-receipt-design.md create mode 100644 prop-acc/concepts/deposit/transaction-types.md diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index 93ea41b..4518254 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -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" ] } \ No newline at end of file diff --git a/prop-acc/concepts/deposit/account-state-machine.md b/prop-acc/concepts/deposit/account-state-machine.md new file mode 100644 index 0000000..8d5108b --- /dev/null +++ b/prop-acc/concepts/deposit/account-state-machine.md @@ -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]] diff --git a/prop-acc/concepts/deposit/deposit-account-vs-transaction.md b/prop-acc/concepts/deposit/deposit-account-vs-transaction.md new file mode 100644 index 0000000..bde5c75 --- /dev/null +++ b/prop-acc/concepts/deposit/deposit-account-vs-transaction.md @@ -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
type=deposit, amount=5000] + A --> C[CollectionOrder
actual=5000] + C --> D[Receipt
amount=5000] + B -.关联.-> C + + E[退款 5000] --> F[DepositTransaction
type=refund, amount=5000] + E --> G[CollectionOrder
actual=-5000 红字] + G --> H[Receipt
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]] diff --git a/prop-acc/concepts/deposit/payer-types.md b/prop-acc/concepts/deposit/payer-types.md new file mode 100644 index 0000000..6c68770 --- /dev/null +++ b/prop-acc/concepts/deposit/payer-types.md @@ -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]] +- [[业户]] +- [[房屋单元]] diff --git a/prop-acc/concepts/deposit/red-receipt-design.md b/prop-acc/concepts/deposit/red-receipt-design.md new file mode 100644 index 0000000..3903f1d --- /dev/null +++ b/prop-acc/concepts/deposit/red-receipt-design.md @@ -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
type=deposit, amount=5000] + A1 --> A3[CollectionOrder
actual=+5000] + A3 --> A4[Receipt
amount=+5000
「装修保证金缴纳 ¥5,000」] + end + + subgraph "退款(红字)" + B1[业户退 3000] --> B2[DepositTransaction
type=refund, amount=3000] + B1 --> B3[CollectionOrder
actual=-3000 红字] + B3 --> B4[Receipt
amount=-3000 红字
「装修保证金退还 ¥-3,000」] + end + + subgraph "扣罚(红字)" + C1[物业扣 2000] --> C2[DepositTransaction
type=forfeiture, amount=2000] + C1 --> C3[CollectionOrder
actual=-2000 红字] + C3 --> C4[Receipt
amount=-2000 红字
「装修保证金扣罚 ¥-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]] diff --git a/prop-acc/concepts/deposit/transaction-types.md b/prop-acc/concepts/deposit/transaction-types.md new file mode 100644 index 0000000..25d2fe3 --- /dev/null +++ b/prop-acc/concepts/deposit/transaction-types.md @@ -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]]