koprogo_api/infrastructure/web/handlers/
organization_handlers.rs

1use crate::domain::entities::Organization;
2use crate::infrastructure::web::{AppState, AuthenticatedUser};
3use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8#[derive(Serialize)]
9pub struct OrganizationResponse {
10    pub id: String,
11    pub name: String,
12    pub slug: String,
13    pub contact_email: String,
14    pub contact_phone: Option<String>,
15    pub subscription_plan: String,
16    pub max_buildings: i32,
17    pub max_users: i32,
18    pub is_active: bool,
19    pub created_at: DateTime<Utc>,
20}
21
22fn to_response(org: Organization) -> OrganizationResponse {
23    OrganizationResponse {
24        id: org.id.to_string(),
25        name: org.name,
26        slug: org.slug,
27        contact_email: org.contact_email,
28        contact_phone: org.contact_phone,
29        subscription_plan: org.subscription_plan.to_string(),
30        max_buildings: org.max_buildings,
31        max_users: org.max_users,
32        is_active: org.is_active,
33        created_at: org.created_at,
34    }
35}
36
37#[derive(Deserialize)]
38pub struct CreateOrganizationRequest {
39    pub name: String,
40    pub slug: String,
41    pub contact_email: String,
42    pub contact_phone: Option<String>,
43    pub subscription_plan: String,
44}
45
46#[derive(Deserialize)]
47pub struct UpdateOrganizationRequest {
48    pub name: String,
49    pub slug: String,
50    pub contact_email: String,
51    pub contact_phone: Option<String>,
52    pub subscription_plan: String,
53}
54
55/// GET /api/v1/organizations
56/// List all organizations (SuperAdmin only)
57#[get("/organizations")]
58pub async fn list_organizations(
59    state: web::Data<AppState>,
60    user: AuthenticatedUser,
61) -> impl Responder {
62    if user.role != "superadmin" {
63        return HttpResponse::Forbidden().json(serde_json::json!({
64            "error": "Only SuperAdmin can access all organizations"
65        }));
66    }
67
68    match state.organization_use_cases.list_all().await {
69        Ok(orgs) => HttpResponse::Ok().json(serde_json::json!({
70            "data": orgs.into_iter().map(to_response).collect::<Vec<_>>()
71        })),
72        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
73            "error": format!("Failed to fetch organizations: {}", e)
74        })),
75    }
76}
77
78/// POST /api/v1/organizations
79/// Create organization (SuperAdmin only)
80#[post("/organizations")]
81pub async fn create_organization(
82    state: web::Data<AppState>,
83    user: AuthenticatedUser,
84    req: web::Json<CreateOrganizationRequest>,
85) -> impl Responder {
86    if user.role != "superadmin" {
87        return HttpResponse::Forbidden().json(serde_json::json!({
88            "error": "Only SuperAdmin can create organizations"
89        }));
90    }
91
92    match state
93        .organization_use_cases
94        .create(
95            req.name.clone(),
96            req.slug.clone(),
97            req.contact_email.clone(),
98            req.contact_phone.clone(),
99            req.subscription_plan.clone(),
100        )
101        .await
102    {
103        Ok(org) => HttpResponse::Created().json(to_response(org)),
104        Err(e) if e == "invalid_plan" => HttpResponse::BadRequest().json(serde_json::json!({
105            "error": "Invalid subscription plan"
106        })),
107        Err(e) if e.starts_with("validation_error:") => {
108            HttpResponse::BadRequest().json(serde_json::json!({
109                "error": e.trim_start_matches("validation_error:")
110            }))
111        }
112        Err(e) if e.contains("unique") || e.contains("duplicate") => HttpResponse::BadRequest()
113            .json(serde_json::json!({
114                "error": "Slug already exists"
115            })),
116        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
117            "error": format!("Failed to create organization: {}", e)
118        })),
119    }
120}
121
122/// PUT /api/v1/organizations/{id}
123/// Update organization (SuperAdmin only)
124#[put("/organizations/{id}")]
125pub async fn update_organization(
126    state: web::Data<AppState>,
127    user: AuthenticatedUser,
128    path: web::Path<Uuid>,
129    req: web::Json<UpdateOrganizationRequest>,
130) -> impl Responder {
131    if user.role != "superadmin" {
132        return HttpResponse::Forbidden().json(serde_json::json!({
133            "error": "Only SuperAdmin can update organizations"
134        }));
135    }
136
137    let org_id = path.into_inner();
138
139    match state
140        .organization_use_cases
141        .update(
142            org_id,
143            req.name.clone(),
144            req.slug.clone(),
145            req.contact_email.clone(),
146            req.contact_phone.clone(),
147            req.subscription_plan.clone(),
148        )
149        .await
150    {
151        Ok(org) => HttpResponse::Ok().json(to_response(org)),
152        Err(e) if e == "not_found" => HttpResponse::NotFound().json(serde_json::json!({
153            "error": "Organization not found"
154        })),
155        Err(e) if e == "invalid_plan" => HttpResponse::BadRequest().json(serde_json::json!({
156            "error": "Invalid subscription plan"
157        })),
158        Err(e) if e.starts_with("validation_error:") => {
159            HttpResponse::BadRequest().json(serde_json::json!({
160                "error": e.trim_start_matches("validation_error:")
161            }))
162        }
163        Err(e) if e.contains("unique") || e.contains("duplicate") => HttpResponse::BadRequest()
164            .json(serde_json::json!({
165                "error": "Slug already exists"
166            })),
167        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
168            "error": format!("Failed to update organization: {}", e)
169        })),
170    }
171}
172
173/// PUT /api/v1/organizations/{id}/activate
174/// Activate organization (SuperAdmin only)
175#[put("/organizations/{id}/activate")]
176pub async fn activate_organization(
177    state: web::Data<AppState>,
178    user: AuthenticatedUser,
179    path: web::Path<Uuid>,
180) -> impl Responder {
181    if user.role != "superadmin" {
182        return HttpResponse::Forbidden().json(serde_json::json!({
183            "error": "Only SuperAdmin can activate organizations"
184        }));
185    }
186
187    let org_id = path.into_inner();
188
189    match state.organization_use_cases.activate(org_id).await {
190        Ok(org) => HttpResponse::Ok().json(to_response(org)),
191        Err(e) if e == "not_found" => HttpResponse::NotFound().json(serde_json::json!({
192            "error": "Organization not found"
193        })),
194        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
195            "error": format!("Failed to activate organization: {}", e)
196        })),
197    }
198}
199
200/// PUT /api/v1/organizations/{id}/suspend
201/// Suspend organization (SuperAdmin only)
202#[put("/organizations/{id}/suspend")]
203pub async fn suspend_organization(
204    state: web::Data<AppState>,
205    user: AuthenticatedUser,
206    path: web::Path<Uuid>,
207) -> impl Responder {
208    if user.role != "superadmin" {
209        return HttpResponse::Forbidden().json(serde_json::json!({
210            "error": "Only SuperAdmin can suspend organizations"
211        }));
212    }
213
214    let org_id = path.into_inner();
215
216    match state.organization_use_cases.suspend(org_id).await {
217        Ok(org) => HttpResponse::Ok().json(to_response(org)),
218        Err(e) if e == "not_found" => HttpResponse::NotFound().json(serde_json::json!({
219            "error": "Organization not found"
220        })),
221        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
222            "error": format!("Failed to suspend organization: {}", e)
223        })),
224    }
225}
226
227/// DELETE /api/v1/organizations/{id}
228/// Delete organization (SuperAdmin only)
229#[delete("/organizations/{id}")]
230pub async fn delete_organization(
231    state: web::Data<AppState>,
232    user: AuthenticatedUser,
233    path: web::Path<Uuid>,
234) -> impl Responder {
235    if user.role != "superadmin" {
236        return HttpResponse::Forbidden().json(serde_json::json!({
237            "error": "Only SuperAdmin can delete organizations"
238        }));
239    }
240
241    let org_id = path.into_inner();
242
243    match state.organization_use_cases.delete(org_id).await {
244        Ok(true) => HttpResponse::Ok().json(serde_json::json!({
245            "message": "Organization deleted successfully"
246        })),
247        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
248            "error": "Organization not found"
249        })),
250        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
251            "error": format!("Failed to delete organization: {}", e)
252        })),
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_to_response_maps_all_fields() {
262        let org = Organization {
263            id: Uuid::new_v4(),
264            name: "Test Org".to_string(),
265            slug: "test-org".to_string(),
266            contact_email: "admin@test.com".to_string(),
267            contact_phone: Some("+32123456789".to_string()),
268            subscription_plan: crate::domain::entities::SubscriptionPlan::Professional,
269            max_buildings: 20,
270            max_users: 50,
271            is_active: true,
272            created_at: Utc::now(),
273            updated_at: Utc::now(),
274        };
275        let resp = to_response(org);
276        assert_eq!(resp.name, "Test Org");
277        assert_eq!(resp.slug, "test-org");
278        assert_eq!(resp.subscription_plan, "professional");
279        assert_eq!(resp.max_buildings, 20);
280    }
281
282    #[test]
283    fn test_to_response_inactive_org() {
284        let org = Organization {
285            id: Uuid::new_v4(),
286            name: "Inactive".to_string(),
287            slug: "inactive".to_string(),
288            contact_email: "x@x.com".to_string(),
289            contact_phone: None,
290            subscription_plan: crate::domain::entities::SubscriptionPlan::Free,
291            max_buildings: 1,
292            max_users: 3,
293            is_active: false,
294            created_at: Utc::now(),
295            updated_at: Utc::now(),
296        };
297        let resp = to_response(org);
298        assert!(!resp.is_active);
299        assert!(resp.contact_phone.is_none());
300    }
301}