# IoT Order Management

## Purpose

Manage orders for IoT card packages and number card products, including order creation, payment processing, status tracking, commission triggering, and support for single-card orders, device-level orders, and carrier number card orders.

This capability supports:
- Unified order entity for package orders and number card orders
- Order status lifecycle management
- Multiple payment methods (wallet, online payment, carrier direct payment)
- Commission triggering on order completion
- Device-level order commission (counted once regardless of bound card count)
- Multi-dimensional order querying and filtering
## Requirements
### Requirement: 订单实体定义

系统 SHALL 定义订单(Order)实体，统一管理两种订单类型：套餐订单、号卡订单，并支持混合支付方式（钱包 + 在线支付）。

**修改说明**：
- 增加 `wallet_payment_amount` 字段：钱包支付金额
- 增加 `online_payment_amount` 字段：在线支付金额
- 支持用户在购买套餐时选择支付方式（全部钱包支付、全部在线支付、混合支付）

**实体字段**（只列出新增字段）：
- `wallet_payment_amount`：钱包支付金额（BIGINT，单位：分，默认 0）**【新增】**
- `online_payment_amount`：在线支付金额（BIGINT，单位：分，默认 0）**【新增】**

**支付规则**：
- `wallet_payment_amount` + `online_payment_amount` = `amount`（订单总金额）
- 当 `payment_method` 为 "wallet" 时，`wallet_payment_amount` = `amount`，`online_payment_amount` = 0
- 当 `payment_method` 为 "online" 时，`online_payment_amount` = `amount`，`wallet_payment_amount` = 0
- 混合支付时，`payment_method` 为 "mixed"，两个字段都 > 0

#### Scenario: 全额钱包支付

- **WHEN** 用户购买套餐，订单金额为 30 00 分（30 元），选择钱包支付，钱包余额为 10000 分
- **THEN** 系统创建订单，`amount` 为 3000，`payment_method` 为 "wallet"，`wallet_payment_amount` 为 3000，`online_payment_amount` 为 0

#### Scenario: 全额在线支付

- **WHEN** 用户购买套餐，订单金额为 3000 分（30 元），选择在线支付
- **THEN** 系统创建订单，`amount` 为 3000，`payment_method` 为 "online"，`wallet_payment_amount` 为 0，`online_payment_amount` 为 3000

#### Scenario: 混合支付

- **WHEN** 用户购买套餐，订单金额为 5000 分（50 元），钱包余额为 3000 分，用户选择钱包支付 3000 分 + 在线支付 2000 分
- **THEN** 系统创建订单，`amount` 为 5000，`payment_method` 为 "mixed"，`wallet_payment_amount` 为 3000，`online_payment_amount` 为 2000

#### Scenario: 钱包余额不足，部分钱包支付

- **WHEN** 用户购买套餐，订单金额为 5000 分（50 元），钱包余额为 2000 分，用户选择钱包支付 2000 分 + 在线支付 3000 分
- **THEN** 系统先冻结钱包余额 2000 分，创建订单，`wallet_payment_amount` 为 2000，`online_payment_amount` 为 3000，等待用户完成在线支付

#### Scenario: 钱包余额不足，无法全额钱包支付

- **WHEN** 用户购买套餐，订单金额为 5000 分（50 元），钱包余额为 3000 分，用户选择钱包支付
- **THEN** 系统拒绝创建订单，返回错误信息"钱包余额不足"，建议用户选择混合支付或在线支付

---

### Requirement: 订单状态流转

系统 SHALL 管理订单的状态流转,确保状态变更符合业务规则。**新增订单超时自动取消的详细场景。**

**状态定义**:
- **1-待支付**: 订单已创建,等待用户支付
- **2-已支付**: 用户已支付,等待系统处理
- **3-已完成**: 订单已完成(激活/发货等)
- **4-已取消**: 订单已取消
- **5-已退款**: 订单已退款

