diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json
index 5fe0bad..3d6e23a 100644
--- a/.obsidian/workspace.json
+++ b/.obsidian/workspace.json
@@ -13,12 +13,12 @@
"state": {
"type": "markdown",
"state": {
- "file": "prop-acc/scenarios/deposit/deposit-additional-topup.md",
+ "file": "prop-acc/scenarios/deposit/deposit-first-time-renovation.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
- "title": "deposit-additional-topup"
+ "title": "deposit-first-time-renovation"
}
}
]
@@ -94,7 +94,7 @@
"state": {
"type": "backlink",
"state": {
- "file": "prop-acc/scenarios/deposit/deposit-additional-topup.md",
+ "file": "prop-acc/scenarios/deposit/deposit-first-time-renovation.md",
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
@@ -104,7 +104,7 @@
"unlinkedCollapsed": true
},
"icon": "links-coming-in",
- "title": "Backlinks for deposit-additional-topup"
+ "title": "Backlinks for deposit-first-time-renovation"
}
},
{
@@ -113,12 +113,12 @@
"state": {
"type": "outgoing-link",
"state": {
- "file": "prop-acc/scenarios/deposit/deposit-additional-topup.md",
+ "file": "prop-acc/scenarios/deposit/deposit-first-time-renovation.md",
"linksCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-going-out",
- "title": "Outgoing links from deposit-additional-topup"
+ "title": "Outgoing links from deposit-first-time-renovation"
}
},
{
@@ -156,13 +156,13 @@
"state": {
"type": "outline",
"state": {
- "file": "prop-acc/scenarios/deposit/deposit-additional-topup.md",
+ "file": "prop-acc/scenarios/deposit/deposit-first-time-renovation.md",
"followCursor": false,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-list",
- "title": "Outline of deposit-additional-topup"
+ "title": "Outline of deposit-first-time-renovation"
}
},
{
@@ -197,10 +197,16 @@
},
"active": "b06ed69835363258",
"lastOpenFiles": [
- "prop-acc/concepts/prepaid/prepaid-account-vs-transaction.md",
- "prop-acc/concepts/prepaid",
+ "prop-acc/concepts/prepaid/auto-deduction-design.md",
"prop-acc/maps/deposit-knowledge-map.md",
"prop-acc/index.md",
+ "prop-acc/concepts/prepaid/consume-via-bill-collection-type.md",
+ "prop-acc/concepts/prepaid/transaction-types.md",
+ "prop-acc/concepts/prepaid/one-account-per-resident.md",
+ "prop-acc/scenarios/deposit/deposit-additional-topup.md",
+ "prop-acc/concepts/prepaid/account-state-machine.md",
+ "prop-acc/concepts/prepaid/prepaid-account-vs-transaction.md",
+ "prop-acc/concepts/prepaid",
"prop-acc/concepts/deposit/deposit-vs-adhoc-vs-prepaid.md",
"prop-acc/scenarios/deposit/audit-long-pending-accounts.md",
"prop-acc/scenarios/deposit/audit-monthly-deposit-balance.md",
@@ -218,13 +224,7 @@
"prop-acc/scenarios/deposit/refund-with-payment-channel-switch.md",
"prop-acc/scenarios/deposit/refund-partial-after-forfeit.md",
"prop-acc/scenarios/deposit/refund-full-no-damage.md",
- "prop-acc/scenarios/deposit/deposit-additional-topup.md",
- "prop-acc/concepts/adhoc/collection-order-and-receipt.md",
- "prop-acc/scenarios/deposit/deposit-on-behalf-by-company.md",
"prop-acc/scenarios/deposit",
- "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",
"prop-acc/scenarios/adhoc",
"prop-acc/concepts/adhoc",
diff --git a/prop-acc/concepts/prepaid/account-state-machine.md b/prop-acc/concepts/prepaid/account-state-machine.md
new file mode 100644
index 0000000..66880cf
--- /dev/null
+++ b/prop-acc/concepts/prepaid/account-state-machine.md
@@ -0,0 +1,155 @@
+---
+title: prop-acc · prepaid · 预存款账户状态机
+aliases:
+ - 预存款账户状态机
+ - PrepaidAccount 状态机
+ - canOperate
+tags:
+ - 概念
+ - prop-acc
+ - 预存款
+ - 状态机
+audience:
+ - 业户
+ - 业务人员
+status: 已发布
+sub_feature: prepaid
+last_review: 2026-05-25
+code_version: 2026-05-22
+---
+
+# 预存款账户状态机
+
+预存款账户三种状态:**Active(可用)** / **Frozen(冻结)** / **Closed(已关闭)**。状态机骨架与 [[../deposit/account-state-machine|押金账户]]相同,但有两条**重要差异**:
+
+1. **零余额不自动关账** —— 业户可以继续充值复用账户
+2. **没有 ForceClose** —— 一户一账 + 预存款纠纷罕见,不需要 deposit 那种 Frozen + 有余额的解困出口
+
+## 三状态速查
+
+| 状态 | 中文 | 何时进入 | 能做什么 |
+|---|---|---|---|
+| `Active` | 可用 | 新账户首次充值后 | 充值 / 消费 / 退款 / 冻结 / 关账(主动)|
+| `Frozen` | 冻结 | 风控、欺诈嫌疑、内审等 | 看流水(只读)/ 解冻 |
+| `Closed` | 已关闭 | 业户搬走 / 主动关账 / 长期失联归档 | 看流水(只读)|
+
+## 状态机图
+
+```mermaid
+stateDiagram-v2
+ [*] --> Active : 开户 + 首次充值
+ Active --> Active : 充值 / 消费 / 退款(任意次数,余额可清零回升)
+ Active --> Frozen : freeze() 风控
+ Frozen --> Active : reactivate() 等同解冻
+ Active --> Closed : close() 业户搬走等
+ Closed --> [*]
+
+ note right of Active
+ 余额 0 时不自动关账
+ 业户可继续充值复用
+ end note
+
+ note right of Frozen
+ 冻结期间禁止任何资金动作
+ canOperate = false
+ end note
+
+ note left of Closed
+ 永久终态
+ 新业务请开新账户
+ (开新账户会撞一户一账约束 —— 业务上需要重新审视)
+ end note
+```
+
+## 守护方法
+
+| 方法 | 返回 true 的状态 | 用途 |
+|---|---|---|
+| `canOperate()` | Active **only** | 统一守护:deposit / consume / refund 都调 |
+| `hasBalance()` | balance > 0 | 配合判断"账户有钱"语义 |
+| `isClosed()` | status == Closed | 给 UI / Policy 判断用 |
+
+> [!info] canOperate 是模型层的最严防御
+> 与 deposit 的 `canDeposit` / `canWithdraw` 二分不同,prepaid 用**统一** `canOperate()` —— 因为预存款的所有资金动作(充值 / 消费 / 退款)在 Frozen 状态下都要拒绝,没有"只能加不能减"的中间逻辑。
+>
+> 这条规则的实施有**三层兜底**(同 deposit):
+> - UI 层:Filament Action 的 `visible` 用 `canOperate()`
+> - Policy 层:`PrepaidAccountPolicy::deposit / consume / refund` 各自检查状态
+> - **模型层(最严)**:`PrepaidAccount::deposit() / consume() / refund()` 内置 `canOperate()` 检查
+>
+> 即使 Filament / Policy 全部绕过(tinker、artisan、第三方调用),模型方法仍会抛错。详见 [[exception-refund-on-frozen]] "三层守护" 段。
+
+## 与 deposit 状态机的关键差异
+
+### 差异 1:零余额不自动关账
+
+deposit:
+
+```
+balance > 0 ─ refund/forfeit ──→ balance == 0 ──自动──→ Closed
+```
+
+prepaid:
+
+```
+balance > 0 ─ consume/refund ──→ balance == 0 ──不自动关──→ Active(可继续充值)
+```
+
+为什么 prepaid 不自动关?
+
+| 理由 | 解释 |
+|---|---|
+| 一户一账 | 关了再开会撞 unique 约束;关掉是浪费 |
+| 业户长期使用 | 预存款是"我以后用"的账户,余额清零只意味着这一轮用完,可以继续充 |
+| 业务高频 | 业户可能下周又充值,频繁开关账户毫无意义 |
+| Bill 关联 | 关账后再有未付账单,业户充值无处去 |
+
+deposit 自动关是因为:
+
+| 理由 | 解释 |
+|---|---|
+| 业务完结 | 押金交完 → 装修完 → 退或扣 → 业务结束 |
+| 多账户合理 | 业户可以同时有装修押金、入驻押金、设备押金多个账户 |
+
+### 差异 2:没有 ForceClose
+
+[[../deposit/account-state-machine|押金账户]]有 ForceClose 解决"Frozen + 有余额 + 想关账"的困境。预存款**没有**这个机制,因为:
+
+- 一户一账,纠纷场景罕见(业户与自己的钱一般不打架)
+- 真要关 Frozen 账户:先 [[unfreeze-after-verification|解冻]] → [[refund-full-resident-moveout|退余]] → 关账
+- 业务方实际遇到再实现(issue.md Q4 "待补"段已记录)
+
+## ReactivateAccountAction = 解冻
+
+历史代码里有个 `ReactivateAccountAction`,字面意思"重新激活"。**实际行为只允许 Frozen → Active**(等同解冻),不允许 Closed → Active(那条永久禁止,同 deposit)。
+
+UI 文案已统一为"解冻"(图标 `lock-open`),与 deposit 模块对齐。
+
+> [!warning] 不要被名字误导
+> "Reactivate" 字面给人"撤销关账"的暗示,实际**做不到**。Policy 守护 + UI visible 都只在 `status === Frozen` 时显示。
+
+## 业务人员视角
+
+后台账户列表的"状态"列对应三个值:
+
+- 看到 `Active`:绿色,所有操作可用
+- 看到 `Frozen`:橙色,所有写入按钮变灰,只剩 `ReactivateAccountAction`
+- 看到 `Closed`:灰色,完全只读
+
+## 业户视角
+
+业户感受不到状态机内部,只感受到:
+
+- 余额能正常用 → Active
+- 充值 / 抵扣失败,提示"账户冻结中" → Frozen
+- 账户已关 → 无法再充值 / 抵扣,看流水可
+
+## 相关文档
+
+- [[prepaid-account-vs-transaction]]
+- [[transaction-types]]
+- [[freeze-suspected-fraud]]
+- [[unfreeze-after-verification]]
+- [[close-with-zero-balance-decision]]
+- [[exception-refund-on-frozen]]
+- [[../deposit/account-state-machine]]
diff --git a/prop-acc/concepts/prepaid/auto-deduction-design.md b/prop-acc/concepts/prepaid/auto-deduction-design.md
new file mode 100644
index 0000000..f380985
--- /dev/null
+++ b/prop-acc/concepts/prepaid/auto-deduction-design.md
@@ -0,0 +1,198 @@
+---
+title: prop-acc · prepaid · 月初批量自动抵扣设计
+aliases:
+ - 自动抵扣设计
+ - 月初批量抵账单
+ - auto-deduction-design
+ - 预存款自动消费 job
+tags:
+ - 概念
+ - prop-acc
+ - 预存款
+ - 架构设计
+ - 待补
+audience:
+ - 业务人员
+ - 架构师
+status: 草稿
+sub_feature: prepaid
+last_review: 2026-05-25
+code_version: 2026-05-22
+---
+
+# 月初批量自动抵扣设计(待补)
+
+> [!warning] 本功能**代码未实现**
+> 当前所有 consume 操作都是**业务人员手动触发**(在后台 `ConsumeAction`)。月初批量自动抵扣**是预存款产品的核心卖点之一**,但代码层暂未实现。本文档描述设计意图,等业务方明确需求后落地。
+>
+> issue.md Q4 "待补" 段记录:
+> > 月初批量自动抵扣 scheduled job:扫所有 status=Active AND balance>0 的预存款账户,找各自社区/业户下未付账单,自动调用 ConsumeFromPrepaidAccountAction 抵扣(优先抵 due_at 最早的账单)。
+
+## 为什么这个 job 重要
+
+预存款的**产品价值主张**是:
+
+> "业户预存一次,以后账单自动扣,不用月月手动操作"。
+
+如果没有自动抵扣 job:
+
+- 业户预存了 ¥5000,以为以后账单会自动扣
+- 月底账单出来,**不会自动扣**,需要业务人员手动到每个账户上点 ConsumeAction
+- 业户次月发现还欠物业费,余额还有 → 投诉
+- 业务人员手动抵扣 100+ 户 → 工作量大,容易遗漏
+
+**没有 job = 产品价值缺失 50%**。
+
+## 设计意图(待实现)
+
+### 1. 触发时机
+
+- **月初**(每月 1 日 00:30,避开 0 点高峰)
+- 或**账单生成后立即触发**(若账单生成本身也在月初,可串行)
+
+### 2. 扫描范围
+
+```sql
+-- 候选预存款账户
+SELECT id, community_id, community_user_profile_id, balance
+FROM acc_prepaid_accounts
+WHERE status = 'active'
+ AND balance > 0;
+```
+
+### 3. 对每个候选账户,找未付账单
+
+```sql
+-- 该业户未付账单(按到期日升序,先抵最早的)
+SELECT id, amount, due_at, bill_type
+FROM acc_bills
+WHERE community_id = ? -- 与账户同社区(跨社区不抵)
+ AND resident_id = ? -- 业户档案
+ AND status = 'unpaid'
+ORDER BY due_at ASC;
+```
+
+### 4. 按账户余额贪心抵扣
+
+```python
+# 伪代码
+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:
+ consume(account, bill, balance) # 部分抵(若 Bill 支持)
+ balance = 0
+```
+
+> [!info] 是否支持部分抵扣?
+> 当前 `ConsumeFromPrepaidAccountAction` 看实现是按完整账单金额走的。**部分抵扣**(余额不够全付时抵一部分)需要 `Bill.recordPayment()` 支持部分支付才能实现。若不支持,余额不够时**跳过该账单**,等下月或业户补充其他方式付。
+
+### 5. 复用既有 Action
+
+```php
+// 伪代码 — Job 内调用既有 Action 类
+app(ConsumeFromPrepaidAccountAction::class)->handle(
+ account: $account,
+ bill: $bill,
+ amount: $consumeAmount,
+);
+```
+
+**关键**:Job 不重新实现 consume 逻辑,直接调 `ConsumeFromPrepaidAccountAction` —— 与 Filament 手动触发**走同一份代码**。守护、事务、Receipt 生成全都自动复用。
+
+### 6. 失败容忍
+
+- 单笔抵扣失败(余额不够 / 账户 Frozen / 数据异常) → 跳过,继续下一个
+- 整个 Job 失败 → 记录到 `failed_jobs` 表,可重跑
+- 不允许"全部成功才提交" —— 部分账户的抵扣成功不应因其他账户失败回滚
+
+### 7. 通知策略(设计待定)
+
+| 业户场景 | 通知 |
+|---|---|
+| 余额充足,账单全抵 | 推送"5 月账单已自动抵扣,余额还有 ¥X" |
+| 余额不够,部分抵 | 推送"已抵 ¥X,还差 ¥Y 请补缴" |
+| 账户冻结无法抵 | 不主动通知(等业户自己看到欠费) |
+
+## 与手动 ConsumeAction 的关系
+
+| 维度 | 手动 ConsumeAction(已实现)| 自动 Job(待实现)|
+|---|---|---|
+| 触发 | 业务人员后台点击 | Scheduled job(crontab / Laravel Scheduler)|
+| 单次范围 | 单个账户 + 单张账单 | 全社区所有账户 + 所有未付账单 |
+| 业务上 | 个别情况(业户主动来抵)| 月度默认行为(产品价值核心)|
+| 代码 | `Filament/Resources/.../Actions/ConsumeAction.php` + `Actions/Prepaids/ConsumeFromPrepaidAccountAction` | 待添加 `Console/Commands/PrepaidAutoDeductionCommand.php`(或类似) |
+| 复用业务 Action | ✅ 直接调 ConsumeFromPrepaidAccountAction | ✅ 同上 |
+
+二者**共用业务层 Action**,只是触发方式不同。这是为什么 issue.md Q4 强调"未来批量自动抵扣 job 可直接复用此 Action"。
+
+## 数据流(自动抵扣场景)
+
+```mermaid
+sequenceDiagram
+ participant Scheduler
+ participant Job
+ participant Account[PrepaidAccount]
+ participant Bill[Bill]
+ participant Consume[ConsumeFromPrepaidAccountAction]
+ participant 数据库
+
+ Note over Scheduler: 每月 1 日 00:30
+
+ Scheduler->>Job: dispatch PrepaidAutoDeductionJob
+ Job->>数据库: SELECT 全部 Active 余额>0 的预存款账户
+ 数据库-->>Job: [account1, account2, ..., accountN]
+
+ loop 对每个 account
+ Job->>数据库: SELECT 该业户未付账单 ORDER BY due_at
+ 数据库-->>Job: [bill1, bill2, ...]
+
+ loop 对每张账单
+ alt 余额够付
+ Job->>Consume: handle(account, bill, full_amount)
+ Consume->>数据库: 建 CO + PrepaidTransaction + 更新 Bill
+ else 余额不够
+ Job->>Job: 跳过(或部分抵,看 Bill 支持)
+ end
+ end
+ end
+
+ Job->>Scheduler: 完成 + 报告(抵扣总额、失败数)
+```
+
+## 待讨论 / 决策
+
+业务方拍板前,以下问题需明确:
+
+| 问题 | 选项 |
+|---|---|
+| **触发频率** | 每月 1 次 / 每周 / 每天扫(更及时但更频繁)|
+| **触发时点** | 月初固定时间 / 账单生成事件触发 / 业户手动充值后立即触发本户 |
+| **优先级排序** | due_at 升序(最早的先) / amount 升序(小额先抵清) / 账单类型(物业费先 → 水电费后)|
+| **部分抵扣** | 支持 / 不支持 / 取决于账单类型 |
+| **失败通知** | 业户立即通知 / 月底汇总通知 / 仅后台告警 |
+| **跨社区策略** | 跨社区一律不抵(已确认) / 跨社区可抵(需重新设计) |
+| **运维监控** | 抵扣金额 / 失败率 / 跳过原因分布 |
+| **回滚机制** | 抵错怎么办?(理论上事务保证,但业务上若抵了不该抵的账单需手工补正) |
+
+## 关联场景
+
+待 job 实现后,场景文档 [[consume-batch-auto-monthly]] 会描述:
+
+- Job 执行的完整时序
+- 业户/业务人员在何处感知结果
+- 失败排查
+- 业务人员的运维介入入口
+
+## 相关文档
+
+- [[transaction-types]]
+- [[consume-via-bill-collection-type]]
+- [[consume-monthly-property-bill]]
+- [[consume-multiple-bills-priority]]
+- [[consume-batch-auto-monthly]]
+- [[one-account-per-resident]]
diff --git a/prop-acc/concepts/prepaid/consume-via-bill-collection-type.md b/prop-acc/concepts/prepaid/consume-via-bill-collection-type.md
new file mode 100644
index 0000000..59ab376
--- /dev/null
+++ b/prop-acc/concepts/prepaid/consume-via-bill-collection-type.md
@@ -0,0 +1,245 @@
+---
+title: prop-acc · prepaid · Consume 走 CollectionType=Bill 的设计
+aliases:
+ - Consume 走 Bill
+ - CollectionType Bill vs Prepaid
+ - 预存款消费的资金流设计
+ - consume-via-bill-collection-type
+tags:
+ - 概念
+ - prop-acc
+ - 预存款
+ - 架构决策
+audience:
+ - 业务人员
+ - 架构师
+ - 财务
+status: 已发布
+sub_feature: prepaid
+last_review: 2026-05-25
+code_version: 2026-05-22
+---
+
+# Consume 走 CollectionType=Bill 的设计
+
+预存款抵扣账单(consume)产生的 `CollectionOrder`,**`collection_type` 字段用 `Bill` 而非 `Prepaid`** —— 这是 prepaid 模块**最独特**的设计决策。本文说明为什么这么设计、对业户感知 / 账单状态 / 报表的影响。
+
+## 核心结论(一句话)
+
+> [!tip] Consume 产生的 CollectionOrder 是「账单视角」的收款单,不是「预存款视角」的支出单。
+> - `collection_type = Bill`(账单已收款)
+> - `actual_amount = +N`(正数,不是红字)
+> - `meta.fund_source = prepaid`(标资金来源)
+> - 触发 Receipt 与现金 / 微信付账单一样,文案"物业费 ¥N"
+
+## 设计对比表
+
+| 视角 | 选项 A:Consume 走 type=Prepaid | 选项 B(已采用):Consume 走 type=Bill |
+|---|---|---|
+| **CollectionOrder.collection_type** | Prepaid | **Bill** |
+| **CollectionOrder.actual_amount** | -800(红字,预存款减少) | **+800**(正数,账单收款)|
+| **Bill 状态** | 需另查找 / 另机制更新 | **自然走 Bill 收款流,翻 Paid** |
+| **Receipt 文案** | "预付款消费 ¥-800" | **"物业费 ¥800"** |
+| **业户感知** | 知道"用预存款付了" | **不感知差异,跟现金付一样** |
+| **报表** | 分两个 type 求和 | **Bill 收款 SUM 直接拿到所有账单收入** |
+| **资金来源追溯** | 关联流水可查 | **meta.fund_source = 'prepaid'** 直接标 |
+
+## 业务模型图
+
+```mermaid
+flowchart TD
+ A[业户充值 5000] --> B[PrepaidAccount.balance=5000]
+
+ C[月底 物业费账单 800] --> D{业户付款方式}
+
+ D -->|现金| E[CollectionOrder
type=Bill
actual=+800
payment=现金] --> F[Bill 翻 Paid
+ Receipt 物业费 ¥800]
+
+ D -->|微信| G[CollectionOrder
type=Bill
actual=+800
payment=微信] --> F
+
+ D -->|**预存款抵扣**| H[CollectionOrder
**type=Bill**
actual=+800
meta.fund_source=prepaid] --> F
+ H --> I[同时 PrepaidTransaction
type=consume
amount=800
balance 5000→4200]
+```
+
+无论怎么付,**Bill 视角看到的都是一笔 Bill 收款** —— 状态翻 Paid,收据"物业费 ¥800"。资金来源(现金 / 微信 / 预存款)只是不同的"付款渠道",最终都是账单已付。
+
+## 为什么这么设计
+
+### 1. 业户感知一致
+
+业户收到的物业费收据**长一样**:"物业费 ¥800"。不会因为付款方式不同,收据就变成"预付款消费 ¥-800" + "物业费 ¥800" 两张分裂凭证。
+
+### 2. 账单状态机自然
+
+`Bill` 模型有自己的状态机(Unpaid → Paid → Settled)。Bill 翻 Paid 的逻辑是"收到对应金额的 CollectionOrder(type=Bill, Completed)"。
+
+如果 prepaid consume 走 type=Prepaid,Bill 状态机需要**单独识别"预存款抵扣"这种例外情况**,代码复杂度激增。走 type=Bill 让"账单已收款"这条路径**统一**,无论付款方式如何。
+
+### 3. 报表友好
+
+物业财务的"账单收入"报表:
+
+```sql
+-- 所有账单收入(无论付款方式)
+SELECT SUM(actual_amount)
+FROM acc_collection_orders
+WHERE collection_type = 'Bill'
+ AND status = 'completed';
+```
+
+走 type=Bill 让所有账单收入**自动归类**。
+
+如果走 type=Prepaid,得做更复杂的 JOIN(找出 Prepaid 类型的红字 CO,关联流水的 related_bill_id,反推回账单收入),容易漏算 / 双算。
+
+### 4. 与其他模块对齐
+
+将来 deposit 的"业务结算"如果也想抵账单(罕见但可能),走同样模式 —— `CollectionOrder.type=Bill + meta.fund_source=deposit`。这种**资金来源标记**模式可推广。
+
+## 资金来源标记(`meta.fund_source`)
+
+`CollectionOrder.meta` 是 JSON,加 `fund_source` 字段标记钱的来源:
+
+```json
+{
+ "fund_source": "prepaid",
+ "prepaid_account_id": 123,
+ "prepaid_transaction_id": 456
+}
+```
+
+对账时可查:
+
+```sql
+-- 上月通过预存款抵扣的账单总额
+SELECT SUM(actual_amount)
+FROM acc_collection_orders
+WHERE collection_type = 'Bill'
+ AND JSON_EXTRACT(meta, '$.fund_source') = 'prepaid'
+ AND completed_at BETWEEN '2026-04-01' AND '2026-04-30 23:59:59';
+```
+
+可能的 `fund_source` 值(枚举 `FundSource`):
+
+| 值 | 含义 |
+|---|---|
+| `external` | 外部支付(默认,现金 / 微信 / POS / 银行转账)|
+| `prepaid` | 预付账户余额抵扣 |
+| `deposit` | 保证金抵扣(罕见,未启用)|
+| `overpayment` | 多付款项转入(罕见)|
+| `credit` | 信用额度(预留)|
+
+`FundSource::Prepaid` 是 `consume` 唯一会用到的值。
+
+## 流水台账(完整对照)
+
+业户陈先生充 ¥5,000 → 抵 ¥800 物业费 → 抵 ¥1,200 水电费 → 退余 ¥3,000 的完整记录:
+
+### CollectionOrder(收款单)
+
+| CO | type | actual_amount | status | meta.fund_source | 关联 |
+|---|---|---|---|---|---|
+| 1 | Prepaid | +5,000 | Completed | external | — |
+| 2 | **Bill** | **+800** | Completed | **prepaid** | Bill #物业费 5月 |
+| 3 | **Bill** | **+1,200** | Completed | **prepaid** | Bill #水电费 5月 |
+| 4 | Prepaid | **-3,000** | Completed | external(退到银行) | — |
+
+### PrepaidTransaction(流水)
+
+| TX | type | amount | balance_before | balance_after | 关联 CO | related_bill_id |
+|---|---|---|---|---|---|---|
+| 1 | deposit | 5000 | 0 | 5000 | CO #1 | — |
+| 2 | consume | 800 | 5000 | 4200 | CO #2 | Bill #物业费 |
+| 3 | consume | 1200 | 4200 | 3000 | CO #3 | Bill #水电费 |
+| 4 | refund | 3000 | 3000 | 0 | CO #4 | — |
+
+### Receipt(凭证)
+
+| Receipt | amount | 文案 |
+|---|---|---|
+| 1 | +5,000 | "预付款充值 ¥5,000" |
+| 2 | **+800** | **"物业费 ¥800(5月)"** |
+| 3 | **+1,200** | **"水电费 ¥1,200(5月)"** |
+| 4 | -3,000 | "预付款退款 ¥-3,000"(红字)|
+
+业户视角看 Receipt #2 #3 = 普通账单收据(跟现金 / 微信付一样),感知不到是预存款抵的。
+
+## Listener `generatePrepaidReceiptItems`
+
+收据 line items 生成由 Listener 处理。逻辑:
+
+```php
+// 伪代码
+when (PrepaidTransaction::created)
+ switch ($transaction->type) {
+ case 'deposit':
+ Receipt::createItem("预付款充值", $transaction->amount);
+ break;
+ case 'consume':
+ // 走 Bill 渠道,Receipt 由 Bill 那边的 Listener 生成
+ // 这里不处理(避免重复)
+ break;
+ case 'refund':
+ Receipt::createItem("预付款退款", -$transaction->amount); // 取负号
+ break;
+ case 'adjustment':
+ Receipt::createItem("预付款调整", $direction * $transaction->amount);
+ break;
+ }
+```
+
+Consume 走 Bill 渠道,Receipt items 由 Bill 那边的 Listener 生成(关联的账单类型决定文案"物业费" / "水电费" 等)。
+
+## 业务人员视角
+
+后台:
+
+- 预存款账户详情看 PrepaidTransaction 流水(每笔有 consume / deposit / refund / adjustment 标记)
+- 关联的 CollectionOrder 列表(consume 的 CO 在"收款单"列表里显示 type=Bill,与现金付的账单看起来一样)
+- 业户在小程序看到的是"账单已付,付款方式:预存款抵扣"
+
+## 财务视角
+
+月度报表查询:
+
+```sql
+-- 本月物业费收入(所有付款方式合计)
+SELECT SUM(actual_amount) FROM acc_collection_orders
+WHERE collection_type = 'Bill'
+ AND status = 'completed'
+ AND completed_at BETWEEN '2026-05-01' AND '2026-05-31 23:59:59';
+-- 这条 SQL 自然涵盖现金 / 微信 / POS / 预存款抵扣
+
+-- 本月通过预存款付的账单
+SELECT SUM(actual_amount) FROM acc_collection_orders
+WHERE collection_type = 'Bill'
+ AND status = 'completed'
+ AND JSON_EXTRACT(meta, '$.fund_source') = 'prepaid'
+ AND completed_at BETWEEN '2026-05-01' AND '2026-05-31 23:59:59';
+-- 这条找出"用预存款付的部分"
+```
+
+两者差值就是"现金 / 微信 / POS / 转账" 等其他渠道的账单收入。
+
+## 常见问题
+
+> [!question] 业户能查到这张账单"是用预存款付的"吗?
+> 后台可以(看 CO 的 `meta.fund_source`)。小程序业户侧默认显示"付款方式:预存款抵扣"(从 meta 读)。
+
+> [!question] 这种设计未来扩展性怎样?
+> 极好。比如将来:
+> - 业户用"积分"抵账单 → CollectionOrder(type=Bill, meta.fund_source=points)
+> - 业户用"信用额度" → meta.fund_source=credit
+> - 业户用"保证金抵扣"(若允许) → meta.fund_source=deposit
+>
+> 所有都遵循同样模式:**Bill 视角统一,资金来源标在 meta**。
+
+> [!question] consume 同时建 CO 和 PrepaidTransaction,如果中间出错怎么办?
+> 整个 `ConsumeFromPrepaidAccountAction` 在**一个事务**里:CO + PrepaidTransaction + Bill 状态更新 + Receipt 生成都在同一事务,任何一步失败回滚。
+
+## 相关文档
+
+- [[transaction-types]]
+- [[prepaid-account-vs-transaction]]
+- [[consume-monthly-property-bill]]
+- [[consume-multiple-bills-priority]]
+- [[auto-deduction-design]]
+- [[../adhoc/collection-order-and-receipt|CollectionOrder 与 Receipt]]
diff --git a/prop-acc/concepts/prepaid/one-account-per-resident.md b/prop-acc/concepts/prepaid/one-account-per-resident.md
new file mode 100644
index 0000000..d3d03e5
--- /dev/null
+++ b/prop-acc/concepts/prepaid/one-account-per-resident.md
@@ -0,0 +1,168 @@
+---
+title: prop-acc · prepaid · 一户一账约束
+aliases:
+ - 一户一账
+ - 预存款一户一账
+ - one-account-per-resident
+ - 跨社区防御
+tags:
+ - 概念
+ - prop-acc
+ - 预存款
+ - 数据约束
+audience:
+ - 业户
+ - 业务人员
+ - 架构师
+status: 已发布
+sub_feature: prepaid
+last_review: 2026-05-25
+code_version: 2026-05-22
+---
+
+# 一户一账约束
+
+预存款账户**每个社区每业户最多一个**。数据库唯一约束 + 业务层跨社区防御**双层保证**。这是 prepaid 子模块**最重要的独有约束**,影响开户、消费、跨社区调度的所有流程。
+
+## 约束的形式化
+
+```sql
+UNIQUE INDEX (community_id, community_user_profile_id)
+ON acc_prepaid_accounts;
+```
+
+- 同业户在同社区 → 只能有 1 个预存款账户
+- 同业户在不同社区 → 可以各自有 1 个预存款账户(多社区独立)
+
+## 为什么要一户一账
+
+> [!info] 类比
+> 业户在同一家超市办储值卡,通常**只有一张**。两张卡难管理(余额分散、要充两次、消费时选哪张),业务上无价值。
+
+具体好处:
+
+| 好处 | 解释 |
+|---|---|
+| **抵扣账单清晰** | 业户某月物业费 ¥800,系统找该业户的预存款账户(唯一),余额够就扣,不够就提示充值 |
+| **业户体验简单** | 业户在小程序只看到 1 个账户,1 个余额,不用选 |
+| **报表对账简单** | "本社区所有预存款余额" = sum(prepaid_accounts.balance where community_id=X),无重复计算 |
+| **避免资金分散** | 业户分散在两个账户各 ¥500,某月账单 ¥800,系统抵不动(单账户余额不够);合并 ¥1000 就能抵 |
+
+## 与 deposit 的对比
+
+| 维度 | DepositAccount | PrepaidAccount |
+|---|---|---|
+| 每业户账户数 | **多个**(按 fee_type、按 asset) | **1 个**(每社区) |
+| 唯一约束 | 无(可重复)| `(community_id, community_user_profile_id)` |
+| 业务理由 | 不同押金种类性质不同(装修押金 vs 入驻押金 vs 设备押金) | 预存款只是"钱包",细分无意义 |
+
+deposit 的多账户合理:同业户可以有"装修保证金 ¥5000" + "入驻押金 ¥2000" + "设备押金 ¥1000",各自独立结算。prepaid 的单账户合理:同业户只有一个"预存款钱包",钱都在一起。
+
+## 跨社区防御
+
+业户可能**同时入住多个社区**(例如自住 + 投资房在另一社区)。两个社区各有自己的预存款账户(`community_id` 不同,各自唯一)。这是**正常情况**,系统允许。
+
+但**禁止跨社区消费** —— A 社区的预存款不能抵 B 社区的账单。这条由 `PrepaidAccount::consume()` 方法**内置守护**:
+
+```php
+public function consume(Bill $bill, ...): PrepaidTransaction
+{
+ // 跨社区防御
+ if ($bill->community_id !== $this->community_id) {
+ throw new InvalidArgumentException(
+ '预存款账户与账单不在同一社区,无法抵扣'
+ );
+ }
+ // ...
+}
+```
+
+> [!warning] 即使账户表面可"逻辑抵扣"也禁止
+> 假设业户在 A 社区有 ¥5000 预存,B 社区有 ¥800 物业费账单。逻辑上"业户的钱够付"。但**仍禁止跨社区**抵扣,理由:
+>
+> - 每个社区财务独立核算,跨社区抵扣等于在两个社区之间转账(账面对不上)
+> - 业户与 A 社区物业的合同关系,与 B 社区无关
+> - 物业管理通常按社区独立公司化运营
+>
+> 业户要付 B 社区账单 → 在 B 社区充值新预存款,或直接现金 / 微信付。
+
+详见 [[exception-cross-community-consume]]。
+
+## 开户时的约束行为
+
+业务人员尝试为某社区某业户**再次**开预存款账户时,系统会:
+
+1. UI 层:`PrepaidAccountForm` 在提交前 SQL 查询是否已存在(可前置友好提示)
+2. 数据库层:即使 UI 绕过,unique 约束抛 SQLSTATE 23000(duplicate entry)
+
+**业务上**:同业户同社区只允许一次开户。如果业户旧账户已 Closed(搬走过又回来),通常做法是:
+
+| 旧账户状态 | 业务处理 |
+|---|---|
+| Active | 不开新账户,继续用 |
+| Frozen | 先解冻,继续用 |
+| Closed + balance=0 | **目前阻塞**:开新会撞 unique。需要业务层加 `where status != 'closed'` 软约束,或允许"重启" Closed 账户(目前不支持)|
+| Closed + balance>0 | 罕见情况(类似 retain),业务层应避免 |
+
+> [!warning] 已知设计 gap
+> 当前 unique 约束**没有过滤 Closed 状态**,等于"账户一旦关了就不能再开"。如果有"业户搬走又回来"的真实场景,需要后续讨论:
+> - 改约束为 `WHERE status != 'closed'`(部分数据库支持)
+> - 或允许 reopen Closed 账户(目前 deposit/prepaid 都禁)
+> - 或运维 / 业务流程上把 closed 账户改名(改业户 id?)
+>
+> 暂时按"业户搬走绝大多数不会回来"的假设运行。issue.md Q4 未列入待补,业务方未提出。
+
+## 业户视角
+
+业户基本感受不到这条约束 —— 您本来就只期望"一个钱包":
+
+- 同社区:看到一个余额、一份流水
+- 跨社区(若有):分别看,不混淆
+
+唯一可见的影响:跨社区购物 / 缴费时,不能用 A 社区的钱付 B 社区的账。
+
+## 业务人员视角
+
+开户时:
+
+- 输入业户档案 → 系统检查该业户在本社区是否已有账户
+- 已有 Active / Frozen → 提示"已有账户,请直接充值",跳到 `ViewPrepaidAccount`
+- 已有 Closed → 当前阻塞,需特殊处理(联系运维)
+- 无 → 走正常开户流程([[deposit-first-time]])
+
+消费时:
+
+- 系统找业户在本社区的预存款账户(唯一)
+- 余额够 → 抵扣
+- 余额不够 → 提示业户先充值
+
+## 系统视角
+
+抵扣账单的查找逻辑:
+
+```php
+// 伪代码 — 找业户的预存款账户
+$account = PrepaidAccount::query()
+ ->where('community_id', $bill->community_id)
+ ->where('community_user_profile_id', $bill->resident_id)
+ ->where('status', PrepaidAccountStatus::Active)
+ ->first(); // ← 因为 unique,first() 直接拿到唯一一个(或 null)
+
+if (! $account || $account->balance < $bill->amount) {
+ // 余额不够 / 没账户
+ return BillPaymentResult::insufficient();
+}
+
+$account->consume($bill, $bill->amount);
+```
+
+唯一约束让 `first()` 100% 准确(理论上不需要 `firstOrFail()` 之类容错)。
+
+## 相关文档
+
+- [[prepaid-account-vs-transaction]]
+- [[account-state-machine]]
+- [[consume-via-bill-collection-type]]
+- [[exception-cross-community-consume]]
+- [[deposit-first-time]]
+- [[../cross/concepts/resident|业户]]
diff --git a/prop-acc/concepts/prepaid/transaction-types.md b/prop-acc/concepts/prepaid/transaction-types.md
new file mode 100644
index 0000000..b1be586
--- /dev/null
+++ b/prop-acc/concepts/prepaid/transaction-types.md
@@ -0,0 +1,135 @@
+---
+title: prop-acc · prepaid · 预存款流水类型
+aliases:
+ - 预存款流水类型
+ - PrepaidTransactionType
+ - 四种流水
+tags:
+ - 概念
+ - prop-acc
+ - 预存款
+ - 业务字典
+audience:
+ - 业户
+ - 业务人员
+status: 已发布
+sub_feature: prepaid
+last_review: 2026-05-25
+code_version: 2026-05-22
+---
+
+# 预存款流水类型(PrepaidTransactionType)
+
+预存款账户的资金变动有 **4 种类型**:**deposit(充值)** / **consume(消费抵扣)** / **refund(退款)** / **adjustment(调整)**。其中 **consume 是最高频操作**(每月业户账单都触发),与 [[../deposit/transaction-types|押金的 3 种流水]]在数量和语义上都有差异。
+
+## 4 种流水速查
+
+| 类型 | 中文 | 余额方向 | 触发 Action | 关联对象 | Receipt 类型 |
+|---|---|---|---|---|---|
+| `Deposit` | 充值 | + 增加 | [[deposit-first-time]] / [[deposit-additional-topup]] | CollectionOrder(type=Prepaid, +N)| 正数,"预付款充值" |
+| `Consume` | 消费抵扣 | − 减少 | [[consume-monthly-property-bill]] / [[consume-batch-auto-monthly]] | CollectionOrder(**type=Bill**, +N) + Bill | 正数(走账单方向),"物业费 ¥800" |
+| `Refund` | 退款 | − 减少 | [[refund-full-resident-moveout]] / [[refund-partial-after-consume]] | CollectionOrder(type=Prepaid, **−N 红字**) | 负数(红字),"预付款退款 ¥-X" |
+| `Adjustment` | 调整 | ± | (无 UI 入口,运维场景)| 无强制关联 | 视方向选词 |
+
+## amount 一律为正数
+
+与 deposit 同样规则:`PrepaidTransaction.amount` **永远是正数**,方向由 `type` 表达。负号只出现在关联的 `CollectionOrder.actual_amount`(refund 时)和 Receipt amount(refund 时)。
+
+举例(业户充 ¥5000 → 抵扣 ¥800 物业费 → 抵扣 ¥1200 水电费 → 退余 ¥3000):
+
+| 流水 | type | amount | balance_before | balance_after |
+|---|---|---|---|---|
+| 1 | Deposit | 5000 | 0 | 5000 |
+| 2 | Consume | 800 | 5000 | 4200 |
+| 3 | Consume | 1200 | 4200 | 3000 |
+| 4 | Refund | 3000 | 3000 | 0 |
+
+最后余额 0,**账户保持 Active**(不像 deposit 自动 Closed)。详见 [[account-state-machine]] "零余额不自动关账" 段。
+
+## Consume 的特殊性
+
+`consume` 是预存款**独有**且**高频**的操作类型。三个关键特性:
+
+### 1. 关联具体账单(`related_bill_id`)
+
+每笔 consume 必须关联一张被抵扣的 [[../../cross/concepts/work-order|账单(Bill)]],流水的 `related_bill_id` 字段记录这个关联。审计时可追溯"这笔消费是抵的哪张账单"。
+
+### 2. CollectionOrder 用 type=Bill 而非 Prepaid
+
+这是**最关键**的设计 —— 详见 [[consume-via-bill-collection-type]]。简而言之:从账单视角,consume 是"账单收款完成";从预存款视角,consume 是"余额扣减"。CollectionOrder 用 `type=Bill` 让账单收款流统一,资金来源标在 `meta.fund_source=prepaid`。
+
+### 3. Receipt 走账单方向(正数)
+
+普通 deposit 模块的退款 / 扣罚出红字 Receipt(`amount=-N`)。但 prepaid 的 consume **出正数 Receipt**,文案是"物业费 ¥800",对业户而言这就是一张**普通的账单收款收据** —— 跟现金 / 微信付物业费拿到的收据完全一样。
+
+这种设计的好处:**业户感知一致** —— 不管钱怎么付(现金 / 微信 / 预存款抵扣),收据都长一样,业务上更直观。
+
+## Refund 的设计
+
+predpaid 的 refund 与 deposit 的 refund **几乎一样**:
+
+- 建红字 CollectionOrder(`actual_amount=-N`)
+- 加 `PrepaidTransaction(type=refund, amount=正数)`
+- 触发红字 Receipt"预付款退款 ¥-X"
+
+**唯一差异**:不自动关账(deposit 余额清零自动 Closed,prepaid 不自动)。详见 [[refund-partial-after-consume]]。
+
+## Adjustment 的设计取舍
+
+> [!warning] Adjustment 没有 UI 入口
+> 与 deposit 模块的 adjustment 一样,**保留 enum case 但不提供前台 UI**。理由(同 deposit issue.md Q3):
+>
+> - 余额修正给前台开后门,审计大忌
+> - 任何错误应通过 deposit / consume / refund 组合补正
+> - 留 enum 给运维场景:tinker / artisan / 一次性数据迁移脚本
+
+具体修正套路(以"业户充值时多录了 ¥1,000"为例):
+
+| 错误做法 | 正确做法 |
+|---|---|
+| 直接改余额 -1000 | 建一笔 `Refund` ¥1,000,备注"录错金额修正",业户拿到红字"预付款退款 ¥-1,000" |
+
+修正的完整凭证链路完整保留,审计可追责。
+
+## Receipt 文案(Listener `generatePrepaidReceiptItems`)
+
+按 `PrepaidTransaction.type` 选词:
+
+| type | Receipt 文案 |
+|---|---|
+| `deposit` | "预付款充值 ¥N" |
+| `consume` | 走 Bill 渠道,文案按账单类型("物业费"、"水电费" 等) |
+| `refund` | "预付款退款 ¥-N"(负数,红字) |
+| `adjustment` | "预付款调整 ¥±N"(罕见) |
+
+## 业户视角
+
+业户在小程序"我的预存款"流水里看到:
+
+```
+2026-05-20 -1,200.00 抵扣 水电费(5月) consume
+2026-05-15 -800.00 抵扣 物业费(5月) consume
+2026-05-01 +5,000.00 预付款充值 deposit
+2026-04-30 -3,000.00 预付款退款(余额提取) refund
+2026-04-20 -500.00 抵扣 物业费(4月,部分) consume
+```
+
+每笔有时间、金额方向、说明、类型。点击可看关联的账单 / 收据。
+
+## 业务人员视角
+
+后台 → 预存款 → 账户详情 → 流水标签。
+
+- 上方按钮:充值 / 消费 / 退款 / 冻结 / 解冻 / 关账
+- 按钮可见性由 [[account-state-machine|canOperate()]] 守护:Frozen / Closed 状态下全部灰化
+- 没有"修改 / 删除流水"按钮 —— 不可变设计
+
+## 相关文档
+
+- [[prepaid-account-vs-transaction]]
+- [[account-state-machine]]
+- [[consume-via-bill-collection-type]]
+- [[auto-deduction-design]]
+- [[consume-monthly-property-bill]]
+- [[refund-full-resident-moveout]]
+- [[../deposit/transaction-types]]