diff --git a/crates/erp-health/src/service/validation.rs b/crates/erp-health/src/service/validation.rs index eabb6a0..4b5f993 100644 --- a/crates/erp-health/src/service/validation.rs +++ b/crates/erp-health/src/service/validation.rs @@ -131,6 +131,85 @@ pub fn validate_online_status(value: &str) -> HealthResult<()> { Ok(()) } +/// article.status 枚举白名单 +pub fn validate_article_status(value: &str) -> HealthResult<()> { + validate_enum!(value, "article.status", [ + "draft", "pending_review", "approved", "rejected", "published", + ]); + Ok(()) +} + +/// article.status 状态转换 +/// draft → pending_review, rejected → pending_review +/// pending_review → approved / rejected +/// approved → published +/// published → draft (下架) +pub fn validate_article_status_transition(current: &str, new: &str) -> HealthResult<()> { + if current == new { + return Ok(()); + } + let allowed = match current { + "draft" => matches!(new, "pending_review"), + "pending_review" => matches!(new, "approved" | "rejected"), + "approved" => matches!(new, "published"), + "rejected" => matches!(new, "pending_review"), + "published" => matches!(new, "draft"), + _ => false, + }; + if allowed { + Ok(()) + } else { + Err(HealthError::InvalidStatusTransition(format!( + "article.status: 不允许从 '{}' 转换到 '{}'", current, new + ))) + } +} + +/// dialysis_record.status 枚举白名单 +pub fn validate_dialysis_status(value: &str) -> HealthResult<()> { + validate_enum!(value, "dialysis_record.status", ["draft", "completed", "reviewed"]); + Ok(()) +} + +/// dialysis_record.status 状态转换 +/// draft → completed → reviewed +pub fn validate_dialysis_status_transition(current: &str, new: &str) -> HealthResult<()> { + if current == new { + return Ok(()); + } + let allowed = match current { + "draft" => matches!(new, "completed"), + "completed" => matches!(new, "reviewed"), + _ => false, + }; + if allowed { + Ok(()) + } else { + Err(HealthError::InvalidStatusTransition(format!( + "dialysis_record.status: 不允许从 '{}' 转换到 '{}'", current, new + ))) + } +} + +/// lab_report.status 状态转换 +/// pending → reviewed +pub fn validate_lab_report_status_transition(current: &str, new: &str) -> HealthResult<()> { + if current == new { + return Ok(()); + } + let allowed = match current { + "pending" => matches!(new, "reviewed"), + _ => false, + }; + if allowed { + Ok(()) + } else { + Err(HealthError::InvalidStatusTransition(format!( + "lab_report.status: 不允许从 '{}' 转换到 '{}'", current, new + ))) + } +} + /// follow_up_task.status 状态转换(含 overdue 状态) pub fn validate_follow_up_status_transition(current: &str, new: &str) -> HealthResult<()> { if current == new { @@ -276,6 +355,68 @@ mod tests { #[test] fn online_invalid() { assert!(validate_online_status("away").is_err()); } + // --- article_status --- + #[test] + fn article_draft() { assert!(validate_article_status("draft").is_ok()); } + #[test] + fn article_published() { assert!(validate_article_status("published").is_ok()); } + #[test] + fn article_invalid() { assert!(validate_article_status("archived").is_err()); } + + // --- article_status_transition --- + #[test] + fn art_draft_to_pending_review() { assert!(validate_article_status_transition("draft", "pending_review").is_ok()); } + #[test] + fn art_draft_to_published_fails() { assert!(validate_article_status_transition("draft", "published").is_err()); } + #[test] + fn art_pending_review_to_approved() { assert!(validate_article_status_transition("pending_review", "approved").is_ok()); } + #[test] + fn art_pending_review_to_rejected() { assert!(validate_article_status_transition("pending_review", "rejected").is_ok()); } + #[test] + fn art_pending_review_to_draft_fails() { assert!(validate_article_status_transition("pending_review", "draft").is_err()); } + #[test] + fn art_approved_to_published() { assert!(validate_article_status_transition("approved", "published").is_ok()); } + #[test] + fn art_rejected_to_pending_review() { assert!(validate_article_status_transition("rejected", "pending_review").is_ok()); } + #[test] + fn art_published_to_draft() { assert!(validate_article_status_transition("published", "draft").is_ok()); } + #[test] + fn art_published_to_approved_fails() { assert!(validate_article_status_transition("published", "approved").is_err()); } + #[test] + fn art_same_status_ok() { assert!(validate_article_status_transition("draft", "draft").is_ok()); } + + // --- dialysis_status --- + #[test] + fn dialysis_draft() { assert!(validate_dialysis_status("draft").is_ok()); } + #[test] + fn dialysis_reviewed() { assert!(validate_dialysis_status("reviewed").is_ok()); } + #[test] + fn dialysis_invalid() { assert!(validate_dialysis_status("approved").is_err()); } + + // --- dialysis_status_transition --- + #[test] + fn dial_draft_to_completed() { assert!(validate_dialysis_status_transition("draft", "completed").is_ok()); } + #[test] + fn dial_draft_to_reviewed_fails() { assert!(validate_dialysis_status_transition("draft", "reviewed").is_err()); } + #[test] + fn dial_completed_to_reviewed() { assert!(validate_dialysis_status_transition("completed", "reviewed").is_ok()); } + #[test] + fn dial_completed_to_draft_fails() { assert!(validate_dialysis_status_transition("completed", "draft").is_err()); } + #[test] + fn dial_reviewed_to_any_fails() { assert!(validate_dialysis_status_transition("reviewed", "draft").is_err()); } + #[test] + fn dial_same_status_ok() { assert!(validate_dialysis_status_transition("draft", "draft").is_ok()); } + + // --- lab_report_status_transition --- + #[test] + fn lab_pending_to_reviewed() { assert!(validate_lab_report_status_transition("pending", "reviewed").is_ok()); } + #[test] + fn lab_pending_to_draft_fails() { assert!(validate_lab_report_status_transition("pending", "draft").is_err()); } + #[test] + fn lab_reviewed_to_any_fails() { assert!(validate_lab_report_status_transition("reviewed", "pending").is_err()); } + #[test] + fn lab_same_status_ok() { assert!(validate_lab_report_status_transition("pending", "pending").is_ok()); } + // --- follow_up_status_transition --- #[test] fn fu_pending_to_in_progress() { assert!(validate_follow_up_status_transition("pending", "in_progress").is_ok()); }