## 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 直接向新库写入业务表