**状态流转规则**:
- 待支付(1) → 已支付(2): 用户完成支付
- 待支付(1) → 已取消(4): 用户手动取消订单或订单超时（30 分钟）
- 已支付(2) → 已完成(3): 系统完成订单处理(激活/发货)
- 已支付(2) → 已退款(5): 用户申请退款且审核通过
- 已完成(3) → 已退款(5): 用户申请退款且审核通过(特殊情况)

#### Scenario: 用户支付订单

- **WHEN** 用户支付待支付订单(ID 为 10001),支付金额为 30.00 元
- **THEN** 系统将订单状态从 1(待支付) 变更为 2(已支付),`paid_at` 记录支付时间

#### Scenario: 单卡套餐订单完成

- **WHEN** 系统处理完单卡套餐订单(ID 为 10001),激活 IoT 卡并分配套餐
- **THEN** 系统将订单状态从 2(已支付) 变更为 3(已完成),`completed_at` 记录完成时间

#### Scenario: 设备级套餐订单完成

- **WHEN** 系统处理完设备级套餐订单(ID 为 10002),为设备绑定的所有 IoT 卡分配套餐
- **THEN** 系统将订单状态从 2(已支付) 变更为 3(已完成),`completed_at` 记录完成时间

#### Scenario: 用户手动取消订单

- **WHEN** 用户手动取消待支付订单(ID 为 10003)
- **THEN** 系统将订单状态从 1(待支付) 变更为 4(已取消),`expires_at` 设置为 NULL，如有钱包预扣则解冻余额

#### Scenario: 订单超时自动取消

- **WHEN** 订单创建后 30 分钟未支付，定时任务扫描到该订单
- **THEN** 系统自动将订单状态从 1(待支付) 变更为 4(已取消),`expires_at` 设置为 NULL，如有钱包预扣则解冻余额

#### Scenario: 订单超时自动取消（混合支付）

- **WHEN** 混合支付订单创建后 30 分钟未完成在线支付，钱包已预扣 2000 分
- **THEN** 系统自动取消订单，解冻钱包余额 2000 分

#### Scenario: 订单超时自动取消（纯在线支付）

- **WHEN** 纯在线支付订单创建后 30 分钟未支付
- **THEN** 系统自动取消订单，无需钱包解冻操作
---

### Requirement: 订单支付方式

系统 SHALL 支持三种支付方式:钱包支付、在线支付、运营商直付。

**支付方式**:
- **钱包支付(wallet)**: 从用户钱包余额扣款
- **在线支付(online)**: 通过第三方支付(微信/支付宝等)
- **运营商直付(carrier)**: 用户直接支付给运营商(仅号卡订单)

**支付规则**:
- 一次性分佣订单必须使用钱包支付
- 套餐购买订单可以使用钱包或在线支付
- 号卡订单必须使用运营商直付

#### Scenario: 钱包支付订单

- **WHEN** 用户使用钱包支付订单(金额为 30.00 元),钱包余额为 50.00 元
- **THEN** 系统从钱包扣除 30.00 元,订单状态变更为 2(已支付),`payment_method` 为 "wallet"

#### Scenario: 钱包余额不足

- **WHEN** 用户使用钱包支付订单(金额为 30.00 元),钱包余额为 20.00 元
- **THEN** 系统拒绝支付,返回错误信息"钱包余额不足"

#### Scenario: 一次性分佣订单强制钱包支付

- **WHEN** 用户购买配置了一次性分佣的套餐,尝试使用在线支付
- **THEN** 系统拒绝支付,返回错误信息"一次性分佣订单必须使用钱包支付"

---

### Requirement: 订单分佣触发

系统 SHALL 在订单完成时触发分佣计算,根据代理分佣规则创建分佣记录。

**触发条件**:
- 订单状态变更为 3(已完成)
- 订单有 `agent_id`(通过代理销售)
- 代理配置了分佣规则

