//! erp-health 预约排班集成测试 //! //! 验证预约 CRUD、租户隔离、CAS 排班名额等核心行为。 //! 使用 TestDb 创建隔离 PostgreSQL 数据库,直接调用 service 层函数。 //! 预约创建依赖患者 + 医护档案 + 排班三条前置数据。 use erp_core::events::EventBus; use erp_health::dto::appointment_dto::CreateAppointmentReq; use erp_health::dto::doctor_dto::CreateDoctorReq; use erp_health::dto::patient_dto::CreatePatientReq; use erp_health::service::{ appointment_service, doctor_service, patient_service, }; use erp_health::state::HealthState; use erp_core::crypto::PiiCrypto; use super::test_db::TestDb; /// 构建测试用 HealthState fn make_state(db: &sea_orm::DatabaseConnection) -> HealthState { HealthState { db: db.clone(), event_bus: EventBus::new(100), crypto: PiiCrypto::dev_default(), } } /// 创建患者并返回其 ID async fn seed_patient( state: &HealthState, tenant_id: uuid::Uuid, name: &str, ) -> uuid::Uuid { let req = CreatePatientReq { name: name.to_string(), gender: Some("male".to_string()), birth_date: None, blood_type: None, id_number: None, allergy_history: None, medical_history_summary: None, emergency_contact_name: None, emergency_contact_phone: None, source: None, notes: None, }; let patient = patient_service::create_patient(state, tenant_id, None, req) .await .expect("创建患者应成功"); patient.id } /// 创建医护档案并返回其 ID async fn seed_doctor( state: &HealthState, tenant_id: uuid::Uuid, name: &str, ) -> uuid::Uuid { let req = CreateDoctorReq { user_id: None, name: name.to_string(), department: Some("内科".to_string()), title: Some("主治医师".to_string()), specialty: Some("心血管内科".to_string()), license_number: None, bio: None, }; let doctor = doctor_service::create_doctor(state, tenant_id, None, req) .await .expect("创建医护档案应成功"); doctor.id } /// 创建排班并返回其 ID async fn seed_schedule( state: &HealthState, tenant_id: uuid::Uuid, doctor_id: uuid::Uuid, date: chrono::NaiveDate, ) -> uuid::Uuid { let req = erp_health::dto::appointment_dto::CreateScheduleReq { doctor_id, schedule_date: date, period_type: Some("am".to_string()), start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap(), end_time: chrono::NaiveTime::from_hms_opt(12, 0, 0).unwrap(), max_appointments: 10, }; let schedule = appointment_service::create_schedule(state, tenant_id, None, req) .await .expect("创建排班应成功"); schedule.id } // --------------------------------------------------------------------------- // 测试 1: 创建预约 // --------------------------------------------------------------------------- #[tokio::test] async fn test_create_appointment() { let test_db = TestDb::new().await; let state = make_state(test_db.db()); let tenant_id = uuid::Uuid::new_v4(); let operator_id = uuid::Uuid::new_v4(); // 前置:患者 + 医护 + 排班 let patient_id = seed_patient(&state, tenant_id, "预约测试患者").await; let doctor_id = seed_doctor(&state, tenant_id, "预约测试医生").await; let date = chrono::NaiveDate::from_ymd_opt(2026, 5, 10).unwrap(); seed_schedule(&state, tenant_id, doctor_id, date).await; let req = CreateAppointmentReq { patient_id, doctor_id: Some(doctor_id), appointment_type: Some("outpatient".to_string()), appointment_date: date, start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap(), end_time: chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap(), notes: Some("首次就诊".to_string()), }; let appointment = appointment_service::create_appointment(&state, tenant_id, Some(operator_id), req) .await .expect("创建预约应成功"); assert_eq!(appointment.patient_id, patient_id); assert_eq!(appointment.doctor_id, Some(doctor_id)); assert_eq!(appointment.appointment_type, "outpatient"); assert_eq!(appointment.status, "pending"); assert_eq!(appointment.version, 1); assert_eq!( appointment.notes, Some("首次就诊".to_string()) ); // 通过 get_appointment 验证存储正确 let found = appointment_service::get_appointment(&state, tenant_id, appointment.id) .await .expect("查询预约应成功"); assert_eq!(found.id, appointment.id); assert_eq!(found.status, "pending"); assert_eq!(found.version, 1); } // --------------------------------------------------------------------------- // 测试 2: 列表查询 — 创建 2 条预约后验证分页计数 // --------------------------------------------------------------------------- #[tokio::test] async fn test_list_appointments() { let test_db = TestDb::new().await; let state = make_state(test_db.db()); let tenant_id = uuid::Uuid::new_v4(); let patient_id = seed_patient(&state, tenant_id, "列表测试患者").await; let doctor_id = seed_doctor(&state, tenant_id, "列表测试医生").await; let date = chrono::NaiveDate::from_ymd_opt(2026, 5, 12).unwrap(); seed_schedule(&state, tenant_id, doctor_id, date).await; // 创建 2 条预约(同一个排班时段,CAS 按排班 start_time 匹配) for _i in 0..2 { let req = CreateAppointmentReq { patient_id, doctor_id: Some(doctor_id), appointment_type: None, appointment_date: date, start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap(), end_time: chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap(), notes: None, }; appointment_service::create_appointment(&state, tenant_id, None, req) .await .expect("创建预约应成功"); } let result = appointment_service::list_appointments( &state, tenant_id, 1, 10, None, None, None, None, ) .await .expect("列表查询应成功"); assert_eq!(result.total, 2, "应有 2 条预约记录"); assert_eq!(result.data.len(), 2, "当前页应返回 2 条"); } // --------------------------------------------------------------------------- // 测试 3: 租户隔离 — 租户 A 的预约对租户 B 不可见 // --------------------------------------------------------------------------- #[tokio::test] async fn test_appointment_tenant_isolation() { let test_db = TestDb::new().await; let state = make_state(test_db.db()); let tenant_a = uuid::Uuid::new_v4(); let tenant_b = uuid::Uuid::new_v4(); // 租户 A 创建完整链路:患者 + 医护 + 排班 + 预约 let patient_a = seed_patient(&state, tenant_a, "租户A患者").await; let doctor_a = seed_doctor(&state, tenant_a, "租户A医生").await; let date = chrono::NaiveDate::from_ymd_opt(2026, 6, 1).unwrap(); seed_schedule(&state, tenant_a, doctor_a, date).await; let req = CreateAppointmentReq { patient_id: patient_a, doctor_id: Some(doctor_a), appointment_type: None, appointment_date: date, start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap(), end_time: chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap(), notes: None, }; let appointment_a = appointment_service::create_appointment(&state, tenant_a, None, req) .await .expect("租户 A 创建预约应成功"); // 租户 B 列表查询应看不到租户 A 的预约 let result_b = appointment_service::list_appointments( &state, tenant_b, 1, 10, None, None, None, None, ) .await .expect("租户 B 列表查询应成功"); assert_eq!(result_b.total, 0, "租户 B 不应看到租户 A 的预约"); assert!(result_b.data.is_empty()); // 租户 B 通过 ID 查询租户 A 的预约应返回错误 let lookup_result = appointment_service::get_appointment(&state, tenant_b, appointment_a.id).await; assert!( lookup_result.is_err(), "跨租户查询预约应返回错误" ); }