koprogo_api/application/use_cases/
organization_use_cases.rs1use crate::application::ports::OrganizationRepository;
2use crate::domain::entities::{Organization, SubscriptionPlan};
3use chrono::Utc;
4use std::sync::Arc;
5use uuid::Uuid;
6use validator::Validate;
7
8pub struct OrganizationUseCases {
9 repo: Arc<dyn OrganizationRepository>,
10}
11
12impl OrganizationUseCases {
13 pub fn new(repo: Arc<dyn OrganizationRepository>) -> Self {
14 Self { repo }
15 }
16
17 pub async fn list_all(&self) -> Result<Vec<Organization>, String> {
18 self.repo.find_all().await
19 }
20
21 pub async fn list_page(
27 &self,
28 recherche: Option<String>,
29 limit: i64,
30 offset: i64,
31 ) -> Result<(Vec<Organization>, i64), String> {
32 let page = self
33 .repo
34 .find_page(recherche.clone(), limit, offset)
35 .await?;
36 let total = self.repo.count_matching(recherche).await?;
37 Ok((page, total))
38 }
39
40 pub async fn create(
41 &self,
42 name: String,
43 slug: String,
44 contact_email: String,
45 contact_phone: Option<String>,
46 subscription_plan: String,
47 ) -> Result<Organization, String> {
48 let plan = subscription_plan
49 .parse::<SubscriptionPlan>()
50 .map_err(|_| "invalid_plan".to_string())?;
51
52 let (max_buildings, max_users) = plan_limits(&plan);
53
54 let org = Organization {
55 id: Uuid::new_v4(),
56 name: name.trim().to_string(),
57 slug: slug.trim().to_lowercase(),
58 contact_email: contact_email.trim().to_lowercase(),
59 contact_phone,
60 subscription_plan: plan,
61 max_buildings,
62 max_users,
63 is_active: true,
64 created_at: Utc::now(),
65 updated_at: Utc::now(),
66 };
67
68 org.validate()
69 .map_err(|e| format!("validation_error:{}", e))?;
70
71 self.repo.create(&org).await
72 }
73
74 pub async fn update(
75 &self,
76 id: Uuid,
77 name: String,
78 slug: String,
79 contact_email: String,
80 contact_phone: Option<String>,
81 subscription_plan: String,
82 ) -> Result<Organization, String> {
83 let mut org = self
84 .repo
85 .find_by_id(id)
86 .await?
87 .ok_or_else(|| "not_found".to_string())?;
88
89 let plan = subscription_plan
90 .parse::<SubscriptionPlan>()
91 .map_err(|_| "invalid_plan".to_string())?;
92
93 let (max_buildings, max_users) = plan_limits(&plan);
94
95 org.name = name.trim().to_string();
96 org.slug = slug.trim().to_lowercase();
97 org.contact_email = contact_email.trim().to_lowercase();
98 org.contact_phone = contact_phone;
99 org.subscription_plan = plan;
100 org.max_buildings = max_buildings;
101 org.max_users = max_users;
102 org.updated_at = Utc::now();
103
104 org.validate()
105 .map_err(|e| format!("validation_error:{}", e))?;
106
107 self.repo.update(&org).await
108 }
109
110 pub async fn activate(&self, id: Uuid) -> Result<Organization, String> {
111 let mut org = self
112 .repo
113 .find_by_id(id)
114 .await?
115 .ok_or_else(|| "not_found".to_string())?;
116 org.activate();
117 self.repo.update(&org).await
118 }
119
120 pub async fn suspend(&self, id: Uuid) -> Result<Organization, String> {
121 let mut org = self
122 .repo
123 .find_by_id(id)
124 .await?
125 .ok_or_else(|| "not_found".to_string())?;
126 org.deactivate();
127 self.repo.update(&org).await
128 }
129
130 pub async fn delete(&self, id: Uuid) -> Result<bool, String> {
131 self.repo.delete(id).await
132 }
133}
134
135fn plan_limits(plan: &SubscriptionPlan) -> (i32, i32) {
136 match plan {
137 SubscriptionPlan::Free => (1, 3),
138 SubscriptionPlan::Starter => (5, 10),
139 SubscriptionPlan::Professional => (20, 50),
140 SubscriptionPlan::Enterprise => (999, 999),
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use async_trait::async_trait;
148 use chrono::Utc;
149
150 struct MockOrgRepository {
151 orgs: Vec<Organization>,
152 }
153
154 fn make_org(name: &str) -> Organization {
155 Organization {
156 id: Uuid::new_v4(),
157 name: name.to_string(),
158 slug: name.to_lowercase().replace(' ', "-"),
159 contact_email: "test@test.com".to_string(),
160 contact_phone: None,
161 subscription_plan: SubscriptionPlan::Free,
162 max_buildings: 1,
163 max_users: 3,
164 is_active: true,
165 created_at: Utc::now(),
166 updated_at: Utc::now(),
167 }
168 }
169
170 impl MockOrgRepository {
171 fn filtrer<'a>(
172 orgs: &'a [Organization],
173 recherche: Option<&'a str>,
174 ) -> impl Iterator<Item = &'a Organization> {
175 let motif = recherche
176 .map(str::trim)
177 .filter(|r| !r.is_empty())
178 .map(|r| r.to_lowercase());
179 orgs.iter().filter(move |o| match &motif {
180 None => true,
181 Some(m) => o.name.to_lowercase().contains(m) || o.slug.to_lowercase().contains(m),
182 })
183 }
184 }
185
186 #[async_trait]
187 impl OrganizationRepository for MockOrgRepository {
188 async fn create(&self, org: &Organization) -> Result<Organization, String> {
189 Ok(org.clone())
190 }
191 async fn find_by_id(&self, id: Uuid) -> Result<Option<Organization>, String> {
192 Ok(self.orgs.iter().find(|o| o.id == id).cloned())
193 }
194 async fn find_by_slug(&self, slug: &str) -> Result<Option<Organization>, String> {
195 Ok(self.orgs.iter().find(|o| o.slug == slug).cloned())
196 }
197 async fn find_all(&self) -> Result<Vec<Organization>, String> {
198 Ok(self.orgs.clone())
199 }
200
201 async fn find_page(
202 &self,
203 recherche: Option<String>,
204 limit: i64,
205 offset: i64,
206 ) -> Result<Vec<Organization>, String> {
207 Ok(Self::filtrer(&self.orgs, recherche.as_deref())
211 .skip(offset.max(0) as usize)
212 .take(limit.max(0) as usize)
213 .cloned()
214 .collect())
215 }
216
217 async fn count_matching(&self, recherche: Option<String>) -> Result<i64, String> {
218 Ok(Self::filtrer(&self.orgs, recherche.as_deref()).count() as i64)
219 }
220 async fn update(&self, org: &Organization) -> Result<Organization, String> {
221 Ok(org.clone())
222 }
223 async fn delete(&self, _id: Uuid) -> Result<bool, String> {
224 Ok(true)
225 }
226 async fn count_buildings(&self, _org_id: Uuid) -> Result<i64, String> {
227 Ok(0)
228 }
229 }
230
231 #[tokio::test]
232 async fn test_list_all() {
233 let org = make_org("TestOrg");
234 let repo = Arc::new(MockOrgRepository { orgs: vec![org] });
235 let uc = OrganizationUseCases::new(repo);
236 let result = uc.list_all().await.unwrap();
237 assert_eq!(result.len(), 1);
238 }
239
240 #[tokio::test]
241 async fn test_create_invalid_plan_returns_error() {
242 let repo = Arc::new(MockOrgRepository { orgs: vec![] });
243 let uc = OrganizationUseCases::new(repo);
244 let result = uc
245 .create(
246 "Test".to_string(),
247 "test".to_string(),
248 "a@b.com".to_string(),
249 None,
250 "invalid_plan".to_string(),
251 )
252 .await;
253 assert!(result.is_err());
254 assert_eq!(result.unwrap_err(), "invalid_plan");
255 }
256
257 #[tokio::test]
258 async fn test_activate_not_found_returns_error() {
259 let repo = Arc::new(MockOrgRepository { orgs: vec![] });
260 let uc = OrganizationUseCases::new(repo);
261 let result = uc.activate(Uuid::new_v4()).await;
262 assert!(result.is_err());
263 assert_eq!(result.unwrap_err(), "not_found");
264 }
265
266 #[tokio::test]
267 async fn test_suspend_org() {
268 let org = make_org("ActiveOrg");
269 let id = org.id;
270 let repo = Arc::new(MockOrgRepository { orgs: vec![org] });
271 let uc = OrganizationUseCases::new(repo);
272 let result = uc.suspend(id).await.unwrap();
273 assert!(!result.is_active);
274 }
275}