Files
uniprop-manual/prop-acc/scenarios/billing/collect-payment-single.md
2026-05-26 01:03:15 +08:00

259 lines
7.5 KiB
Markdown

---
title: prop-acc · billing · 场景 - 单张账单收款
aliases:
- 单张收款
- 收款
- CollectPaymentAction
- collect-payment-single
- 场景-单张账单收款
tags:
- 场景
- prop-acc
- 账单
- 收款
audience:
- 业户
- 业务人员
status: 已发布
sub_feature: billing
last_review: 2026-05-26
code_version: 2026-05-22
---
# 场景:单张账单收款
业户**单张账单付款**(物业费 / 水费 / 电费的某一张),业务人员后台触发 `CollectPaymentAction`。最基础高频的收款场景。
## 典型情境
> [!example] 真实情境
> 张阿姨 5 月物业费 ¥800 账单已生成,她下午到物业前台:
>
> - "我交 5 月物业费"
> - 业务人员小李打开张阿姨账户 → 找到 5 月物业费账单 → 收款
## 业户视角
### 第 1 步:到前台 / 小程序
带:
- 钱(现金 / 微信 / POS 卡)
- 房号 / 姓名(身份证)
### 第 2 步:告诉业务人员要付哪张账单
> "我交 5 月物业费"
业务人员从系统找到对应账单。
### 第 3 步:确认金额 + 付款方式
业务人员告诉张阿姨:
> "您 5 月物业费 ¥800,请选付款方式"
| 付款方式 | 操作 |
|---|---|
| 现金 | 给钱 → 找零 |
| 微信扫码 | 业务人员出示物业收款码 → 业户扫 |
| POS 刷卡 | 业户给银行卡 → POS 机刷 |
| 银行转账 | 业户给凭证(线下转账已到账)|
### 第 4 步:拿收据
- 纸质:**当场打印**
- 电子:发到业户微信 / 邮箱
> [!success] 完成
> 账单状态从 Unpaid → Paid。业户带收据离开,5 分钟内搞定。
## 业务人员视角
### 第 1 步:找账单
后台 → 账单 → 列表 → 按业户姓名 / 房号 / 期次过滤 → 找到张阿姨的 5 月物业费(Unpaid,¥800)→ 进 `ViewBill`
或者:
- 后台 → 业户 → 找到张阿姨 → "她的账单"标签 → 看 Unpaid 列表 → 选
### 第 2 步:点击 `CollectPaymentAction`(标签"收款")
右上角状态管理组。
> [!warning] 按钮可见性
> `CollectPaymentAction` 守护:`canBePaid()`(Unpaid / Partial)+ `->authorize('collect')`。Paid / Void / Suspended / Processing 灰化。
Modal 表单:
| 字段 | 填什么 |
|---|---|
| **收款金额** | ¥800(默认全额,可改为部分)|
| **支付方式(`payment_channel_id`)** | 现金 / 微信 / POS / 银行转账 |
| **收款银行账户(`bank_account_id`)** | 微信/POS/转账选对应银行账户;现金可空 |
| **收款备注** | 选填,如"业户现场付款" |
### 第 3 步:提交
系统在**一个事务**内:
1. 校验 Bill 可付(`canBePaid` = Unpaid / Partial)
2. 校验金额 ≤ Bill 剩余应付(`amount - paid_amount`)
3.`CollectionOrder`(`type=Bill`,`actual_amount=+800`,`status=Completed`,`payment_channel`,`meta.fund_source=external`)
4.`CollectionOrderBill`(`bill_id`,`collection_order_id`,`allocated_amount=800`)
5. 更新 `Bill.paid_amount += 800`
6. 更新 `Bill.status`:若 paid_amount = amount → `Paid`;若 < amount → `Partial`
7. 触发 `CollectionOrderCompleted` 事件
8. Listener 自动建 `Receipt` + `ReceiptItem`(文案"物业费 ¥800")
9. 写 activitylog(可选,具体看实现)
### 第 4 步:给业户收据
后台找到新生成 Receipt → 打印 / 微信发。
## 系统流程
```mermaid
sequenceDiagram
participant 业户
participant 业务[业务人员]
participant Filament
participant Action[CollectPaymentAction]
participant DB
participant Listener
业户->>业务: 付物业费 800
业务->>Filament: ViewBill → CollectPaymentAction(modal)
Filament->>Action: handle(bill, 800, channel, bank)
Action->>Action: canBePaid()? Unpaid=true
Action->>Action: 800 ≤ remaining(800)? yes
Action->>DB: 开启事务
Action->>DB: 1. 建 CollectionOrder(type=Bill, +800, Completed)
Action->>DB: 2. 建 CollectionOrderBill(allocated=800)
Action->>DB: 3. Bill.paid_amount=800
Action->>DB: 4. Bill.status=Paid(800=800)
Action->>Listener: 5. 触发 CollectionOrderCompleted
Listener->>DB: 6. 建 Receipt("物业费 ¥800")
Action->>DB: 提交事务
Filament-->>业务: 成功通知
业务-->>业户: 收据
```
## 部分付场景(business 上常见)
业户只想付 ¥300(全额 ¥800):
```
Modal 表单:
- 收款金额:300(手动改)
- 支付方式:现金
- 备注:"暂时只能付一部分"
```
提交后:
- Bill.paid_amount = 300
- Bill.status: Unpaid → **Partial**
- 建 CollectionOrder(+300)+ CollectionOrderBill(allocated=300)
业户后续补付 ¥500 → 同样走 `CollectPaymentAction` → 第 2 笔 CollectionOrderBill → Bill 收齐 → Paid。
详见 [[exception-partial-payment]]。
## 数据示例(完整流水)
业户付 ¥800 后:
### Bill 表
| 字段 | 值 |
|---|---|
| `id` | 12345 |
| `amount` | 800 |
| `paid_amount` | 800 |
| `status` | Paid |
### CollectionOrderBill 表
| 字段 | 值 |
|---|---|
| `bill_id` | 12345 |
| `collection_order_id` | 67890 |
| `allocated_amount` | 800 |
### CollectionOrder 表
| 字段 | 值 |
|---|---|
| `id` | 67890 |
| `collection_type` | Bill |
| `actual_amount` | +800 |
| `payment_channel_id` | 微信 |
| `status` | Completed |
| `meta.fund_source` | external |
### Receipt 表
| 字段 | 值 |
|---|---|
| `collection_order_id` | 67890 |
| `amount` | +800 |
| line_items | [{ 物业费(5月), 800 }] |
## 常见问题
> [!question] 业户付的钱与账单金额不一致(多付 / 少付)?
> | 情况 | 处置 |
> |---|---|
> | 少付 | 走部分付 Partial 状态;后续补付 |
> | **多付** | **不应该**(Modal 守护 `amount ≤ remaining`)。如果业务上业户给的钱多,**找零给业户**(系统层面只收账单的金额) |
> [!question] 业户付错账单(本想付物业费,付到电费)?
> 业务人员可:
> - 立即作废这笔 CollectionOrder(走 [[void-paid-bill]] 类似流程)+ 重新对正确账单收款
> - 或者**留这笔不动**(资金确实到账)+ 业务人员手工调整 CollectionOrderBill 的 allocated_amount(危险,会破坏审计)
>
> **推荐第一种**(走作废 + 重收)。
> [!question] Frozen Bill 能收款吗?
> 不能。`canBePaid()` 只允许 Unpaid / Partial。Suspended 状态需先 [[resume-bill|恢复]]。
> [!question] 已付的 Bill 能再收款吗?
> 不能。`canBePaid()` Paid 时返 false。理论上 Bill 已经付清。
> [!question] 业户预存款够付,业务人员怎么操作?
> 看自动 / 手动:
> - 自动(待补的 [[../prepaid/auto-deduction-design|预存款自动抵扣 job]]):业务人员不操作,系统自动
> - 手动:业务人员在 `ViewBill` → 走 `CollectPaymentAction` 选"预存款抵扣"(或专用 Action)→ 详见 [[collect-via-prepaid-auto]]
> [!question] 收款时 PaymentChannel 写错(选了微信实际是现金)?
> CollectionOrder 一经创建**通常不可改**字段。错了:
> - 走作废(详见 [[void-paid-bill]])+ 重新建
> - 或 tinker 改字段(运维 + 留审计)
>
> 预防:Modal 提交前再三确认。
> [!question] activitylog 记什么?
> 详见 [[smart-bulk-delete-design]] activitylog 设计。CollectPayment 通常也记一条 activitylog:event=collected,properties 含 amount / payment_channel / receipt_id。
## 异常分支
- 部分付场景 → [[exception-partial-payment]]
- 批量付(多张账单一起付)→ [[collect-payment-batch]]
- 预存款抵 → [[collect-via-prepaid-auto]]
- 收款错了想撤 → [[void-paid-bill]]
- Bill 挂起中无法收 → [[resume-bill]]
- 逾期账单催收 → [[exception-overdue-bills]]
## 相关文档
- [[bill-six-state-machine]]
- [[bill-vs-collection-order]]
- [[exception-partial-payment]]
- [[collect-payment-batch]]
- [[collect-via-prepaid-auto]]
- [[../adhoc/collection-order-and-receipt]]