## ADDED Requirements

### Requirement: 批量归属决策

奇成迁移脚本 SHALL 使用 `mapping.yaml` 的批量归属规则决定卡和设备的目标店铺。奇成 `agent_id` 只能作为可选参考信息输出到审核文件，MUST NOT 作为默认归属决策来源。

#### Scenario: 默认店铺分配设备

- **WHEN** `ownership_rules.device.mode = default_shop` 且 `default_target_shop_code = KWTX`
- **THEN** 所有未被 `overrides.devices` 覆盖的设备迁移 SQL 使用 `KWTX` 对应的 `tb_shop.id` 作为 `shop_id`
- **AND** `ownership_resolution.csv` 记录每台设备的决策来源为 `default_shop`

#### Scenario: 默认店铺分配独立卡

- **WHEN** `ownership_rules.standalone_card.mode = default_shop` 且 `default_target_shop_code = KWTX`
- **THEN** 所有未绑定设备且未被 `overrides.cards` 覆盖的卡迁移 SQL 使用 `KWTX` 对应的 `tb_shop.id` 作为 `shop_id`

#### Scenario: 覆盖项优先于批量规则

- **WHEN** 某设备在 `overrides.devices` 中配置了 `target_shop_code = OTHER`
- **THEN** 该设备使用 `OTHER` 作为目标店铺
- **AND** 审核文件记录决策来源为 `override`

#### Scenario: 目标店铺缺失阻断

- **WHEN** 规则要求资产分配到某个 `target_shop_code`，但新库不存在该店铺
- **THEN** 脚本 MUST 在 `errors.csv` 写入该资产的错误
- **AND** 脚本 MUST NOT 为该资产生成店铺分配 SQL

### Requirement: 显式设备主卡和套餐来源槽位

奇成迁移脚本 SHALL 支持在输入数据或 `mapping.yaml` 中显式指定设备 `current_slot` 和 `package_source_slot`。未逐行指定时，脚本 SHALL 使用 `ownership_rules.device.current_slot` 和 `ownership_rules.device.package_source_slot` 作为批量默认值。

#### Scenario: 使用批量默认槽位

- **WHEN** `devices.csv` 某设备未填写 `current_slot` 和 `package_source_slot`
- **AND** `ownership_rules.device.current_slot = 2`
- **AND** `ownership_rules.device.package_source_slot = 2`
- **THEN** 该设备的当前绑定卡和套餐来源卡均使用 `sim_iccid_2`

#### Scenario: 使用设备行覆盖槽位

- **WHEN** `devices.csv` 某设备填写 `current_slot = 1` 和 `package_source_slot = 1`
- **THEN** 该设备使用第 1 槽作为当前槽位和套餐来源槽位
- **AND** 该行配置优先于批量默认槽位

#### Scenario: 槽位无卡阻断设备套餐

- **WHEN** 某设备的 `package_source_slot = 2`，但 `sim_iccid_2` 为空
- **THEN** 脚本 MUST 在 `errors.csv` 写入该设备的错误
- **AND** 脚本 MUST NOT 为该设备生成套餐使用记录

### Requirement: 单卡与设备迁移分支分离

奇成迁移脚本 SHALL 分离独立卡套餐迁移和设备套餐迁移。独立卡套餐 MUST 写入 `usage_type = single_card` 并关联 `iot_card_id`；设备套餐 MUST 写入 `usage_type = device` 并关联 `device_id`。

#### Scenario: 独立卡生成单卡套餐

- **WHEN** 某卡未绑定设备且命中可迁移套餐
- **THEN** 脚本为该卡生成 `tb_order.order_type = single_card`
- **AND** 脚本为该卡生成 `tb_package_usage.usage_type = single_card`

#### Scenario: 设备绑定卡生成设备套餐

- **WHEN** 某卡位于设备 `package_source_slot`
- **THEN** 脚本为该设备生成 `tb_order.order_type = device`
- **AND** 脚本为该设备生成 `tb_package_usage.usage_type = device`

