koprogo_api/domain/entities/
individual_member.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9pub struct IndividualMember {
10 pub id: Uuid,
11 pub campaign_id: Uuid,
12 pub email: String,
13 pub postal_code: String,
14 pub has_gdpr_consent: bool,
15 pub consent_at: Option<DateTime<Utc>>,
16 pub annual_consumption_kwh: Option<f64>,
17 pub current_provider: Option<String>,
18 pub ean_code: Option<String>, pub unsubscribed_at: Option<DateTime<Utc>>,
20 pub created_at: DateTime<Utc>,
21}
22
23impl IndividualMember {
24 pub fn new(campaign_id: Uuid, email: String, postal_code: String) -> Result<Self, String> {
25 if email.is_empty() || !email.contains('@') {
26 return Err("Invalid email address".to_string());
27 }
28 if postal_code.is_empty() {
29 return Err("Postal code cannot be empty".to_string());
30 }
31
32 Ok(Self {
33 id: Uuid::new_v4(),
34 campaign_id,
35 email,
36 postal_code,
37 has_gdpr_consent: false,
38 consent_at: None,
39 annual_consumption_kwh: None,
40 current_provider: None,
41 ean_code: None,
42 unsubscribed_at: None,
43 created_at: Utc::now(),
44 })
45 }
46
47 pub fn grant_consent(&mut self) -> Result<(), String> {
49 if self.unsubscribed_at.is_some() {
50 return Err("Cannot grant consent to unsubscribed member".to_string());
51 }
52 self.has_gdpr_consent = true;
53 self.consent_at = Some(Utc::now());
54 Ok(())
55 }
56
57 pub fn update_consumption(
59 &mut self,
60 kwh: f64,
61 provider: Option<String>,
62 ean: Option<String>,
63 ) -> Result<(), String> {
64 if kwh < 0.0 {
65 return Err("Annual consumption cannot be negative".to_string());
66 }
67 self.annual_consumption_kwh = Some(kwh);
68 self.current_provider = provider;
69 self.ean_code = ean;
70 Ok(())
71 }
72
73 pub fn unsubscribe(&mut self) -> Result<(), String> {
75 if self.unsubscribed_at.is_some() {
76 return Err("Member already unsubscribed".to_string());
77 }
78 self.unsubscribed_at = Some(Utc::now());
79 Ok(())
80 }
81
82 pub fn is_active(&self) -> bool {
84 self.unsubscribed_at.is_none()
85 }
86
87 pub fn has_valid_consent(&self) -> bool {
89 self.has_gdpr_consent && self.is_active()
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn test_individual_member_new_success() {
99 let campaign_id = Uuid::new_v4();
100 let member = IndividualMember::new(
101 campaign_id,
102 "user@example.com".to_string(),
103 "1000".to_string(),
104 );
105
106 assert!(member.is_ok());
107 let m = member.unwrap();
108 assert_eq!(m.email, "user@example.com");
109 assert_eq!(m.postal_code, "1000");
110 assert!(!m.has_gdpr_consent);
111 assert!(m.is_active());
112 }
113
114 #[test]
115 fn test_individual_member_invalid_email() {
116 let campaign_id = Uuid::new_v4();
117 let result = IndividualMember::new(campaign_id, "invalid".to_string(), "1000".to_string());
118 assert!(result.is_err());
119 }
120
121 #[test]
122 fn test_individual_member_empty_postal_code() {
123 let campaign_id = Uuid::new_v4();
124 let result =
125 IndividualMember::new(campaign_id, "user@example.com".to_string(), "".to_string());
126 assert!(result.is_err());
127 }
128
129 #[test]
130 fn test_grant_consent() {
131 let campaign_id = Uuid::new_v4();
132 let mut member = IndividualMember::new(
133 campaign_id,
134 "user@example.com".to_string(),
135 "1000".to_string(),
136 )
137 .unwrap();
138
139 assert!(!member.has_gdpr_consent);
140 let _ = member.grant_consent();
141 assert!(member.has_gdpr_consent);
142 assert!(member.consent_at.is_some());
143 }
144
145 #[test]
146 fn test_unsubscribe() {
147 let campaign_id = Uuid::new_v4();
148 let mut member = IndividualMember::new(
149 campaign_id,
150 "user@example.com".to_string(),
151 "1000".to_string(),
152 )
153 .unwrap();
154
155 assert!(member.is_active());
156 let _ = member.unsubscribe();
157 assert!(!member.is_active());
158 assert!(member.unsubscribed_at.is_some());
159 }
160
161 #[test]
162 fn test_has_valid_consent() {
163 let campaign_id = Uuid::new_v4();
164 let mut member = IndividualMember::new(
165 campaign_id,
166 "user@example.com".to_string(),
167 "1000".to_string(),
168 )
169 .unwrap();
170
171 assert!(!member.has_valid_consent());
172 let _ = member.grant_consent();
173 assert!(member.has_valid_consent());
174 let _ = member.unsubscribe();
175 assert!(!member.has_valid_consent());
176 }
177
178 #[test]
179 fn test_update_consumption() {
180 let campaign_id = Uuid::new_v4();
181 let mut member = IndividualMember::new(
182 campaign_id,
183 "user@example.com".to_string(),
184 "1000".to_string(),
185 )
186 .unwrap();
187
188 let result = member.update_consumption(
189 5000.0,
190 Some("Engie".to_string()),
191 Some("501234567890".to_string()),
192 );
193 assert!(result.is_ok());
194 assert_eq!(member.annual_consumption_kwh, Some(5000.0));
195 assert_eq!(member.current_provider, Some("Engie".to_string()));
196 }
197
198 #[test]
199 fn test_update_consumption_negative() {
200 let campaign_id = Uuid::new_v4();
201 let mut member = IndividualMember::new(
202 campaign_id,
203 "user@example.com".to_string(),
204 "1000".to_string(),
205 )
206 .unwrap();
207
208 let result = member.update_consumption(-100.0, None, None);
209 assert!(result.is_err());
210 }
211}