use std::collections::HashMap;
use chrono::{Datelike, NaiveDate};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::assertion::ComplianceAssertion;
use super::standard_id::StandardId;
use crate::models::graph_properties::{GraphPropertyValue, ToNodeProperties};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeficiencyLevel {
MaterialWeakness,
SignificantDeficiency,
ControlDeficiency,
}
impl DeficiencyLevel {
pub fn severity_score(&self) -> f64 {
match self {
Self::MaterialWeakness => 1.0,
Self::SignificantDeficiency => 0.66,
Self::ControlDeficiency => 0.33,
}
}
}
impl std::fmt::Display for DeficiencyLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MaterialWeakness => write!(f, "Material Weakness"),
Self::SignificantDeficiency => write!(f, "Significant Deficiency"),
Self::ControlDeficiency => write!(f, "Control Deficiency"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FindingSeverity {
High,
Moderate,
Low,
}
impl FindingSeverity {
pub fn score(&self) -> f64 {
match self {
Self::High => 1.0,
Self::Moderate => 0.66,
Self::Low => 0.33,
}
}
}
impl std::fmt::Display for FindingSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::High => write!(f, "High"),
Self::Moderate => write!(f, "Moderate"),
Self::Low => write!(f, "Low"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RemediationStatus {
Open,
InProgress,
Remediated,
}
impl std::fmt::Display for RemediationStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Open => write!(f, "Open"),
Self::InProgress => write!(f, "In Progress"),
Self::Remediated => write!(f, "Remediated"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplianceFinding {
pub finding_id: Uuid,
pub company_code: String,
pub title: String,
pub description: String,
pub severity: FindingSeverity,
pub deficiency_level: DeficiencyLevel,
pub control_id: Option<String>,
pub procedure_id: Option<String>,
pub affected_assertions: Vec<ComplianceAssertion>,
pub related_standards: Vec<StandardId>,
pub identified_date: NaiveDate,
pub remediation_status: RemediationStatus,
pub financial_impact: Option<Decimal>,
pub is_repeat: bool,
pub affected_accounts: Vec<String>,
pub fiscal_year: i32,
}
impl ComplianceFinding {
pub fn new(
company_code: impl Into<String>,
title: impl Into<String>,
severity: FindingSeverity,
deficiency_level: DeficiencyLevel,
identified_date: NaiveDate,
) -> Self {
Self {
finding_id: Uuid::new_v4(),
company_code: company_code.into(),
title: title.into(),
description: String::new(),
severity,
deficiency_level,
control_id: None,
procedure_id: None,
affected_assertions: Vec::new(),
related_standards: Vec::new(),
identified_date,
remediation_status: RemediationStatus::Open,
financial_impact: None,
is_repeat: false,
affected_accounts: Vec::new(),
fiscal_year: identified_date.year(),
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = desc.into();
self
}
pub fn on_control(mut self, control_id: impl Into<String>) -> Self {
self.control_id = Some(control_id.into());
self
}
pub fn identified_by(mut self, procedure_id: impl Into<String>) -> Self {
self.procedure_id = Some(procedure_id.into());
self
}
pub fn with_assertion(mut self, assertion: ComplianceAssertion) -> Self {
self.affected_assertions.push(assertion);
self
}
pub fn with_standard(mut self, id: StandardId) -> Self {
self.related_standards.push(id);
self
}
pub fn with_remediation(mut self, status: RemediationStatus) -> Self {
self.remediation_status = status;
self
}
pub fn as_repeat(mut self) -> Self {
self.is_repeat = true;
self
}
}
impl ToNodeProperties for ComplianceFinding {
fn node_type_name(&self) -> &'static str {
"compliance_finding"
}
fn node_type_code(&self) -> u16 {
511
}
fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
let mut p = HashMap::new();
p.insert(
"findingId".into(),
GraphPropertyValue::String(self.finding_id.to_string()),
);
p.insert(
"companyCode".into(),
GraphPropertyValue::String(self.company_code.clone()),
);
p.insert(
"title".into(),
GraphPropertyValue::String(self.title.clone()),
);
p.insert(
"severity".into(),
GraphPropertyValue::String(self.severity.to_string()),
);
p.insert(
"severityScore".into(),
GraphPropertyValue::Float(self.severity.score()),
);
p.insert(
"deficiencyLevel".into(),
GraphPropertyValue::String(self.deficiency_level.to_string()),
);
p.insert(
"deficiencySeverityScore".into(),
GraphPropertyValue::Float(self.deficiency_level.severity_score()),
);
if let Some(ref cid) = self.control_id {
p.insert("controlId".into(), GraphPropertyValue::String(cid.clone()));
}
if let Some(ref pid) = self.procedure_id {
p.insert(
"procedureId".into(),
GraphPropertyValue::String(pid.clone()),
);
}
p.insert(
"identifiedDate".into(),
GraphPropertyValue::Date(self.identified_date),
);
p.insert(
"remediationStatus".into(),
GraphPropertyValue::String(self.remediation_status.to_string()),
);
if let Some(impact) = self.financial_impact {
p.insert(
"financialImpact".into(),
GraphPropertyValue::Decimal(impact),
);
}
p.insert("isRepeat".into(), GraphPropertyValue::Bool(self.is_repeat));
p.insert(
"fiscalYear".into(),
GraphPropertyValue::Int(self.fiscal_year as i64),
);
if !self.affected_assertions.is_empty() {
p.insert(
"affectedAssertions".into(),
GraphPropertyValue::StringList(
self.affected_assertions
.iter()
.map(|a| a.to_string())
.collect(),
),
);
}
if !self.related_standards.is_empty() {
p.insert(
"relatedStandards".into(),
GraphPropertyValue::StringList(
self.related_standards
.iter()
.map(|s| s.as_str().to_string())
.collect(),
),
);
}
if !self.affected_accounts.is_empty() {
p.insert(
"affectedAccounts".into(),
GraphPropertyValue::StringList(self.affected_accounts.clone()),
);
}
p
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_finding_creation() {
let date = NaiveDate::from_ymd_opt(2025, 6, 30).expect("valid date");
let finding = ComplianceFinding::new(
"C001",
"Three-way match exception",
FindingSeverity::Moderate,
DeficiencyLevel::SignificantDeficiency,
date,
)
.on_control("C010")
.with_assertion(ComplianceAssertion::Occurrence)
.with_standard(StandardId::new("SOX", "404"));
assert_eq!(finding.severity, FindingSeverity::Moderate);
assert_eq!(finding.control_id.as_deref(), Some("C010"));
assert_eq!(finding.related_standards.len(), 1);
}
#[test]
fn test_deficiency_severity_ordering() {
assert!(
DeficiencyLevel::MaterialWeakness.severity_score()
> DeficiencyLevel::SignificantDeficiency.severity_score()
);
assert!(
DeficiencyLevel::SignificantDeficiency.severity_score()
> DeficiencyLevel::ControlDeficiency.severity_score()
);
}
}