#### Scenario: 设备非来源槽位不重复生成套餐

- **WHEN** 某卡绑定在设备非 `package_source_slot` 的槽位
- **THEN** 脚本 MUST NOT 为该卡单独生成主套餐使用记录
- **AND** 该卡仍 SHALL 生成基础卡资产和设备绑定 SQL

### Requirement: 当前生效和待生效套餐迁移

奇成迁移脚本 SHALL 迁移正式套餐中的当前生效套餐和未生效待生效套餐。当前生效套餐写入 `tb_package_usage.status = 1`；待生效套餐写入 `tb_package_usage.status = 0` 并设置稳定 `priority`。

#### Scenario: 当前生效套餐迁移为 active

- **WHEN** 奇成某正式套餐处于当前生效状态
- **THEN** 脚本生成的 `tb_package_usage.status = 1`
- **AND** `activated_at` 和 `expires_at` 来源于奇成套餐生命周期时间

#### Scenario: 未生效套餐迁移为 pending

- **WHEN** 奇成某正式套餐属于未生效待生效套餐
- **THEN** 脚本生成的 `tb_package_usage.status = 0`
- **AND** `data_usage_mb = 0`
- **AND** `priority` 按待生效顺序稳定递增

#### Scenario: 已过期套餐不迁移

- **WHEN** 奇成某正式套餐已过期且不属于当前生效或待生效范围
- **THEN** 脚本 MUST NOT 为该套餐生成 `tb_package_usage`
- **AND** `package_resolution.csv` 记录跳过原因

#### Scenario: 目标套餐映射缺失阻断

- **WHEN** 某可迁移 legacy 套餐没有配置 `target_package_id`
- **THEN** 脚本 MUST 在 `errors.csv` 或 `package_resolution.csv` 中记录缺失映射
- **AND** 脚本 MUST NOT 为该套餐生成套餐使用记录

### Requirement: 迁移审核产物

奇成迁移脚本 SHALL 在生成 SQL 时同步输出可审计 CSV，说明每个资产和套餐的最终决策。

#### Scenario: 输出归属审核文件

- **WHEN** 执行 `migrate_assets.py`
- **THEN** 脚本输出 `ownership_resolution.csv`
- **AND** 文件至少包含资产类型、资产标识、来源卡、目标店铺、决策来源、是否生成分配、原因

#### Scenario: 输出套餐审核文件

- **WHEN** 执行 `migrate_runtime.py`
- **THEN** 脚本输出 `package_resolution.csv`
- **AND** 文件至少包含资产类型、资产标识、来源卡、legacy 套餐 ID、legacy 套餐名称、目标套餐 ID、目标状态、优先级、决策、原因

#### Scenario: 摘要显示关键数量

- **WHEN** 脚本生成完成
- **THEN** `summary.txt` MUST 展示卡数、设备数、分配资产数、active 套餐数、pending 套餐数、错误数和跳过数

### Requirement: 迁移脚本只读和幂等边界

奇成迁移脚本 SHALL 保持对奇成库只读，并生成可重复执行的 PostgreSQL SQL 文件。脚本 MUST NOT 直接写新库业务数据。

#### Scenario: 奇成连接只读

- **WHEN** 脚本连接奇成库查询元数据和套餐生命周期
- **THEN** 连接 MUST 使用只读事务

#### Scenario: 生成 SQL 幂等

- **WHEN** 同一批次 SQL 被重复执行
- **THEN** 插入类语句 SHALL 使用唯一键或条件确保不会重复创建相同资产、订单、套餐使用记录和分配记录

#### Scenario: 不直接写新库

- **WHEN** 执行 Python 迁移脚本
- **THEN** 脚本只生成 SQL 和审核文件
- **AND** 脚本 MUST NOT 直接向新库写入业务表
