# Capability: 代理系列授权管理

## Purpose

定义"系列授权"（Series Grant）的 CRUD 操作。系列授权将原本割裂的"系列分配"和"套餐分配"合并为一个原子操作，一次请求完成：授权代理可销售某系列下的指定套餐、设定每个套餐的成本价、配置一次性佣金（固定模式：单值天花板；梯度模式：每档位上限金额列表）和代理自设强充（平台未设时）。

底层仍使用 `tb_shop_series_allocation` 和 `tb_shop_package_allocation` 两张表，对外以 `ShopSeriesAllocation.ID` 作为 grant 主键。

---

## Requirements

### Requirement: 创建系列授权（固定模式）

系统 SHALL 提供 `POST /shop-series-grants` 接口，在一次请求中原子性创建系列分配和套餐分配列表。固定模式下 `one_time_commission_amount` MUST 必填，且不得超过分配者自身的天花板。

#### Scenario: 代理成功创建固定模式授权
- **WHEN** 代理A（自身天花板=80元）为直属下级代理B 创建系列授权，commission_type=fixed，one_time_commission_amount=5000（50元），packages=[{package_id:1, cost_price:3000}, {package_id:2, cost_price:5000}]
- **THEN** 系统在事务中创建 1 条 ShopSeriesAllocation（one_time_commission_amount=5000）和 2 条 ShopPackageAllocation，响应返回包含 packages 列表的聚合视图

#### Scenario: 代理B 已存在此系列授权，重复创建
- **WHEN** 代理A 为代理B 创建系列授权，但代理B 在此系列下已有 active 授权记录
- **THEN** 系统返回错误"该代理已存在此系列授权"

#### Scenario: 分配者自身无此系列授权
- **WHEN** 代理A 自身未被授权此套餐系列，尝试为代理B 创建此系列授权
- **THEN** 系统返回错误"当前账号无此系列授权，无法向下分配"

#### Scenario: 平台成功创建固定模式授权
- **WHEN** 平台管理员为一级代理创建系列授权，commission_type=fixed，one_time_commission_amount=8000（80元），系列总额 commission_amount=10000（100元）
- **THEN** 系统创建授权，响应中 allocator_shop_id=0，allocator_shop_name="平台"

#### Scenario: 固定模式 one_time_commission_amount 为必填
- **WHEN** 请求中不包含 one_time_commission_amount
- **THEN** 系统返回参数错误"固定模式下一次性佣金额度为必填项"

#### Scenario: 金额超过代理自身天花板
- **WHEN** 代理A（天花板=8000分）为代理B 创建授权，one_time_commission_amount=10000
- **THEN** 系统返回错误"一次性佣金额度不能超过上级限额"

#### Scenario: 平台金额超过系列总额
- **WHEN** 平台为代理A 创建授权，one_time_commission_amount=12000，但 PackageSeries.commission_amount=10000
- **THEN** 系统返回错误"一次性佣金额度不能超过套餐系列设定的总额"

---

### Requirement: 创建系列授权（梯度模式）

系统 SHALL 支持梯度模式的系列授权创建。梯度模式下，`commission_tiers` MUST 为必填，且必须包含与 PackageSeries 完全相同数量和阈值的阶梯（不多不少）。若某档位不希望给下级佣金，应将该档位的 amount 设为 0，不可省略该档位。创建成功后的响应中，`commission_tiers` 每个档位 MUST 包含 `operator`、`dimension`、`stat_scope` 字段（从全局配置合并）。

#### Scenario: 代理成功创建梯度模式授权
- **WHEN** 代理A 的专属阶梯为 `[{operator:">=" , threshold:100, amount:80}, {operator:">=" , threshold:150, amount:120}]`，A 为代理B 创建授权，传入 `commission_tiers=[{threshold:100, amount:50}, {threshold:150, amount:100}]`
- **THEN** 系统创建授权，`commission_tiers_json` 存储 `[{threshold:100, amount:50}, {threshold:150, amount:100}]`
- **AND** 响应中 `commission_tiers=[{operator:">=" , dimension:"sales_count", stat_scope:"self", threshold:100, amount:50}, ...]`

#### Scenario: 平台成功创建梯度模式授权
- **WHEN** 平台为顶级代理A 创建授权，PackageSeries 阶梯含 `operator`/`dimension`/`stat_scope`
- **THEN** 系统创建授权，响应中 `commission_tiers` 包含 PackageSeries 全局 `operator`、`dimension`、`stat_scope`

#### Scenario: 梯度模式某档位金额超过父级
- **WHEN** 代理A 的阶梯第一档 `amount=80`，A 为 B 创建授权时传入第一档 `amount=90`
- **THEN** 系统返回错误“某档位佣金金额超过上级天花板”

