koprogo_api/application/use_cases/
organization_use_cases.rs

1use 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}