**分佣计算规则**:
- **单卡套餐订单**: 根据 IoT 卡关联的代理分佣规则计算分佣
- **设备级套餐订单**: 分佣只计算一次(不按设备绑定的 IoT 卡数量倍增)
- **号卡订单**: 下单即冻结分佣,次月通过 Excel 导入解冻

#### Scenario: 单卡套餐购买订单触发分佣

- **WHEN** 代理(ID 为 123)的单卡套餐订单(ID 为 10001)完成,订单金额为 30.00 元,代理配置了 5.00 元一次性分佣
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10001,`amount` 为 5.00,状态为 1(冻结)

#### Scenario: 设备级套餐订单触发分佣(只计算一次)

- **WHEN** 代理(ID 为 123)的设备级套餐订单(ID 为 10002)完成,设备绑定 3 张 IoT 卡,订单金额为 399.00 元,代理配置了 100.00 元长期分佣
- **THEN** 系统创建一条分佣记录,`agent_id` 为 123,`order_id` 为 10002,`amount` 为 100.00,状态为 1(冻结),不是 3 × 100.00

#### Scenario: 号卡订单触发分佣

- **WHEN** 代理(ID 为 123)的号卡订单(ID 为 10003)创建,订单金额为 30.00 元,代理配置了长期分佣
- **THEN** 系统创建分佣记录,`agent_id` 为 123,`order_id` 为 10003,状态为 1(冻结),等待次月通过 Excel 导入解冻

---

### Requirement: 订单查询和筛选

系统 SHALL 支持多维度查询和筛选订单。

**查询条件**:
- 订单编号(精确匹配)
- 订单类型(1-套餐订单 2-号卡订单)
- 订单状态(单选或多选)
- IoT 卡 ID(精确匹配)
- 设备 ID(精确匹配)
- 号卡 ID(精确匹配)
- 用户 ID(精确匹配)
- 代理 ID(精确匹配)
- 支付方式(单选或多选)
- 创建时间范围(开始时间 - 结束时间)
- 支付时间范围(开始时间 - 结束时间)
- 完成时间范围(开始时间 - 结束时间)

**分页**:
- 默认每页 20 条,最大每页 100 条
- 返回总记录数和总页数

#### Scenario: 查询用户的所有订单

- **WHEN** 用户(ID 为 2001)查询自己的所有订单
- **THEN** 系统返回 `user_id` 为 2001 的所有订单列表,按创建时间倒序排列

#### Scenario: 查询代理的订单

- **WHEN** 代理(ID 为 123)查询自己的订单,筛选已完成的套餐订单
- **THEN** 系统返回 `agent_id` 为 123 且 `order_type` 为 1 且 `status` 为 3(已完成) 的订单列表

#### Scenario: 查询 IoT 卡的订单历史

- **WHEN** 运营人员查询 IoT 卡(ID 为 1001)的所有订单
- **THEN** 系统返回 `iot_card_id` 为 1001 的所有订单列表,包含套餐购买记录

#### Scenario: 查询设备的订单历史

- **WHEN** 运营人员查询设备(ID 为 5001)的所有订单
- **THEN** 系统返回 `device_id` 为 5001 的所有设备级套餐订单列表

---

### Requirement: 订单数据校验

系统 SHALL 对订单数据进行校验，确保数据完整性和一致性，特别是支付金额的一致性。

**新增校验规则**：
- `wallet_payment_amount`：必填，≥ 0，最多精确到分
- `online_payment_amount`：必填，≥ 0，最多精确到分
- `wallet_payment_amount` + `online_payment_amount` = `amount`（订单总金额）
- 当 `payment_method` 为 "wallet" 时，`wallet_payment_amount` 必须 = `amount`
- 当 `payment_method` 为 "online" 时，`online_payment_amount` 必须 = `amount`
- 当 `payment_method` 为 "mixed" 时，两个字段都必须 > 0

#### Scenario: 支付金额不一致

