1use crate::infrastructure::web::{AppState, AuthenticatedUser};
2use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7#[derive(Serialize)]
8pub struct OrganizationResponse {
9 pub id: String,
10 pub name: String,
11 pub slug: String,
12 pub contact_email: String,
13 pub contact_phone: Option<String>,
14 pub subscription_plan: String,
15 pub max_buildings: i32,
16 pub max_users: i32,
17 pub is_active: bool,
18 pub created_at: DateTime<Utc>,
19}
20
21#[get("/organizations")]
23pub async fn list_organizations(
24 state: web::Data<AppState>,
25 user: AuthenticatedUser,
26) -> impl Responder {
27 if user.role != "superadmin" {
29 return HttpResponse::Forbidden().json(serde_json::json!({
30 "error": "Only SuperAdmin can access all organizations"
31 }));
32 }
33
34 let result = sqlx::query!(
35 r#"
36 SELECT id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at
37 FROM organizations
38 ORDER BY created_at DESC
39 "#
40 )
41 .fetch_all(&state.pool)
42 .await;
43
44 match result {
45 Ok(rows) => {
46 let organizations: Vec<OrganizationResponse> = rows
47 .into_iter()
48 .map(|row| OrganizationResponse {
49 id: row.id.to_string(),
50 name: row.name,
51 slug: row.slug,
52 contact_email: row.contact_email,
53 contact_phone: row.contact_phone,
54 subscription_plan: row.subscription_plan,
55 max_buildings: row.max_buildings,
56 max_users: row.max_users,
57 is_active: row.is_active,
58 created_at: row.created_at,
59 })
60 .collect();
61
62 HttpResponse::Ok().json(serde_json::json!({
63 "data": organizations
64 }))
65 }
66 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
67 "error": format!("Failed to fetch organizations: {}", e)
68 })),
69 }
70}
71
72#[derive(Deserialize)]
73pub struct CreateOrganizationRequest {
74 pub name: String,
75 pub slug: String,
76 pub contact_email: String,
77 pub contact_phone: Option<String>,
78 pub subscription_plan: String,
79}
80
81#[derive(Deserialize)]
82pub struct UpdateOrganizationRequest {
83 pub name: String,
84 pub slug: String,
85 pub contact_email: String,
86 pub contact_phone: Option<String>,
87 pub subscription_plan: String,
88}
89
90#[post("/organizations")]
92pub async fn create_organization(
93 state: web::Data<AppState>,
94 user: AuthenticatedUser,
95 req: web::Json<CreateOrganizationRequest>,
96) -> impl Responder {
97 if user.role != "superadmin" {
99 return HttpResponse::Forbidden().json(serde_json::json!({
100 "error": "Only SuperAdmin can create organizations"
101 }));
102 }
103
104 let valid_plans = ["free", "starter", "professional", "enterprise"];
106 if !valid_plans.contains(&req.subscription_plan.to_lowercase().as_str()) {
107 return HttpResponse::BadRequest().json(serde_json::json!({
108 "error": "Invalid subscription plan"
109 }));
110 }
111
112 if !req.contact_email.contains('@') {
114 return HttpResponse::BadRequest().json(serde_json::json!({
115 "error": "Invalid email format"
116 }));
117 }
118
119 if req.name.trim().len() < 2 || req.slug.trim().len() < 2 {
121 return HttpResponse::BadRequest().json(serde_json::json!({
122 "error": "Name and slug must be at least 2 characters"
123 }));
124 }
125
126 if !req
128 .slug
129 .chars()
130 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
131 {
132 return HttpResponse::BadRequest().json(serde_json::json!({
133 "error": "Slug must contain only lowercase letters, numbers, and hyphens"
134 }));
135 }
136
137 let (max_buildings, max_users) = match req.subscription_plan.to_lowercase().as_str() {
139 "free" => (1, 3),
140 "starter" => (5, 10),
141 "professional" => (20, 50),
142 "enterprise" => (999, 999),
143 _ => (1, 3), };
145
146 let org_id = Uuid::new_v4();
148
149 let result = sqlx::query!(
150 r#"
151 INSERT INTO organizations (id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at, updated_at)
152 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true, NOW(), NOW())
153 RETURNING id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at
154 "#,
155 org_id,
156 req.name.trim(),
157 req.slug.trim().to_lowercase(),
158 req.contact_email.trim().to_lowercase(),
159 req.contact_phone.as_ref().map(|s| s.trim()),
160 req.subscription_plan.to_lowercase(),
161 max_buildings,
162 max_users
163 )
164 .fetch_one(&state.pool)
165 .await;
166
167 match result {
168 Ok(row) => HttpResponse::Created().json(OrganizationResponse {
169 id: row.id.to_string(),
170 name: row.name,
171 slug: row.slug,
172 contact_email: row.contact_email,
173 contact_phone: row.contact_phone,
174 subscription_plan: row.subscription_plan,
175 max_buildings: row.max_buildings,
176 max_users: row.max_users,
177 is_active: row.is_active,
178 created_at: row.created_at,
179 }),
180 Err(sqlx::Error::Database(db_err)) => {
181 if db_err.is_unique_violation() {
182 HttpResponse::BadRequest().json(serde_json::json!({
183 "error": "Slug already exists"
184 }))
185 } else {
186 HttpResponse::InternalServerError().json(serde_json::json!({
187 "error": format!("Failed to create organization: {}", db_err)
188 }))
189 }
190 }
191 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
192 "error": format!("Failed to create organization: {}", e)
193 })),
194 }
195}
196
197#[put("/organizations/{id}")]
199pub async fn update_organization(
200 state: web::Data<AppState>,
201 user: AuthenticatedUser,
202 path: web::Path<Uuid>,
203 req: web::Json<UpdateOrganizationRequest>,
204) -> impl Responder {
205 if user.role != "superadmin" {
207 return HttpResponse::Forbidden().json(serde_json::json!({
208 "error": "Only SuperAdmin can update organizations"
209 }));
210 }
211
212 let org_id = path.into_inner();
213
214 let valid_plans = ["free", "starter", "professional", "enterprise"];
216 if !valid_plans.contains(&req.subscription_plan.to_lowercase().as_str()) {
217 return HttpResponse::BadRequest().json(serde_json::json!({
218 "error": "Invalid subscription plan"
219 }));
220 }
221
222 if !req.contact_email.contains('@') {
224 return HttpResponse::BadRequest().json(serde_json::json!({
225 "error": "Invalid email format"
226 }));
227 }
228
229 if req.name.trim().len() < 2 || req.slug.trim().len() < 2 {
231 return HttpResponse::BadRequest().json(serde_json::json!({
232 "error": "Name and slug must be at least 2 characters"
233 }));
234 }
235
236 if !req
238 .slug
239 .chars()
240 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
241 {
242 return HttpResponse::BadRequest().json(serde_json::json!({
243 "error": "Slug must contain only lowercase letters, numbers, and hyphens"
244 }));
245 }
246
247 let (max_buildings, max_users) = match req.subscription_plan.to_lowercase().as_str() {
249 "free" => (1, 3),
250 "starter" => (5, 10),
251 "professional" => (20, 50),
252 "enterprise" => (999, 999),
253 _ => (1, 3),
254 };
255
256 let result = sqlx::query!(
257 r#"
258 UPDATE organizations
259 SET name = $1, slug = $2, contact_email = $3, contact_phone = $4, subscription_plan = $5, max_buildings = $6, max_users = $7, updated_at = NOW()
260 WHERE id = $8
261 RETURNING id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at
262 "#,
263 req.name.trim(),
264 req.slug.trim().to_lowercase(),
265 req.contact_email.trim().to_lowercase(),
266 req.contact_phone.as_ref().map(|s| s.trim()),
267 req.subscription_plan.to_lowercase(),
268 max_buildings,
269 max_users,
270 org_id
271 )
272 .fetch_one(&state.pool)
273 .await;
274
275 match result {
276 Ok(row) => HttpResponse::Ok().json(OrganizationResponse {
277 id: row.id.to_string(),
278 name: row.name,
279 slug: row.slug,
280 contact_email: row.contact_email,
281 contact_phone: row.contact_phone,
282 subscription_plan: row.subscription_plan,
283 max_buildings: row.max_buildings,
284 max_users: row.max_users,
285 is_active: row.is_active,
286 created_at: row.created_at,
287 }),
288 Err(sqlx::Error::RowNotFound) => HttpResponse::NotFound().json(serde_json::json!({
289 "error": "Organization not found"
290 })),
291 Err(sqlx::Error::Database(db_err)) => {
292 if db_err.is_unique_violation() {
293 HttpResponse::BadRequest().json(serde_json::json!({
294 "error": "Slug already exists"
295 }))
296 } else {
297 HttpResponse::InternalServerError().json(serde_json::json!({
298 "error": format!("Failed to update organization: {}", db_err)
299 }))
300 }
301 }
302 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
303 "error": format!("Failed to update organization: {}", e)
304 })),
305 }
306}
307
308#[put("/organizations/{id}/activate")]
310pub async fn activate_organization(
311 state: web::Data<AppState>,
312 user: AuthenticatedUser,
313 path: web::Path<Uuid>,
314) -> impl Responder {
315 if user.role != "superadmin" {
317 return HttpResponse::Forbidden().json(serde_json::json!({
318 "error": "Only SuperAdmin can activate organizations"
319 }));
320 }
321
322 let org_id = path.into_inner();
323
324 let result = sqlx::query!(
325 r#"
326 UPDATE organizations
327 SET is_active = true, updated_at = NOW()
328 WHERE id = $1
329 RETURNING id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at
330 "#,
331 org_id
332 )
333 .fetch_one(&state.pool)
334 .await;
335
336 match result {
337 Ok(row) => HttpResponse::Ok().json(OrganizationResponse {
338 id: row.id.to_string(),
339 name: row.name,
340 slug: row.slug,
341 contact_email: row.contact_email,
342 contact_phone: row.contact_phone,
343 subscription_plan: row.subscription_plan,
344 max_buildings: row.max_buildings,
345 max_users: row.max_users,
346 is_active: row.is_active,
347 created_at: row.created_at,
348 }),
349 Err(sqlx::Error::RowNotFound) => HttpResponse::NotFound().json(serde_json::json!({
350 "error": "Organization not found"
351 })),
352 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
353 "error": format!("Failed to activate organization: {}", e)
354 })),
355 }
356}
357
358#[put("/organizations/{id}/suspend")]
360pub async fn suspend_organization(
361 state: web::Data<AppState>,
362 user: AuthenticatedUser,
363 path: web::Path<Uuid>,
364) -> impl Responder {
365 if user.role != "superadmin" {
367 return HttpResponse::Forbidden().json(serde_json::json!({
368 "error": "Only SuperAdmin can suspend organizations"
369 }));
370 }
371
372 let org_id = path.into_inner();
373
374 let result = sqlx::query!(
375 r#"
376 UPDATE organizations
377 SET is_active = false, updated_at = NOW()
378 WHERE id = $1
379 RETURNING id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at
380 "#,
381 org_id
382 )
383 .fetch_one(&state.pool)
384 .await;
385
386 match result {
387 Ok(row) => HttpResponse::Ok().json(OrganizationResponse {
388 id: row.id.to_string(),
389 name: row.name,
390 slug: row.slug,
391 contact_email: row.contact_email,
392 contact_phone: row.contact_phone,
393 subscription_plan: row.subscription_plan,
394 max_buildings: row.max_buildings,
395 max_users: row.max_users,
396 is_active: row.is_active,
397 created_at: row.created_at,
398 }),
399 Err(sqlx::Error::RowNotFound) => HttpResponse::NotFound().json(serde_json::json!({
400 "error": "Organization not found"
401 })),
402 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
403 "error": format!("Failed to suspend organization: {}", e)
404 })),
405 }
406}
407
408#[delete("/organizations/{id}")]
410pub async fn delete_organization(
411 state: web::Data<AppState>,
412 user: AuthenticatedUser,
413 path: web::Path<Uuid>,
414) -> impl Responder {
415 if user.role != "superadmin" {
417 return HttpResponse::Forbidden().json(serde_json::json!({
418 "error": "Only SuperAdmin can delete organizations"
419 }));
420 }
421
422 let org_id = path.into_inner();
423
424 let result = sqlx::query!(
425 r#"
426 DELETE FROM organizations
427 WHERE id = $1
428 "#,
429 org_id
430 )
431 .execute(&state.pool)
432 .await;
433
434 match result {
435 Ok(result) => {
436 if result.rows_affected() == 0 {
437 HttpResponse::NotFound().json(serde_json::json!({
438 "error": "Organization not found"
439 }))
440 } else {
441 HttpResponse::Ok().json(serde_json::json!({
442 "message": "Organization deleted successfully"
443 }))
444 }
445 }
446 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
447 "error": format!("Failed to delete organization: {}", e)
448 })),
449 }
450}