# Implementation Plan: 赠送卡矩阵细分数据 (gift-card-breakdown)

## Overview

贯穿全栈数据链路的改动：DDL 新增 6 字段 → ETL 拆分填充 → RLS 视图 + FDW 同步 → 后端 SQL + 接口返回 → 小程序替换 mock。消费行因上游 API 限制，细分列保持 0。

## Tasks

- [x] 1. DDL 迁移与基线同步
  - [x] 1.1 创建 DDL 迁移脚本 `db/etl_feiqiu/migrations/2026-xx-xx_add_gift_breakdown_fields.sql`
    - ALTER TABLE 新增 6 个 NUMERIC(14,2) 字段，NOT NULL DEFAULT 0
    - 使用 `ADD COLUMN IF NOT EXISTS` 保证幂等
    - _Requirements: 1.1, 1.2_
  - [x] 1.2 同步 DDL 基线文件 `docs/database/ddl/etl_feiqiu__dws.sql`
    - 在 `dws_finance_recharge_summary` 表定义中追加 6 个新字段
    - _Requirements: 1.1_

- [x] 2. ETL 赠送卡余额拆分
  - [x] 2.1 修改 `_extract_card_balances()` 按 card_type_id 分组返回细分余额
    - 文件：`apps/etl/connectors/feiqiu/tasks/dws/finance_recharge_task.py`
    - 新增 `GIFT_TYPE_FIELD_MAP` 常量映射 card_type_id → 字段名
    - 返回值新增 `gift_liquor_balance`、`gift_table_fee_balance`、`gift_voucher_balance`
    - 保留原有 `gift_balance` 字段（向后兼容）
    - 某种卡类型无记录时对应字段返回 0
    - _Requirements: 2.1, 2.2, 2.3_
  - [x] 2.2 编写属性测试：ETL 余额提取 round-trip
    - **Property 3: ETL 余额提取 round-trip**
    - 生成随机 `dim_member_card_account` 记录，mock DB，验证各类型余额等于对应 card_type_id 的 balance 之和
    - 当某种卡类型无记录时，对应余额为 0
    - **Validates: Requirements 2.1, 2.2, 2.3, 10.1**

- [x] 3. ETL 赠送卡新增充值拆分
  - [x] 3.1 新增 `_extract_gift_recharge_breakdown()` 方法
    - 文件：`apps/etl/connectors/feiqiu/tasks/dws/finance_recharge_task.py`
    - SQL：`dwd_recharge_order JOIN dim_member_card_account`（`tenant_member_card_id` → `tenant_member_id`）按 card_type_id 分组
    - 新增 `GIFT_RECHARGE_FIELD_MAP` 常量映射
    - 返回 `{gift_liquor_recharge, gift_table_fee_recharge, gift_voucher_recharge}`，缺失卡类型默认 0
    - _Requirements: 3.1, 3.2, 3.3_
  - [x] 3.2 编写属性测试：ETL 新增提取 round-trip
    - **Property 4: ETL 新增提取 round-trip**
    - 生成随机 `dwd_recharge_order` + `dim_member_card_account` 记录，mock DB，验证各类型新增等于对应 card_type_id 的 point_amount 之和
    - 当某种卡类型无充值记录时，对应新增为 0
    - **Validates: Requirements 3.1, 3.2, 3.3, 10.2**

- [x] 4. ETL transform 合并细分字段
  - [x] 4.1 修改 `extract()` 调用新方法并传递结果
    - 文件：`apps/etl/connectors/feiqiu/tasks/dws/finance_recharge_task.py`
    - 在 extract 返回值中新增 `gift_recharge_breakdown` key
    - _Requirements: 4.1_
  - [x] 4.2 修改 `transform()` 将 6 个新字段写入 record dict
    - 使用 `self.safe_decimal()` 处理值，缺失 key 时填充 0
    - 沿用现有 delete-before-insert 幂等策略
    - _Requirements: 4.1, 4.2, 4.3_
  - [x] 4.3 编写属性测试：transform 正确合并细分字段
    - **Property 5: transform 正确合并细分字段**
    - 生成随机 `card_balances` dict 和 `gift_recharge_breakdown` dict，验证 record 包含 6 个字段且值正确
    - 输入 dict 缺少某个 key 时，对应字段为 0
    - **Validates: Requirements 4.1, 4.2**

- [x] 5. Checkpoint — ETL 层验证
  - Ensure all tests pass, ask the user if questions arise.

- [x] 6. 数据库视图层同步
  - [x] 6.1 RLS 视图重建 `app.v_dws_finance_recharge_summary`
    - 创建迁移脚本 `db/zqyy_app/migrations/` 下
    - `CREATE OR REPLACE VIEW` 包含全部 6 个新字段
    - 保持 `site_id` 行级安全过滤策略不变
    - _Requirements: 5.1, 5.2, 5.3_
  - [x] 6.2 FDW 外部表同步
    - 创建/更新幂等脚本（先 DROP 再 `IMPORT FOREIGN SCHEMA`）
    - 支持重复执行不报错
    - _Requirements: 6.1, 6.2_