- **WHEN** 创建订单，`amount` 为 5000，`wallet_payment_amount` 为 2000，`online_payment_amount` 为 2000
- **THEN** 系统拒绝创建，返回错误信息"支付金额总和与订单金额不一致"

#### Scenario: 钱包支付时在线支付金额不为 0

- **WHEN** 创建订单，`payment_method` 为 "wallet"，`wallet_payment_amount` 为 3000，`online_payment_amount` 为 0（正确），但用户错误地设置 `online_payment_amount` 为 100
- **THEN** 系统拒绝创建，返回错误信息"钱包支付时在线支付金额必须为 0"

#### Scenario: 混合支付时钱包支付金额为 0

- **WHEN** 创建订单，`payment_method` 为 "mixed"，`wallet_payment_amount` 为 0，`online_payment_amount` 为 5000
- **THEN** 系统拒绝创建，返回错误信息"混合支付时钱包支付金额和在线支付金额都必须大于 0"

### Requirement: 订单支付处理

系统 SHALL 根据支付方式正确处理订单支付，包括钱包扣款、在线支付、混合支付等。

**钱包支付流程**：
1. 检查钱包可用余额是否充足
2. 冻结钱包余额（`frozen_balance` 增加）
3. 创建订单，状态为"待支付"
4. 订单完成后，扣减钱包余额（`balance` 减少，`frozen_balance` 减少），创建钱包明细记录
5. 订单取消时，解冻钱包余额（`frozen_balance` 减少）

**在线支付流程**：
1. 创建订单，状态为"待支付"
2. 调用第三方支付接口
3. 用户完成支付后，订单状态变更为"已支付"
4. 订单完成后，订单状态变更为"已完成"

**混合支付流程**：
1. 检查钱包可用余额是否充足（钱包支付部分）
2. 冻结钱包余额
3. 创建订单，状态为"待支付"
4. 调用第三方支付接口（在线支付部分）
5. 用户完成在线支付后，扣减钱包余额，订单状态变更为"已支付"
6. 订单完成后，订单状态变更为"已完成"

#### Scenario: 钱包支付订单完成

- **WHEN** 用户使用钱包支付购买套餐，订单金额为 3000 分
- **THEN** 系统：
  1. 创建订单，状态为"待支付"，冻结钱包余额 3000 分
  2. 订单处理完成后，扣减钱包余额 3000 分，解冻 3000 分，创建钱包明细记录（类型为"扣款"），订单状态变更为"已完成"

#### Scenario: 混合支付订单完成

- **WHEN** 用户使用混合支付购买套餐，钱包支付 2000 分 + 在线支付 3000 分
- **THEN** 系统：
  1. 创建订单，状态为"待支付"，冻结钱包余额 2000 分
  2. 用户完成在线支付 3000 分后，扣减钱包余额 2000 分，解冻 2000 分，创建钱包明细记录，订单状态变更为"已支付"
  3. 订单处理完成后，订单状态变更为"已完成"

#### Scenario: 订单取消，解冻钱包余额

- **WHEN** 用户使用钱包支付创建订单，订单金额为 3000 分，然后取消订单
- **THEN** 系统解冻钱包余额 3000 分（`frozen_balance` 减少 3000），订单状态变更为"已取消"

---

### Requirement: 订单来源与代际字段

系统 SHALL 在订单（Order）实体新增来源与代际字段：
- `source varchar(20) NOT NULL DEFAULT 'admin'`，取值 `admin/client`
- `generation int NOT NULL DEFAULT 1`

#### Scenario: 新建订单默认后台来源
- **WHEN** 系统创建订单且未显式指定来源
- **THEN** `source` MUST 默认为 `admin`

#### Scenario: 客户端下单写入客户端来源
- **WHEN** 客户端入口创建订单
- **THEN** `source` MUST 写入为 `client`

#### Scenario: 新建订单默认代际为 1
- **WHEN** 系统创建订单且未显式指定代际
- **THEN** `generation` MUST 默认为 `1`