#### Scenario: 梯度模式传入了不存在的阈值
- **WHEN** PackageSeries 只有 `threshold=100` 和 `150` 两档，请求中传入 `threshold=200`
- **THEN** 系统返回错误“梯度阶梯 threshold 与系列配置不匹配”

#### Scenario: 梯度模式 commission_tiers 为必填
- **WHEN** 请求中不包含 `commission_tiers` 或为空数组
- **THEN** 系统返回参数错误“梯度模式必须填写阶梯配置”

---

### Requirement: 强充配置的平台/代理层级

创建系列授权时，系统 SHALL 根据 PackageSeries 的触发类型和强充设置决定代理是否可自设强充。

#### Scenario: 首次充值触发类型，强充不可配置
- **WHEN** PackageSeries.trigger_type=first_recharge，代理创建授权时传入任意 enable_force_recharge 值
- **THEN** 系统忽略代理的强充设置，响应中 force_recharge_locked=true（首次充值本身即为强充机制，无需额外配置）

#### Scenario: 累计充值触发类型，平台已设强充，代理配置被忽略
- **WHEN** PackageSeries.trigger_type=accumulated_recharge 且 enable_force_recharge=true，代理创建授权时传入 enable_force_recharge=false
- **THEN** 系统忽略代理的强充设置，响应中 force_recharge_locked=true

#### Scenario: 累计充值触发类型，平台未设强充，代理可自设
- **WHEN** PackageSeries.trigger_type=accumulated_recharge 且 enable_force_recharge=false，代理创建授权时传入 enable_force_recharge=true，force_recharge_amount=10000
- **THEN** 系统保存代理的强充配置，响应中 force_recharge_locked=false，force_recharge_enabled=true，force_recharge_amount=10000

---

### Requirement: 查询系列授权详情

系统 SHALL 提供 `GET /shop-series-grants/:id` 接口，返回包含套餐列表的聚合视图。梯度模式下，`commission_tiers` 中每个档位 MUST 包含 `dimension`（统计维度）和 `stat_scope`（统计范围）字段，这两个字段从 PackageSeries 全局配置按 `threshold` 合并，对代理只读。

#### Scenario: 固定模式详情
- **WHEN** 查询固定模式系列授权详情
- **THEN** 响应包含 `commission_type="fixed"`，`one_time_commission_amount=有效值`，`commission_tiers=[]`

#### Scenario: 梯度模式详情
- **WHEN** 查询梯度模式系列授权详情
- **THEN** 响应包含 `commission_type="tiered"`，`one_time_commission_amount=0`
- **AND** `commission_tiers` 中每个档位包含 `operator`、`dimension`、`stat_scope`、`threshold`、`amount` 五个字段
- **AND** `operator`、`dimension`、`stat_scope` 的值来自 PackageSeries 全局配置（对应 threshold 的档位），代理的 `amount` 来自 `ShopSeriesAllocation.commission_tiers_json`

#### Scenario: 梯度模式 dimension 为销售量
- **WHEN** 查询梯度模式授权详情，PackageSeries 阶梯 `dimension = "sales_count"`
- **THEN** 响应中对应档位 `dimension = "sales_count"`，前端展示“销售量”条件

#### Scenario: 梯度模式 dimension 为销售额
- **WHEN** 查询梯度模式授权详情，PackageSeries 阶梯 `dimension = "sales_amount"`
- **THEN** 响应中对应档位 `dimension = "sales_amount"`，前端展示“销售额”条件

#### Scenario: 梯度模式 stat_scope 区分
- **WHEN** 查询梯度模式授权详情
- **THEN** 响应中 `stat_scope` 正确反映 PackageSeries 配置的统计范围（`"self"` 或 `"self_and_sub"`）

#### Scenario: 查询不存在的授权
- **WHEN** 查询不存在的授权 ID
- **THEN** 系统返回错误“授权记录不存在”

---

### Requirement: 查询系列授权列表

系统 SHALL 提供 `GET /shop-series-grants` 接口，支持分页和多维度筛选，响应内嵌套餐数量摘要（不含完整套餐列表）。

#### Scenario: 列表查询支持按店铺和系列筛选
- **WHEN** 传入 shop_id、series_id、allocator_shop_id 等筛选条件
- **THEN** 仅返回符合条件的授权记录，每条记录包含 package_count

---

### Requirement: 更新系列授权配置

系统 SHALL 提供 `PUT /shop-series-grants/:id` 接口，支持更新一次性佣金配置和强充配置。

#### Scenario: 固定模式更新佣金额度
- **WHEN** 更新 one_time_commission_amount，新值不超过分配者天花板
- **THEN** 系统更新成功

