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 create(
22 &self,
23 name: String,
24 slug: String,
25 contact_email: String,
26 contact_phone: Option<String>,
27 subscription_plan: String,
28 ) -> Result<Organization, String> {
29 let plan = subscription_plan
30 .parse::<SubscriptionPlan>()
31 .map_err(|_| "invalid_plan".to_string())?;
32
33 let (max_buildings, max_users) = plan_limits(&plan);
34
35 let org = Organization {
36 id: Uuid::new_v4(),
37 name: name.trim().to_string(),
38 slug: slug.trim().to_lowercase(),
39 contact_email: contact_email.trim().to_lowercase(),
40 contact_phone,
41 subscription_plan: plan,
42 max_buildings,
43 max_users,
44 is_active: true,
45 created_at: Utc::now(),
46 updated_at: Utc::now(),
47 };
48
49 org.validate()
50 .map_err(|e| format!("validation_error:{}", e))?;
51
52 self.repo.create(&org).await
53 }
54
55 pub async fn update(
56 &self,
57 id: Uuid,
58 name: String,
59 slug: String,
60 contact_email: String,
61 contact_phone: Option<String>,
62 subscription_plan: String,
63 ) -> Result<Organization, String> {
64 let mut org = self
65 .repo
66 .find_by_id(id)
67 .await?
68 .ok_or_else(|| "not_found".to_string())?;
69
70 let plan = subscription_plan
71 .parse::<SubscriptionPlan>()
72 .map_err(|_| "invalid_plan".to_string())?;
73
74 let (max_buildings, max_users) = plan_limits(&plan);
75
76 org.name = name.trim().to_string();
77 org.slug = slug.trim().to_lowercase();
78 org.contact_email = contact_email.trim().to_lowercase();
79 org.contact_phone = contact_phone;
80 org.subscription_plan = plan;
81 org.max_buildings = max_buildings;
82 org.max_users = max_users;
83 org.updated_at = Utc::now();
84
85 org.validate()
86 .map_err(|e| format!("validation_error:{}", e))?;
87
88 self.repo.update(&org).await
89 }
90
91 pub async fn activate(&self, id: Uuid) -> Result<Organization, String> {
92 let mut org = self
93 .repo
94 .find_by_id(id)
95 .await?
96 .ok_or_else(|| "not_found".to_string())?;
97 org.activate();
98 self.repo.update(&org).await
99 }
100
101 pub async fn suspend(&self, id: Uuid) -> Result<Organization, String> {
102 let mut org = self
103 .repo
104 .find_by_id(id)
105 .await?
106 .ok_or_else(|| "not_found".to_string())?;
107 org.deactivate();
108 self.repo.update(&org).await
109 }
110
111 pub async fn delete(&self, id: Uuid) -> Result<bool, String> {
112 self.repo.delete(id).await
113 }
114}
115
116fn plan_limits(plan: &SubscriptionPlan) -> (i32, i32) {
117 match plan {
118 SubscriptionPlan::Free => (1, 3),
119 SubscriptionPlan::Starter => (5, 10),
120 SubscriptionPlan::Professional => (20, 50),
121 SubscriptionPlan::Enterprise => (999, 999),
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use async_trait::async_trait;
129 use chrono::Utc;
130
131 struct MockOrgRepository {
132 orgs: Vec<Organization>,
133 }
134
135 fn make_org(name: &str) -> Organization {
136 Organization {
137 id: Uuid::new_v4(),
138 name: name.to_string(),
139 slug: name.to_lowercase().replace(' ', "-"),
140 contact_email: "test@test.com".to_string(),
141 contact_phone: None,
142 subscription_plan: SubscriptionPlan::Free,
143 max_buildings: 1,
144 max_users: 3,
145 is_active: true,
146 created_at: Utc::now(),
147 updated_at: Utc::now(),
148 }
149 }
150
151 #[async_trait]
152 impl OrganizationRepository for MockOrgRepository {
153 async fn create(&self, org: &Organization) -> Result<Organization, String> {
154 Ok(org.clone())
155 }
156 async fn find_by_id(&self, id: Uuid) -> Result<Option<Organization>, String> {
157 Ok(self.orgs.iter().find(|o| o.id == id).cloned())
158 }
159 async fn find_by_slug(&self, slug: &str) -> Result<Option<Organization>, String> {
160 Ok(self.orgs.iter().find(|o| o.slug == slug).cloned())
161 }
162 async fn find_all(&self) -> Result<Vec<Organization>, String> {
163 Ok(self.orgs.clone())
164 }
165 async fn update(&self, org: &Organization) -> Result<Organization, String> {
166 Ok(org.clone())
167 }
168 async fn delete(&self, _id: Uuid) -> Result<bool, String> {
169 Ok(true)
170 }
171 async fn count_buildings(&self, _org_id: Uuid) -> Result<i64, String> {
172 Ok(0)
173 }
174 }
175
176 #[tokio::test]
177 async fn test_list_all() {
178 let org = make_org("TestOrg");
179 let repo = Arc::new(MockOrgRepository { orgs: vec![org] });
180 let uc = OrganizationUseCases::new(repo);
181 let result = uc.list_all().await.unwrap();
182 assert_eq!(result.len(), 1);
183 }
184
185 #[tokio::test]
186 async fn test_create_invalid_plan_returns_error() {
187 let repo = Arc::new(MockOrgRepository { orgs: vec![] });
188 let uc = OrganizationUseCases::new(repo);
189 let result = uc
190 .create(
191 "Test".to_string(),
192 "test".to_string(),
193 "a@b.com".to_string(),
194 None,
195 "invalid_plan".to_string(),
196 )
197 .await;
198 assert!(result.is_err());
199 assert_eq!(result.unwrap_err(), "invalid_plan");
200 }
201
202 #[tokio::test]
203 async fn test_activate_not_found_returns_error() {
204 let repo = Arc::new(MockOrgRepository { orgs: vec![] });
205 let uc = OrganizationUseCases::new(repo);
206 let result = uc.activate(Uuid::new_v4()).await;
207 assert!(result.is_err());
208 assert_eq!(result.unwrap_err(), "not_found");
209 }
210
211 #[tokio::test]
212 async fn test_suspend_org() {
213 let org = make_org("ActiveOrg");
214 let id = org.id;
215 let repo = Arc::new(MockOrgRepository { orgs: vec![org] });
216 let uc = OrganizationUseCases::new(repo);
217 let result = uc.suspend(id).await.unwrap();
218 assert!(!result.is_active);
219 }
220}