1use crate::domain::entities::{User, UserRole};
2use bcrypt::{hash, DEFAULT_COST};
3use chrono::Utc;
4use fake::faker::address::en::*;
5use fake::faker::name::en::*;
6use fake::Fake;
7use rand::Rng;
8use sqlx::{PgPool, Row};
9use uuid::Uuid;
10
11pub struct DatabaseSeeder {
12 pool: PgPool,
13}
14
15impl DatabaseSeeder {
16 pub fn new(pool: PgPool) -> Self {
17 Self { pool }
18 }
19
20 pub async fn seed_superadmin(&self) -> Result<User, String> {
22 let superadmin_email = "admin@koprogo.com";
23 let superadmin_password = "admin123"; let password_hash = hash(superadmin_password, DEFAULT_COST)
27 .map_err(|e| format!("Failed to hash password: {}", e))?;
28
29 let superadmin_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001")
30 .map_err(|e| format!("Failed to parse UUID: {}", e))?;
31
32 let now = Utc::now();
33
34 sqlx::query!(
36 r#"
37 INSERT INTO users (id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at)
38 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
39 ON CONFLICT (email)
40 DO UPDATE SET
41 password_hash = EXCLUDED.password_hash,
42 updated_at = EXCLUDED.updated_at,
43 is_active = true
44 "#,
45 superadmin_id,
46 superadmin_email,
47 password_hash,
48 "Super",
49 "Admin",
50 "superadmin",
51 None::<Uuid>,
52 true,
53 now,
54 now
55 )
56 .execute(&self.pool)
57 .await
58 .map_err(|e| format!("Failed to upsert superadmin: {}", e))?;
59
60 log::info!("✅ Superadmin ready: {}", superadmin_email);
61
62 Ok(User {
63 id: superadmin_id,
64 email: superadmin_email.to_string(),
65 password_hash,
66 first_name: "Super".to_string(),
67 last_name: "Admin".to_string(),
68 role: UserRole::SuperAdmin,
69 organization_id: None,
70 is_active: true,
71 created_at: now,
72 updated_at: now,
73 })
74 }
75
76 pub async fn seed_demo_data(&self) -> Result<String, String> {
78 log::info!("🌱 Starting demo data seeding...");
79
80 let existing_orgs = sqlx::query!("SELECT COUNT(*) as count FROM organizations")
82 .fetch_one(&self.pool)
83 .await
84 .map_err(|e| format!("Failed to count organizations: {}", e))?;
85
86 if existing_orgs.count.unwrap_or(0) > 0 {
87 return Err("Demo data already exists. Please clean the database first.".to_string());
88 }
89
90 let org1_id = Uuid::new_v4();
92 let now = Utc::now();
93
94 sqlx::query!(
95 r#"
96 INSERT INTO organizations (id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at, updated_at)
97 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
98 "#,
99 org1_id,
100 "Résidence Grand Place SPRL",
101 "residence-grand-place",
102 "contact@grandplace.be",
103 "+32 2 501 23 45",
104 "professional",
105 20,
106 50,
107 true,
108 now,
109 now
110 )
111 .execute(&self.pool)
112 .await
113 .map_err(|e| format!("Failed to create demo organization 1: {}", e))?;
114
115 log::info!("✅ Organization 1 created: Résidence Grand Place SPRL");
116
117 let syndic1_id = self
119 .create_demo_user(
120 "syndic@grandplace.be",
121 "syndic123",
122 "Jean",
123 "Dupont",
124 "syndic",
125 Some(org1_id),
126 )
127 .await?;
128
129 let _accountant_id = self
130 .create_demo_user(
131 "comptable@grandplace.be",
132 "comptable123",
133 "Marie",
134 "Martin",
135 "accountant",
136 Some(org1_id),
137 )
138 .await?;
139
140 let _owner1_id = self
141 .create_demo_user(
142 "proprietaire1@grandplace.be",
143 "owner123",
144 "Pierre",
145 "Durand",
146 "owner",
147 Some(org1_id),
148 )
149 .await?;
150
151 let _owner2_id = self
152 .create_demo_user(
153 "proprietaire2@grandplace.be",
154 "owner123",
155 "Sophie",
156 "Bernard",
157 "owner",
158 Some(org1_id),
159 )
160 .await?;
161
162 log::info!("✅ Demo users created");
163
164 let building1_id = self
166 .create_demo_building(
167 org1_id,
168 "Résidence Grand Place",
169 "Grand Place 15",
170 "Bruxelles",
171 "1000",
172 "Belgique",
173 15,
174 1995,
175 )
176 .await?;
177
178 let building2_id = self
179 .create_demo_building(
180 org1_id,
181 "Les Jardins d'Ixelles",
182 "Rue du Trône 85",
183 "Bruxelles",
184 "1050",
185 "Belgique",
186 8,
187 2010,
188 )
189 .await?;
190
191 log::info!("✅ Demo buildings created");
192
193 let owner1_db_id = self
195 .create_demo_owner(
196 org1_id,
197 "Pierre",
198 "Durand",
199 "pierre.durand@email.be",
200 "+32 476 12 34 56",
201 "Avenue Louise 15",
202 "Bruxelles",
203 "1050",
204 "Belgique",
205 )
206 .await?;
207
208 let owner2_db_id = self
209 .create_demo_owner(
210 org1_id,
211 "Sophie",
212 "Bernard",
213 "sophie.bernard@email.be",
214 "+32 495 98 76 54",
215 "Rue Royale 28",
216 "Bruxelles",
217 "1000",
218 "Belgique",
219 )
220 .await?;
221
222 let owner3_db_id = self
223 .create_demo_owner(
224 org1_id,
225 "Michel",
226 "Lefebvre",
227 "michel.lefebvre@email.be",
228 "+32 477 11 22 33",
229 "Boulevard d'Avroy 42",
230 "Liège",
231 "4000",
232 "Belgique",
233 )
234 .await?;
235
236 log::info!("✅ Demo owners created");
237
238 let _unit1_id = self
240 .create_demo_unit(
241 org1_id,
242 building1_id,
243 Some(owner1_db_id),
244 "101",
245 "apartment",
246 Some(1),
247 75.5,
248 250.0,
249 )
250 .await?;
251
252 let _unit2_id = self
253 .create_demo_unit(
254 org1_id,
255 building1_id,
256 Some(owner2_db_id),
257 "102",
258 "apartment",
259 Some(1),
260 62.0,
261 200.0,
262 )
263 .await?;
264
265 let _unit3_id = self
266 .create_demo_unit(
267 org1_id,
268 building1_id,
269 None,
270 "103",
271 "apartment",
272 Some(1),
273 85.0,
274 300.0,
275 )
276 .await?;
277
278 let _unit4_id = self
279 .create_demo_unit(
280 org1_id,
281 building2_id,
282 Some(owner3_db_id),
283 "201",
284 "apartment",
285 Some(2),
286 95.0,
287 350.0,
288 )
289 .await?;
290
291 log::info!("✅ Demo units created");
292
293 self.create_demo_expense(
295 building1_id,
296 org1_id,
297 "Charges de copropriété Q1 2025 - Charges trimestrielles incluant eau, chauffage, entretien",
298 5000.0,
299 "2025-01-15",
300 "administration",
301 "pending",
302 Some("Syndic Services"),
303 Some("INV-2025-001"),
304 )
305 .await?;
306
307 self.create_demo_expense(
308 building1_id,
309 org1_id,
310 "Réparation ascenseur - Maintenance et réparation de l'ascenseur principal",
311 2500.0,
312 "2025-02-10",
313 "maintenance",
314 "paid",
315 Some("Ascenseurs Plus"),
316 Some("ASC-2025-023"),
317 )
318 .await?;
319
320 self.create_demo_expense(
321 building2_id,
322 org1_id,
323 "Charges de copropriété Q1 2025 - Charges trimestrielles",
324 3000.0,
325 "2025-01-15",
326 "administration",
327 "pending",
328 Some("Syndic Services"),
329 Some("INV-2025-002"),
330 )
331 .await?;
332
333 self.create_demo_expense(
334 building2_id,
335 org1_id,
336 "Nettoyage des parties communes - Contrat annuel de nettoyage",
337 1200.0,
338 "2025-01-01",
339 "cleaning",
340 "paid",
341 Some("CleanPro"),
342 Some("CLN-2025-156"),
343 )
344 .await?;
345
346 log::info!("✅ Demo expenses created");
347
348 self.create_demo_meeting(
350 building1_id,
351 org1_id,
352 "Assemblée Générale Ordinaire 2025",
353 "ordinary",
354 "2025-03-15",
355 "scheduled",
356 )
357 .await?;
358
359 self.create_demo_meeting(
360 building2_id,
361 org1_id,
362 "Assemblée Générale Extraordinaire - Travaux",
363 "extraordinary",
364 "2025-04-20",
365 "scheduled",
366 )
367 .await?;
368
369 log::info!("✅ Demo meetings created");
370
371 self.create_demo_document(
373 building1_id,
374 org1_id,
375 "Procès-Verbal AG 2024",
376 "meeting_minutes",
377 "/uploads/demo/pv-ag-2024.pdf",
378 syndic1_id,
379 )
380 .await?;
381
382 self.create_demo_document(
383 building1_id,
384 org1_id,
385 "Règlement de copropriété",
386 "regulation",
387 "/uploads/demo/reglement.pdf",
388 syndic1_id,
389 )
390 .await?;
391
392 log::info!("✅ Demo documents created");
393
394 let org2_id = Uuid::new_v4();
396 sqlx::query!(
397 r#"
398 INSERT INTO organizations (id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at, updated_at)
399 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
400 "#,
401 org2_id,
402 "Copropriété Bruxelles SPRL",
403 "copro-bruxelles",
404 "info@copro-bruxelles.be",
405 "+32 2 123 45 67",
406 "starter",
407 5,
408 10,
409 true,
410 now,
411 now
412 )
413 .execute(&self.pool)
414 .await
415 .map_err(|e| format!("Failed to create demo organization 2: {}", e))?;
416
417 let _syndic2_id = self
418 .create_demo_user(
419 "syndic@copro-bruxelles.be",
420 "syndic123",
421 "Marc",
422 "Dubois",
423 "syndic",
424 Some(org2_id),
425 )
426 .await?;
427
428 let building3_id = self
429 .create_demo_building(
430 org2_id,
431 "Résidence Européenne",
432 "Avenue Louise 123",
433 "Bruxelles",
434 "1050",
435 "Belgique",
436 12,
437 2005,
438 )
439 .await?;
440
441 self.create_demo_meeting(
442 building3_id,
443 org2_id,
444 "AG Annuelle 2025",
445 "ordinary",
446 "2025-05-10",
447 "scheduled",
448 )
449 .await?;
450
451 log::info!("✅ Organization 2 created");
452
453 let org3_id = Uuid::new_v4();
455 sqlx::query!(
456 r#"
457 INSERT INTO organizations (id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at, updated_at)
458 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
459 "#,
460 org3_id,
461 "Syndic Liège SA",
462 "syndic-liege",
463 "contact@syndic-liege.be",
464 "+32 4 222 33 44",
465 "enterprise",
466 50,
467 100,
468 true,
469 now,
470 now
471 )
472 .execute(&self.pool)
473 .await
474 .map_err(|e| format!("Failed to create demo organization 3: {}", e))?;
475
476 let _syndic3_id = self
477 .create_demo_user(
478 "syndic@syndic-liege.be",
479 "syndic123",
480 "Sophie",
481 "Lambert",
482 "syndic",
483 Some(org3_id),
484 )
485 .await?;
486
487 let _building4_id = self
488 .create_demo_building(
489 org3_id,
490 "Les Terrasses de Liège",
491 "Boulevard de la Sauvenière 45",
492 "Liège",
493 "4000",
494 "Belgique",
495 8,
496 2018,
497 )
498 .await?;
499
500 log::info!("✅ Organization 3 created");
501
502 Ok("✅ Demo data seeded successfully!\n\n\
503 📊 Summary:\n\
504 - 3 Organizations: Grand Place (Bruxelles), Bruxelles Louise, Liège\n\
505 - 6+ Users: 3 Syndics, 1 Accountant, 2+ Owners\n\
506 - 4 Buildings across Belgium\n\
507 - 3 Owners (database records)\n\
508 - 4 Units\n\
509 - 4 Expenses\n\
510 - 3 Meetings\n\
511 - 2 Documents\n\n\
512 🇧🇪 Belgian Demo - Credentials:\n\
513 - Org 1 (Grand Place): syndic@grandplace.be / syndic123\n\
514 - Org 2 (Bruxelles): syndic@copro-bruxelles.be / syndic123\n\
515 - Org 3 (Liège): syndic@syndic-liege.be / syndic123\n\
516 - SuperAdmin: admin@koprogo.com / admin123"
517 .to_string())
518 }
519
520 async fn create_demo_user(
521 &self,
522 email: &str,
523 password: &str,
524 first_name: &str,
525 last_name: &str,
526 role: &str,
527 organization_id: Option<Uuid>,
528 ) -> Result<Uuid, String> {
529 let password_hash =
530 hash(password, DEFAULT_COST).map_err(|e| format!("Failed to hash password: {}", e))?;
531
532 let user_id = Uuid::new_v4();
533 let now = Utc::now();
534
535 sqlx::query!(
536 r#"
537 INSERT INTO users (id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at)
538 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
539 "#,
540 user_id,
541 email,
542 password_hash,
543 first_name,
544 last_name,
545 role,
546 organization_id,
547 true,
548 now,
549 now
550 )
551 .execute(&self.pool)
552 .await
553 .map_err(|e| format!("Failed to create user {}: {}", email, e))?;
554
555 Ok(user_id)
556 }
557
558 #[allow(clippy::too_many_arguments)]
559 async fn create_demo_building(
560 &self,
561 org_id: Uuid,
562 name: &str,
563 address: &str,
564 city: &str,
565 postal_code: &str,
566 country: &str,
567 total_units: i32,
568 construction_year: i32,
569 ) -> Result<Uuid, String> {
570 let building_id = Uuid::new_v4();
571 let now = Utc::now();
572
573 sqlx::query!(
574 r#"
575 INSERT INTO buildings (id, organization_id, name, address, city, postal_code, country, total_units, construction_year, created_at, updated_at)
576 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
577 "#,
578 building_id,
579 org_id,
580 name,
581 address,
582 city,
583 postal_code,
584 country,
585 total_units,
586 construction_year,
587 now,
588 now
589 )
590 .execute(&self.pool)
591 .await
592 .map_err(|e| format!("Failed to create building {}: {}", name, e))?;
593
594 Ok(building_id)
595 }
596
597 #[allow(clippy::too_many_arguments)]
598 async fn create_demo_owner(
599 &self,
600 organization_id: Uuid,
601 first_name: &str,
602 last_name: &str,
603 email: &str,
604 phone: &str,
605 address: &str,
606 city: &str,
607 postal_code: &str,
608 country: &str,
609 ) -> Result<Uuid, String> {
610 let owner_id = Uuid::new_v4();
611 let now = Utc::now();
612
613 sqlx::query!(
614 r#"
615 INSERT INTO owners (id, organization_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at)
616 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
617 "#,
618 owner_id,
619 organization_id,
620 first_name,
621 last_name,
622 email,
623 phone,
624 address,
625 city,
626 postal_code,
627 country,
628 now,
629 now
630 )
631 .execute(&self.pool)
632 .await
633 .map_err(|e| format!("Failed to create owner {} {}: {}", first_name, last_name, e))?;
634
635 Ok(owner_id)
636 }
637
638 #[allow(clippy::too_many_arguments)]
639 async fn create_demo_unit(
640 &self,
641 organization_id: Uuid,
642 building_id: Uuid,
643 owner_id: Option<Uuid>,
644 unit_number: &str,
645 unit_type: &str,
646 floor: Option<i32>,
647 surface_area: f64,
648 quota: f64,
649 ) -> Result<Uuid, String> {
650 let unit_id = Uuid::new_v4();
651 let now = Utc::now();
652
653 sqlx::query(
654 r#"
655 INSERT INTO units (id, organization_id, building_id, owner_id, unit_number, unit_type, floor, surface_area, quota, created_at, updated_at)
656 VALUES ($1, $2, $3, $4, $5, $6::unit_type, $7, $8, $9, $10, $11)
657 "#
658 )
659 .bind(unit_id)
660 .bind(organization_id)
661 .bind(building_id)
662 .bind(owner_id)
663 .bind(unit_number)
664 .bind(unit_type)
665 .bind(floor)
666 .bind(surface_area)
667 .bind(quota)
668 .bind(now)
669 .bind(now)
670 .execute(&self.pool)
671 .await
672 .map_err(|e| format!("Failed to create unit {}: {}", unit_number, e))?;
673
674 Ok(unit_id)
675 }
676
677 #[allow(clippy::too_many_arguments)]
678 async fn create_demo_expense(
679 &self,
680 building_id: Uuid,
681 organization_id: Uuid,
682 description: &str,
683 amount: f64,
684 expense_date: &str,
685 category: &str,
686 payment_status: &str,
687 supplier: Option<&str>,
688 invoice_number: Option<&str>,
689 ) -> Result<Uuid, String> {
690 let expense_id = Uuid::new_v4();
691 let now = Utc::now();
692 let expense_date_parsed =
693 chrono::DateTime::parse_from_rfc3339(&format!("{}T00:00:00Z", expense_date))
694 .map_err(|e| format!("Failed to parse date: {}", e))?
695 .with_timezone(&Utc);
696
697 sqlx::query(
698 r#"
699 INSERT INTO expenses (id, organization_id, building_id, category, description, amount, expense_date, payment_status, supplier, invoice_number, created_at, updated_at)
700 VALUES ($1, $2, $3, $4::expense_category, $5, $6, $7, $8::payment_status, $9, $10, $11, $12)
701 "#
702 )
703 .bind(expense_id)
704 .bind(organization_id)
705 .bind(building_id)
706 .bind(category)
707 .bind(description)
708 .bind(amount)
709 .bind(expense_date_parsed)
710 .bind(payment_status)
711 .bind(supplier)
712 .bind(invoice_number)
713 .bind(now)
714 .bind(now)
715 .execute(&self.pool)
716 .await
717 .map_err(|e| format!("Failed to create expense: {}", e))?;
718
719 Ok(expense_id)
720 }
721
722 #[allow(clippy::too_many_arguments)]
723 async fn create_demo_meeting(
724 &self,
725 building_id: Uuid,
726 org_id: Uuid,
727 title: &str,
728 meeting_type: &str,
729 scheduled_date: &str,
730 status: &str,
731 ) -> Result<Uuid, String> {
732 let meeting_id = Uuid::new_v4();
733 let now = Utc::now();
734 let scheduled_date_parsed =
735 chrono::DateTime::parse_from_rfc3339(&format!("{}T10:00:00Z", scheduled_date))
736 .map_err(|e| format!("Failed to parse date: {}", e))?
737 .with_timezone(&Utc);
738
739 let agenda_json = serde_json::json!([
740 "Approbation des comptes",
741 "Travaux à prévoir",
742 "Questions diverses"
743 ]);
744
745 sqlx::query(
746 r#"
747 INSERT INTO meetings (id, building_id, organization_id, meeting_type, title, description, scheduled_date, location, status, agenda, created_at, updated_at)
748 VALUES ($1, $2, $3, $4::meeting_type, $5, $6, $7, $8, $9::meeting_status, $10, $11, $12)
749 "#
750 )
751 .bind(meeting_id)
752 .bind(building_id)
753 .bind(org_id)
754 .bind(meeting_type)
755 .bind(title)
756 .bind(Some("Assemblée générale annuelle"))
757 .bind(scheduled_date_parsed)
758 .bind("Salle polyvalente")
759 .bind(status)
760 .bind(agenda_json)
761 .bind(now)
762 .bind(now)
763 .execute(&self.pool)
764 .await
765 .map_err(|e| format!("Failed to create meeting: {}", e))?;
766
767 Ok(meeting_id)
768 }
769
770 #[allow(clippy::too_many_arguments)]
771 async fn create_demo_document(
772 &self,
773 building_id: Uuid,
774 org_id: Uuid,
775 title: &str,
776 document_type: &str,
777 file_path: &str,
778 uploaded_by: Uuid,
779 ) -> Result<Uuid, String> {
780 let document_id = Uuid::new_v4();
781 let now = Utc::now();
782
783 sqlx::query(
784 r#"
785 INSERT INTO documents (id, building_id, organization_id, document_type, title, description, file_path, file_size, mime_type, uploaded_by, created_at, updated_at)
786 VALUES ($1, $2, $3, $4::document_type, $5, $6, $7, $8, $9, $10, $11, $12)
787 "#
788 )
789 .bind(document_id)
790 .bind(building_id)
791 .bind(org_id)
792 .bind(document_type)
793 .bind(title)
794 .bind(Some("Document de démonstration"))
795 .bind(file_path)
796 .bind(1024_i64)
797 .bind("application/pdf")
798 .bind(uploaded_by)
799 .bind(now)
800 .bind(now)
801 .execute(&self.pool)
802 .await
803 .map_err(|e| format!("Failed to create document: {}", e))?;
804
805 Ok(document_id)
806 }
807
808 pub async fn seed_realistic_data(&self) -> Result<String, String> {
811 log::info!("🌱 Starting realistic data seeding...");
812
813 let existing_orgs = sqlx::query("SELECT COUNT(*) as count FROM organizations")
815 .fetch_one(&self.pool)
816 .await
817 .map_err(|e| format!("Failed to count organizations: {}", e))?;
818
819 let count: i64 = existing_orgs
820 .try_get("count")
821 .map_err(|e| format!("Failed to get count: {}", e))?;
822 if count > 0 {
823 return Err("Data already exists. Please clear the database first.".to_string());
824 }
825
826 let mut rng = rand::rng();
827
828 let cities = [
830 "Bruxelles",
831 "Anvers",
832 "Gand",
833 "Charleroi",
834 "Liège",
835 "Bruges",
836 "Namur",
837 "Louvain",
838 ];
839 let street_types = ["Rue", "Avenue", "Boulevard", "Place", "Chaussée"];
840 let street_names = [
841 "des Fleurs",
842 "du Parc",
843 "de la Gare",
844 "Royale",
845 "de l'Église",
846 "du Commerce",
847 "de la Liberté",
848 "des Arts",
849 "Victor Hugo",
850 "Louise",
851 ];
852
853 let org_configs = [
855 ("Petite Copropriété SPRL", "small", 5, 30), ("Copropriété Moyenne SA", "medium", 8, 60), ("Grande Résidence NV", "large", 10, 100), ];
859
860 let mut total_buildings = 0;
861 let mut total_units = 0;
862 let mut total_owners = 0;
863 let mut total_expenses = 0;
864
865 for (idx, (org_name, size, num_buildings, target_units)) in org_configs.iter().enumerate() {
866 let org_id = Uuid::new_v4();
867 let now = Utc::now();
868
869 log::info!(
870 "📍 Organization {}: {} ({} buildings, ~{} units)",
871 idx + 1,
872 org_name,
873 num_buildings,
874 target_units
875 );
876
877 sqlx::query(
879 "INSERT INTO organizations (id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at, updated_at)
880 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)"
881 )
882 .bind(org_id)
883 .bind(*org_name)
884 .bind(format!("{}-{}", size, idx))
885 .bind(format!("contact@{}.be", size))
886 .bind(format!("+32 2 {} {} {}", rng.random_range(100..999), rng.random_range(10..99), rng.random_range(10..99)))
887 .bind(if *size == "large" { "enterprise" } else if *size == "medium" { "professional" } else { "starter" })
888 .bind(*num_buildings)
889 .bind(if *size == "large" { 50 } else if *size == "medium" { 20 } else { 10 })
890 .bind(true)
891 .bind(now)
892 .bind(now)
893 .execute(&self.pool)
894 .await
895 .map_err(|e| format!("Failed to create organization: {}", e))?;
896
897 let user_id = Uuid::new_v4();
899 let password_hash = hash("admin123", DEFAULT_COST)
900 .map_err(|e| format!("Failed to hash password: {}", e))?;
901
902 sqlx::query(
903 "INSERT INTO users (id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at)
904 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"
905 )
906 .bind(user_id)
907 .bind(format!("admin@{}.be", size))
908 .bind(&password_hash)
909 .bind("Admin")
910 .bind(org_name.split_whitespace().next().unwrap_or("User"))
911 .bind("syndic")
912 .bind(Some(org_id))
913 .bind(true)
914 .bind(now)
915 .bind(now)
916 .execute(&self.pool)
917 .await
918 .map_err(|e| format!("Failed to create user: {}", e))?;
919
920 let num_owners = (target_units * 2 / 3) as usize; let mut owner_ids = Vec::new();
923
924 for o in 0..num_owners {
925 let owner_id = Uuid::new_v4();
926
927 let first_name: String = FirstName().fake();
929 let last_name: String = LastName().fake();
930 let street: String = StreetName().fake();
931 let city_idx = rng.random_range(0..cities.len());
932 let owner_city = cities[city_idx];
933
934 sqlx::query(
935 "INSERT INTO owners (id, organization_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at)
936 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)"
937 )
938 .bind(owner_id)
939 .bind(org_id)
940 .bind(&first_name)
941 .bind(&last_name)
942 .bind(format!("{}. {}{}@{}.be", first_name.chars().next().unwrap_or('x'), last_name.to_lowercase(), o + 1, size))
943 .bind(format!("+32 {} {} {} {}",
944 if rng.random_bool(0.5) { "2" } else { "4" },
945 rng.random_range(100..999),
946 rng.random_range(10..99),
947 rng.random_range(10..99)
948 ))
949 .bind(format!("{} {}", street, rng.random_range(1..200)))
950 .bind(owner_city)
951 .bind(format!("{}", rng.random_range(1000..9999)))
952 .bind("Belgium")
953 .bind(now)
954 .bind(now)
955 .execute(&self.pool)
956 .await
957 .map_err(|e| format!("Failed to create owner: {}", e))?;
958
959 owner_ids.push(owner_id);
960 }
961
962 total_owners += num_owners;
963
964 let units_per_building = target_units / num_buildings;
966 let mut org_units = 0;
967
968 for b in 0..*num_buildings {
969 let building_id = Uuid::new_v4();
970 let city = cities[rng.random_range(0..cities.len())];
971 let street_type = street_types[rng.random_range(0..street_types.len())];
972 let street_name = street_names[rng.random_range(0..street_names.len())];
973 let building_name = format!("Résidence {}", street_name);
974
975 sqlx::query(
976 "INSERT INTO buildings (id, organization_id, name, address, city, postal_code, country, total_units, construction_year, created_at, updated_at)
977 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)"
978 )
979 .bind(building_id)
980 .bind(org_id)
981 .bind(&building_name)
982 .bind(format!("{} {} {}", street_type, street_name, rng.random_range(1..200)))
983 .bind(city)
984 .bind(format!("{}", rng.random_range(1000..9999)))
985 .bind("Belgium")
986 .bind(units_per_building)
987 .bind(rng.random_range(1960..2024))
988 .bind(now)
989 .bind(now)
990 .execute(&self.pool)
991 .await
992 .map_err(|e| format!("Failed to create building: {}", e))?;
993
994 let units_this_building = if b == num_buildings - 1 {
996 target_units - org_units
998 } else {
999 units_per_building
1000 };
1001
1002 for u in 0..units_this_building {
1003 let floor = u / 4; let unit_number = format!("{}.{}", floor, (u % 4) + 1);
1005
1006 let owner_id = if rng.random_bool(0.66) && !owner_ids.is_empty() {
1008 Some(owner_ids[rng.random_range(0..owner_ids.len())])
1009 } else {
1010 None
1011 };
1012
1013 let unit_types = ["apartment", "apartment", "apartment", "parking", "cellar"];
1015 let unit_type = unit_types[rng.random_range(0..unit_types.len())];
1016
1017 sqlx::query(
1018 "INSERT INTO units (id, organization_id, building_id, unit_number, unit_type, floor, surface_area, quota, owner_id, created_at, updated_at)
1019 VALUES ($1, $2, $3, $4, $5::unit_type, $6, $7, $8, $9, $10, $11)"
1020 )
1021 .bind(Uuid::new_v4())
1022 .bind(org_id)
1023 .bind(building_id)
1024 .bind(&unit_number)
1025 .bind(unit_type)
1026 .bind(floor)
1027 .bind(rng.random_range(45.0..150.0))
1028 .bind(rng.random_range(50..200) as i32)
1029 .bind(owner_id)
1030 .bind(now)
1031 .bind(now)
1032 .execute(&self.pool)
1033 .await
1034 .map_err(|e| format!("Failed to create unit: {}", e))?;
1035 }
1036
1037 org_units += units_this_building;
1038
1039 let num_expenses = rng.random_range(2..=3);
1041 let expense_types = [
1042 ("Entretien ascenseur", 450.0, 800.0),
1043 ("Nettoyage parties communes", 300.0, 600.0),
1044 ("Chauffage collectif", 1500.0, 3000.0),
1045 ("Assurance immeuble", 800.0, 1500.0),
1046 ("Travaux façade", 5000.0, 15000.0),
1047 ];
1048
1049 for _ in 0..num_expenses {
1050 let (desc, min_amount, max_amount) =
1051 expense_types[rng.random_range(0..expense_types.len())];
1052 let amount = rng.random_range(min_amount..max_amount);
1053 let days_ago = rng.random_range(0..90);
1054 let expense_date = Utc::now() - chrono::Duration::days(days_ago);
1055
1056 let categories = [
1058 "maintenance",
1059 "repairs",
1060 "insurance",
1061 "utilities",
1062 "cleaning",
1063 "administration",
1064 "works",
1065 ];
1066 let category = categories[rng.random_range(0..categories.len())];
1067
1068 let payment_status = if rng.random_bool(0.7) {
1070 "paid"
1071 } else {
1072 "pending"
1073 };
1074
1075 sqlx::query(
1076 "INSERT INTO expenses (id, organization_id, building_id, category, description, amount, expense_date, payment_status, created_at, updated_at)
1077 VALUES ($1, $2, $3, $4::expense_category, $5, $6, $7, $8::payment_status, $9, $10)"
1078 )
1079 .bind(Uuid::new_v4())
1080 .bind(org_id)
1081 .bind(building_id)
1082 .bind(category)
1083 .bind(desc)
1084 .bind(amount)
1085 .bind(expense_date)
1086 .bind(payment_status)
1087 .bind(now)
1088 .bind(now)
1089 .execute(&self.pool)
1090 .await
1091 .map_err(|e| format!("Failed to create expense: {}", e))?;
1092
1093 total_expenses += 1;
1094 }
1095 }
1096
1097 total_buildings += num_buildings;
1098 total_units += org_units as usize;
1099
1100 log::info!(
1101 " ✅ Created {} buildings, {} units, {} owners",
1102 num_buildings,
1103 org_units,
1104 num_owners
1105 );
1106 }
1107
1108 Ok(format!(
1109 "✅ Realistic seed data created successfully!\n\
1110 Total: {} orgs, {} buildings, {} units, {} owners, {} expenses\n\
1111 \nTest credentials:\n\
1112 - Small org: admin@small.be / admin123\n\
1113 - Medium org: admin@medium.be / admin123\n\
1114 - Large org: admin@large.be / admin123",
1115 org_configs.len(),
1116 total_buildings,
1117 total_units,
1118 total_owners,
1119 total_expenses
1120 ))
1121 }
1122
1123 pub async fn clear_demo_data(&self) -> Result<String, String> {
1125 log::warn!("⚠️ Clearing all demo data...");
1126
1127 sqlx::query("DELETE FROM documents")
1129 .execute(&self.pool)
1130 .await
1131 .map_err(|e| format!("Failed to delete documents: {}", e))?;
1132
1133 sqlx::query("DELETE FROM meetings")
1134 .execute(&self.pool)
1135 .await
1136 .map_err(|e| format!("Failed to delete meetings: {}", e))?;
1137
1138 sqlx::query!("DELETE FROM expenses")
1139 .execute(&self.pool)
1140 .await
1141 .map_err(|e| format!("Failed to delete expenses: {}", e))?;
1142
1143 sqlx::query!("DELETE FROM units")
1144 .execute(&self.pool)
1145 .await
1146 .map_err(|e| format!("Failed to delete units: {}", e))?;
1147
1148 sqlx::query!("DELETE FROM owners")
1149 .execute(&self.pool)
1150 .await
1151 .map_err(|e| format!("Failed to delete owners: {}", e))?;
1152
1153 sqlx::query!("DELETE FROM buildings")
1154 .execute(&self.pool)
1155 .await
1156 .map_err(|e| format!("Failed to delete buildings: {}", e))?;
1157
1158 sqlx::query!("DELETE FROM users WHERE role != 'superadmin'")
1159 .execute(&self.pool)
1160 .await
1161 .map_err(|e| format!("Failed to delete users: {}", e))?;
1162
1163 sqlx::query!("DELETE FROM organizations")
1164 .execute(&self.pool)
1165 .await
1166 .map_err(|e| format!("Failed to delete organizations: {}", e))?;
1167
1168 log::info!("✅ Demo data cleared (superadmin preserved)");
1169
1170 Ok("✅ Demo data cleared successfully!".to_string())
1171 }
1172}