- [ ] 7. 后端接口修改
  - [x] 7.1 修改 `fdw_queries.get_finance_recharge()` SQL 查询
    - 文件：`apps/backend/app/services/fdw_queries.py`
    - SQL 新增 6 个字段的 SUM 聚合
    - _Requirements: 7.1_
  - [x] 7.2 修改 `gift_rows` 构建逻辑
    - 余额行：`liquor`/`table_fee`/`voucher` 填充对应细分余额
    - 新增行：`liquor`/`table_fee`/`voucher` 填充对应细分新增，`total` 使用三个细分之和
    - 消费行：`liquor`/`table_fee`/`voucher` 保持 0，`total` 返回消费总额
    - _Requirements: 7.2, 7.3, 7.4, 8.1, 8.2_
  - [x] 7.3 修改 `_empty_recharge_data()` 空默认值同步
    - 确保新增 6 个字段在空数据结构中默认为 0
    - _Requirements: 7.5_
  - [x] 7.4 编写属性测试：余额恒等式
    - **Property 1: 余额恒等式**
    - 生成随机三种余额，验证 `gift_card_balance = gift_liquor_balance + gift_table_fee_balance + gift_voucher_balance`
    - **Validates: Requirements 1.3, 10.3**
  - [x] 7.5 编写属性测试：新增恒等式
    - **Property 2: 新增恒等式**
    - 生成随机三种新增，验证 `recharge_gift = gift_liquor_recharge + gift_table_fee_recharge + gift_voucher_recharge`
    - **Validates: Requirements 1.4**
  - [x] 7.6 编写属性测试：后端接口返回正确细分数据
    - **Property 6: 后端接口返回正确的细分数据**
    - 生成随机 DWS 行，mock FDW 查询，验证 gift_rows 余额行和新增行的细分值等于对应字段 SUM
    - **Validates: Requirements 7.2, 7.3**
  - [x] 7.7 编写属性测试：消费行细分列始终为 0
    - **Property 7: 消费行细分列始终为 0**
    - 验证 gift_rows 消费行的 `liquor.value`、`table_fee.value`、`voucher.value` 始终为 0
    - **Validates: Requirements 7.4, 8.1, 8.2**
  - [x] 7.8 编写属性测试：环比计算对新字段正确适配
    - **Property 8: 环比计算对新字段正确适配**
    - 生成随机当期/上期数据，验证 `compare=1` 时 gift_rows 每个 cell 的 compare 等于 `calc_compare(当期值, 上期值)`
    - **Validates: Requirements 7.6**

- [x] 8. Checkpoint — 后端层验证
  - Ensure all tests pass, ask the user if questions arise.

- [ ] 9. 小程序联调
  - [x] 9.1 替换 `board-finance.ts` 中 mock 数据为真实 API 调用
    - 文件：`apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts`
    - 移除 `giftRows` 硬编码 mock 数据
    - 使用 Finance_Board_API 返回的真实数据
    - 字段映射：`liquor→wine`、`table_fee→table`、`voucher→coupon`，在数据转换层处理
    - API 返回错误或超时时展示加载失败提示，不显示 mock 数据
    - _Requirements: 9.1, 9.2, 9.3, 9.4_

- [ ] 10. 数据一致性验证
  - [x] 10.1 创建验证 SQL 脚本
    - 验证余额恒等式：`gift_card_balance = 三种余额之和`
    - 验证新增恒等式：`recharge_gift = 三种新增之和`
    - 验证 DWS 与 DWD 源数据一致：DWS 各类型余额 = DWD `dim_member_card_account` 对应 card_type_id 的 balance 之和
    - 脚本放置在 `scripts/ops/` 或迁移目录
    - _Requirements: 10.1, 10.2, 10.3_

- [ ] 11. BD 手册更新
  - [x] 11.1 更新 `BD_manual_dws_finance_recharge_summary.md`
    - 文件：`apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_dws_finance_recharge_summary.md`
    - 新增 6 个字段的说明（字段名、类型、含义、数据来源）
    - 更新恒等式约束说明
    - 更新数据流向描述
    - _Requirements: 1.1, 1.3, 1.4_

- [x] 12. Final checkpoint — 全链路验证
  - Ensure all tests pass, ask the user if questions arise.

## Notes

- Tasks marked with `*` are optional and can be skipped for faster MVP
- 设计文档使用 Python（ETL）、SQL（DDL/查询）、TypeScript（小程序），任务中代码示例沿用对应语言
- 消费行因上游飞球 API 限制（`dwd_settlement_head.gift_card_amount` 仅提供总额），细分列保持 0
- 属性测试使用 `hypothesis` 库，测试文件统一放置在 `tests/test_gift_card_breakdown_properties.py`
- 单元测试放置在各模块的 `tests/` 目录下
- 所有 DDL 迁移脚本使用 `IF NOT EXISTS` 保证幂等
- card_type_id 硬编码沿用现有 `GIFT_CARD_TYPE_IDS` 常量
