koprogo_api/domain/entities/
contract_evaluation.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use uuid::Uuid;
5
6/// Contract Evaluation (Review of contractor work)
7/// Issue #276: Marketplace corps de métier + ContractEvaluation
8/// Art. 3.89 §5 12° Code Civil Belge: Évaluations contracteurs (L13 annual report)
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub struct ContractEvaluation {
11    pub id: Uuid,
12    pub organization_id: Uuid,
13    pub service_provider_id: Uuid,
14    pub quote_id: Option<Uuid>,
15    pub ticket_id: Option<Uuid>,
16    pub evaluator_id: Uuid,
17    pub building_id: Uuid,
18    /// Criteria scores: qualite, delai, prix, communication, proprete, conformite_devis — each 0-5
19    pub criteria: HashMap<String, u8>,
20    pub global_score: f64, // weighted average 0-5
21    pub comments: Option<String>,
22    pub would_recommend: bool,
23    pub is_legal_evaluation: bool, // true = rapport L13 (Art. 3.89 §5 12° CC)
24    pub is_anonymous: bool,
25    pub created_at: DateTime<Utc>,
26}
27
28impl ContractEvaluation {
29    pub fn new(
30        organization_id: Uuid,
31        service_provider_id: Uuid,
32        evaluator_id: Uuid,
33        building_id: Uuid,
34        criteria: HashMap<String, u8>,
35        would_recommend: bool,
36    ) -> Result<Self, String> {
37        // Validate all criteria values are 0-5
38        for (key, &val) in &criteria {
39            if val > 5 {
40                return Err(format!("Criteria '{}' must be 0-5, got {}", key, val));
41            }
42        }
43
44        let global = if criteria.is_empty() {
45            0.0
46        } else {
47            criteria.values().map(|&v| v as f64).sum::<f64>() / criteria.len() as f64
48        };
49
50        Ok(Self {
51            id: Uuid::new_v4(),
52            organization_id,
53            service_provider_id,
54            quote_id: None,
55            ticket_id: None,
56            evaluator_id,
57            building_id,
58            criteria,
59            global_score: global,
60            comments: None,
61            would_recommend,
62            is_legal_evaluation: false,
63            is_anonymous: false,
64            created_at: Utc::now(),
65        })
66    }
67
68    /// Link evaluation to quote (optional)
69    pub fn link_quote(&mut self, quote_id: Uuid) -> Result<(), String> {
70        self.quote_id = Some(quote_id);
71        Ok(())
72    }
73
74    /// Link evaluation to ticket (optional)
75    pub fn link_ticket(&mut self, ticket_id: Uuid) -> Result<(), String> {
76        self.ticket_id = Some(ticket_id);
77        Ok(())
78    }
79
80    /// Mark as legal evaluation (L13 annual report)
81    pub fn mark_as_legal_evaluation(&mut self) -> Result<(), String> {
82        self.is_legal_evaluation = true;
83        Ok(())
84    }
85
86    /// Mark as anonymous (GDPR compliant)
87    pub fn mark_as_anonymous(&mut self) -> Result<(), String> {
88        self.is_anonymous = true;
89        Ok(())
90    }
91
92    /// Recalculate global score based on updated criteria
93    pub fn recalculate_global_score(&mut self) -> Result<(), String> {
94        self.global_score = if self.criteria.is_empty() {
95            0.0
96        } else {
97            self.criteria.values().map(|&v| v as f64).sum::<f64>() / self.criteria.len() as f64
98        };
99        Ok(())
100    }
101
102    /// Set comments on evaluation
103    pub fn set_comments(&mut self, comments: String) -> Result<(), String> {
104        if comments.is_empty() {
105            return Err("Comments cannot be empty".to_string());
106        }
107        self.comments = Some(comments);
108        Ok(())
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_contract_evaluation_new_success() {
118        let org_id = Uuid::new_v4();
119        let provider_id = Uuid::new_v4();
120        let evaluator_id = Uuid::new_v4();
121        let building_id = Uuid::new_v4();
122
123        let mut criteria = HashMap::new();
124        criteria.insert("qualite".to_string(), 5);
125        criteria.insert("delai".to_string(), 4);
126        criteria.insert("prix".to_string(), 3);
127
128        let eval = ContractEvaluation::new(
129            org_id,
130            provider_id,
131            evaluator_id,
132            building_id,
133            criteria,
134            true,
135        );
136
137        assert!(eval.is_ok());
138        let e = eval.unwrap();
139        assert_eq!(e.criteria.len(), 3);
140        assert!(e.global_score > 0.0);
141        assert!(e.would_recommend);
142    }
143
144    #[test]
145    fn test_contract_evaluation_invalid_criteria() {
146        let org_id = Uuid::new_v4();
147        let provider_id = Uuid::new_v4();
148        let evaluator_id = Uuid::new_v4();
149        let building_id = Uuid::new_v4();
150
151        let mut criteria = HashMap::new();
152        criteria.insert("qualite".to_string(), 6); // Invalid: > 5
153
154        let result = ContractEvaluation::new(
155            org_id,
156            provider_id,
157            evaluator_id,
158            building_id,
159            criteria,
160            true,
161        );
162
163        assert!(result.is_err());
164    }
165
166    #[test]
167    fn test_global_score_calculation() {
168        let org_id = Uuid::new_v4();
169        let provider_id = Uuid::new_v4();
170        let evaluator_id = Uuid::new_v4();
171        let building_id = Uuid::new_v4();
172
173        let mut criteria = HashMap::new();
174        criteria.insert("qualite".to_string(), 5);
175        criteria.insert("delai".to_string(), 3);
176
177        let eval = ContractEvaluation::new(
178            org_id,
179            provider_id,
180            evaluator_id,
181            building_id,
182            criteria,
183            true,
184        )
185        .unwrap();
186
187        // (5 + 3) / 2 = 4.0
188        assert_eq!(eval.global_score, 4.0);
189    }
190
191    #[test]
192    fn test_mark_legal_evaluation() {
193        let org_id = Uuid::new_v4();
194        let provider_id = Uuid::new_v4();
195        let evaluator_id = Uuid::new_v4();
196        let building_id = Uuid::new_v4();
197
198        let criteria = HashMap::new();
199        let mut eval = ContractEvaluation::new(
200            org_id,
201            provider_id,
202            evaluator_id,
203            building_id,
204            criteria,
205            true,
206        )
207        .unwrap();
208
209        assert!(!eval.is_legal_evaluation);
210        let _ = eval.mark_as_legal_evaluation();
211        assert!(eval.is_legal_evaluation);
212    }
213}