//! erp-health 积分系统集成测试 //! //! 验证积分赚取/消费 FIFO、签到、兑换、租户隔离等核心行为。 //! 使用 TestApp 创建隔离环境,直接调用 service 层函数。 use erp_health::dto::points_dto::*; use erp_health::service::points_service; use super::test_fixture::TestApp; /// 创建测试用积分规则 async fn seed_rule(app: &TestApp, event_type: &str, points_value: i32) -> PointsRuleResp { let req = CreatePointsRuleReq { event_type: event_type.to_string(), name: format!("{}规则", event_type), description: None, points_value, daily_cap: 100, streak_7d_bonus: 10, streak_14d_bonus: 20, streak_30d_bonus: 50, }; points_service::create_rule(app.health_state(), app.tenant_id(), Some(app.operator_id()), req) .await .expect("创建规则应成功") } /// 创建测试用商品(无限库存) async fn seed_product(app: &TestApp, name: &str, points_cost: i32) -> PointsProductResp { let req = CreatePointsProductReq { name: name.to_string(), product_type: Some("virtual".to_string()), points_cost, stock: Some(-1), image_url: None, description: None, service_config: None, sort_order: None, }; points_service::create_product(app.health_state(), app.tenant_id(), Some(app.operator_id()), req) .await .expect("创建商品应成功") } // --------------------------------------------------------------------------- // 测试 1: 签到积分 — 首次签到成功 // --------------------------------------------------------------------------- #[tokio::test] async fn test_points_earn_sign_in() { let app = TestApp::new().await; let patient_id = app.create_patient("签到患者").await; seed_rule(&app, "daily_checkin", 5).await; // 首次签到 let result = points_service::daily_checkin( app.health_state(), app.tenant_id(), patient_id, Some(app.operator_id()), ) .await .expect("签到应成功"); assert!(result.checked_in_today); // 验证积分到账 let account = points_service::get_account(app.health_state(), app.tenant_id(), patient_id) .await .expect("查询账户应成功"); assert_eq!(account.balance, 5); assert_eq!(account.total_earned, 5); } // --------------------------------------------------------------------------- // 测试 2: 自定义积分增加 // --------------------------------------------------------------------------- #[tokio::test] async fn test_points_earn_custom() { let app = TestApp::new().await; let patient_id = app.create_patient("自定义积分患者").await; seed_rule(&app, "custom_event", 20).await; let tx = points_service::earn_points( app.health_state(), app.tenant_id(), patient_id, "custom_event", Some(app.operator_id()), ) .await .expect("赚取积分应成功"); assert_eq!(tx.amount, 20); assert_eq!(tx.remaining_amount, 20); assert_eq!(tx.transaction_type, "earn"); } // --------------------------------------------------------------------------- // 测试 3: FIFO 消费 — 先消耗最早的积分记录 // --------------------------------------------------------------------------- #[tokio::test] async fn test_points_consume_fifo_deduction() { let app = TestApp::new().await; let patient_id = app.create_patient("FIFO患者").await; seed_rule(&app, "earn_a", 10).await; seed_rule(&app, "earn_b", 30).await; let product = seed_product(&app, "测试商品", 25).await; // 赚两笔: 10 + 30 = 40 points_service::earn_points( app.health_state(), app.tenant_id(), patient_id, "earn_a", Some(app.operator_id()), ) .await .unwrap(); points_service::earn_points( app.health_state(), app.tenant_id(), patient_id, "earn_b", Some(app.operator_id()), ) .await .unwrap(); // 消费 25: FIFO 先消耗第一笔 10(全部用完),再从第二笔消耗 15(剩 15) let order = points_service::exchange_product( app.health_state(), app.tenant_id(), patient_id, ExchangeReq { product_id: product.id }, Some(app.operator_id()), ) .await .expect("兑换应成功"); assert_eq!(order.points_cost, 25); assert_eq!(order.status, "pending"); // 验证余额: 40 - 25 = 15 let account = points_service::get_account(app.health_state(), app.tenant_id(), patient_id) .await .unwrap(); assert_eq!(account.balance, 15); } // --------------------------------------------------------------------------- // 测试 4: 余额不足时返回错误 // --------------------------------------------------------------------------- #[tokio::test] async fn test_points_consume_balance_insufficient() { let app = TestApp::new().await; let patient_id = app.create_patient("余额不足患者").await; seed_rule(&app, "small_earn", 5).await; let product = seed_product(&app, "贵重商品", 100).await; // 只赚 5 分 points_service::earn_points( app.health_state(), app.tenant_id(), patient_id, "small_earn", Some(app.operator_id()), ) .await .unwrap(); // 消费 100 应失败 let result = points_service::exchange_product( app.health_state(), app.tenant_id(), patient_id, ExchangeReq { product_id: product.id }, Some(app.operator_id()), ) .await; assert!(result.is_err(), "余额不足应返回错误"); } // --------------------------------------------------------------------------- // 测试 5: 消费金额等于余额(精确消费) // --------------------------------------------------------------------------- #[tokio::test] async fn test_points_consume_exact_balance() { let app = TestApp::new().await; let patient_id = app.create_patient("精确消费患者").await; seed_rule(&app, "exact_earn", 50).await; let product = seed_product(&app, "等价商品", 50).await; points_service::earn_points( app.health_state(), app.tenant_id(), patient_id, "exact_earn", Some(app.operator_id()), ) .await .unwrap(); let _order = points_service::exchange_product( app.health_state(), app.tenant_id(), patient_id, ExchangeReq { product_id: product.id }, Some(app.operator_id()), ) .await .expect("精确消费应成功"); let account = points_service::get_account(app.health_state(), app.tenant_id(), patient_id) .await .unwrap(); assert_eq!(account.balance, 0); } // --------------------------------------------------------------------------- // 测试 6: 消费金额小于单条记录,正确拆分 // --------------------------------------------------------------------------- #[tokio::test] async fn test_points_consume_partial() { let app = TestApp::new().await; let patient_id = app.create_patient("部分消费患者").await; seed_rule(&app, "big_earn", 100).await; let product = seed_product(&app, "小商品", 30).await; points_service::earn_points( app.health_state(), app.tenant_id(), patient_id, "big_earn", Some(app.operator_id()), ) .await .unwrap(); let _order = points_service::exchange_product( app.health_state(), app.tenant_id(), patient_id, ExchangeReq { product_id: product.id }, Some(app.operator_id()), ) .await .expect("部分消费应成功"); let account = points_service::get_account(app.health_state(), app.tenant_id(), patient_id) .await .unwrap(); assert_eq!(account.balance, 70); } // --------------------------------------------------------------------------- // 测试 7: 首次获取积分时自动创建账户 // --------------------------------------------------------------------------- #[tokio::test] async fn test_points_account_create_on_first_earn() { let app = TestApp::new().await; let patient_id = app.create_patient("新账户患者").await; seed_rule(&app, "first_earn", 10).await; // earn_points 应自动创建账户 points_service::earn_points( app.health_state(), app.tenant_id(), patient_id, "first_earn", Some(app.operator_id()), ) .await .unwrap(); let account = points_service::get_account(app.health_state(), app.tenant_id(), patient_id) .await .expect("账户应已自动创建"); assert_eq!(account.balance, 10); assert_eq!(account.patient_id, patient_id); } // --------------------------------------------------------------------------- // 测试 8: 连续签到奖励 // --------------------------------------------------------------------------- #[tokio::test] async fn test_points_checkin_streak() { let app = TestApp::new().await; let patient_id = app.create_patient("连续签到患者").await; seed_rule(&app, "daily_checkin", 5).await; // 连续签到 3 天验证 consecutive_days 递增 let status = points_service::get_checkin_status( app.health_state(), app.tenant_id(), patient_id, ) .await .expect("查询签到状态应成功"); assert!(!status.checked_in_today); assert_eq!(status.consecutive_days, 0); // 第 1 天签到 let result = points_service::daily_checkin( app.health_state(), app.tenant_id(), patient_id, Some(app.operator_id()), ) .await .unwrap(); assert!(result.checked_in_today); assert_eq!(result.consecutive_days, 1); } // --------------------------------------------------------------------------- // 测试 9: 积分兑换商品 — 创建订单 // --------------------------------------------------------------------------- #[tokio::test] async fn test_points_order_create() { let app = TestApp::new().await; let patient_id = app.create_patient("兑换患者").await; seed_rule(&app, "order_earn", 100).await; let product = seed_product(&app, "兑换商品", 50).await; points_service::earn_points( app.health_state(), app.tenant_id(), patient_id, "order_earn", Some(app.operator_id()), ) .await .unwrap(); let order = points_service::exchange_product( app.health_state(), app.tenant_id(), patient_id, ExchangeReq { product_id: product.id }, Some(app.operator_id()), ) .await .expect("创建订单应成功"); assert_eq!(order.status, "pending"); assert_eq!(order.points_cost, 50); assert!(order.qr_code.is_some()); } // --------------------------------------------------------------------------- // 测试 10: 积分不足时订单失败 // --------------------------------------------------------------------------- #[tokio::test] async fn test_points_order_insufficient_cancel() { let app = TestApp::new().await; let patient_id = app.create_patient("不足兑换患者").await; seed_rule(&app, "tiny_earn", 1).await; let product = seed_product(&app, "昂贵商品", 999).await; points_service::earn_points( app.health_state(), app.tenant_id(), patient_id, "tiny_earn", Some(app.operator_id()), ) .await .unwrap(); let result = points_service::exchange_product( app.health_state(), app.tenant_id(), patient_id, ExchangeReq { product_id: product.id }, Some(app.operator_id()), ) .await; assert!(result.is_err(), "积分不足兑换应失败"); } // --------------------------------------------------------------------------- // 测试 11: 交易记录查询 // --------------------------------------------------------------------------- #[tokio::test] async fn test_points_transaction_history() { let app = TestApp::new().await; let patient_id = app.create_patient("记录查询患者").await; seed_rule(&app, "history_earn", 10).await; // 赚两笔 points_service::earn_points( app.health_state(), app.tenant_id(), patient_id, "history_earn", Some(app.operator_id()), ) .await .unwrap(); points_service::earn_points( app.health_state(), app.tenant_id(), patient_id, "history_earn", Some(app.operator_id()), ) .await .unwrap(); let history = points_service::list_transactions( app.health_state(), app.tenant_id(), patient_id, 1, 20, ) .await .expect("查询记录应成功"); assert_eq!(history.total, 2); assert_eq!(history.data.len(), 2); } // --------------------------------------------------------------------------- // 测试 12: 租户隔离 — A 的积分 B 看不到 // --------------------------------------------------------------------------- #[tokio::test] async fn test_points_tenant_isolation() { let app = TestApp::new().await; let patient_a = app.create_patient("租户A患者").await; seed_rule(&app, "iso_earn", 50).await; points_service::earn_points( app.health_state(), app.tenant_id(), patient_a, "iso_earn", Some(app.operator_id()), ) .await .unwrap(); // 用不同 tenant_id 查询应找不到账户 let other_tenant = uuid::Uuid::new_v4(); let result = points_service::get_account(app.health_state(), other_tenant, patient_a).await; // 不同租户应无法获取(返回错误或空账户) // 实现可能是返回新建的空账户,取决于 get_account 行为 if let Ok(account) = result { assert_eq!(account.balance, 0, "不同租户应看到独立账户"); } }