#### Scenario: 梯度模式更新阶梯金额
- **WHEN** 更新 commission_tiers，每档位金额不超过分配者同档位上限
- **THEN** 系统更新 commission_tiers_json 字段

#### Scenario: 更新代理自设强充（平台未设时）
- **WHEN** 平台未设强充，更新 enable_force_recharge=true，force_recharge_amount=10000
- **THEN** 系统更新成功，后续该代理渠道下的客户须满足强充要求

---

### Requirement: 管理授权内套餐

系统 SHALL 提供 `PUT /shop-series-grants/:id/packages` 接口，支持添加套餐、移除套餐、更新成本价，操作在事务中完成，成功后返回 HTTP 200（无需返回完整授权视图）。

#### Scenario: 向授权中添加新套餐
- **WHEN** 请求包含新的 package_id 和 cost_price，且该套餐属于此系列
- **THEN** 系统创建新的 ShopPackageAllocation

#### Scenario: 更新套餐成本价
- **WHEN** 请求中套餐的 cost_price 与当前值不同
- **THEN** 系统更新 cost_price 并写价格历史记录

#### Scenario: 移除授权中的套餐
- **WHEN** 请求中某套餐标记 remove=true，且该套餐在当前授权中存在
- **THEN** 系统软删除对应的 ShopPackageAllocation

#### Scenario: remove=true 但套餐已不在授权中
- **WHEN** 请求中某套餐标记 remove=true，但该套餐已被软删除或从未在此授权中
- **THEN** 系统静默忽略该条目，不报错，继续处理其他条目

#### Scenario: 重新添加曾被移除的套餐
- **WHEN** 某套餐曾经被软删除，请求中再次包含该 package_id（无 remove 标志）
- **THEN** 系统创建一条新的 ShopPackageAllocation 记录，不恢复旧记录

#### Scenario: 添加不属于该系列的套餐
- **WHEN** 请求中包含不属于该系列的 package_id
- **THEN** 系统返回错误"套餐不属于该系列，无法添加到此授权"

#### Scenario: 添加上级未授权的套餐
- **WHEN** 代理A 尝试添加代理A 自己也未获授权的套餐
- **THEN** 系统返回错误"无权限分配该套餐"

---

### Requirement: 删除系列授权

系统 SHALL 提供 `DELETE /shop-series-grants/:id` 接口，删除时同步软删除所有关联的套餐分配。

#### Scenario: 成功删除无下级依赖的授权
- **WHEN** 删除一个下级代理未基于此授权再分配的记录
- **THEN** 系统软删除 ShopSeriesAllocation 和所有关联的 ShopPackageAllocation

#### Scenario: 有下级依赖时禁止删除
- **WHEN** 删除一个已被下级代理用于创建子授权的记录
- **THEN** 系统返回错误"存在下级依赖，无法删除，请先删除下级授权"

---

### Requirement: 系列授权列表强充状态正确反映

系列授权列表 (`GET /shop-series-grants`) MUST 在每个列表项中返回 `force_recharge_locked`（是否被套餐系列锁定）和 `force_recharge_amount`（强充金额）。`force_recharge_enabled` MUST 反映有效状态：当锁定时为 `true`（无论分配记录自身如何）；未锁定时取分配记录的实际设置。

#### Scenario: 套餐系列锁定强充
- **WHEN** 套餐系列配置 `enable_force_recharge=true` 或 `trigger_type=first_recharge`，查询列表
- **THEN** 列表项中 `force_recharge_locked=true`，`force_recharge_enabled=true`，`force_recharge_amount`=系列配置的 `force_amount`

#### Scenario: 代理自身开启强充（未锁定）
- **WHEN** 套餐系列未锁定强充，分配记录中 `enable_force_recharge=true`，查询列表
- **THEN** 列表项中 `force_recharge_locked=false`，`force_recharge_enabled=true`，`force_recharge_amount`=分配记录的实际金额

#### Scenario: 代理未开启强充（未锁定）
- **WHEN** 套餐系列未锁定强充，分配记录中 `enable_force_recharge=false`，查询列表
- **THEN** 列表项中 `force_recharge_locked=false`，`force_recharge_enabled=false`，`force_recharge_amount=0`

---

### Requirement: 查询系列授权详情强充有效状态

系列授权详情 (`GET /shop-series-grants/:id`) 中，`force_recharge_enabled` MUST 反映有效状态：当锁定时为 `true`；`force_recharge_amount` 锁定时应返回系列配置的 `force_amount`。

#### Scenario: 锁定强充时详情响应
- **WHEN** 套餐系列锁定强充，查询对应分配记录详情
- **THEN** `force_recharge_locked=true`，`force_recharge_enabled=true`，`force_recharge_amount`=系列配置的 `force_amount`
