koprogo_api/infrastructure/web/handlers/
organization_handlers.rs1use 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("/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("/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("/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("/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("/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("/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}