koprogo_api/infrastructure/database/
seed.rs

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    /// Create or update the default superadmin user
21    pub async fn seed_superadmin(&self) -> Result<User, String> {
22        let superadmin_email = "admin@koprogo.com";
23        let superadmin_password = "admin123"; // Change in production!
24
25        // Hash password
26        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        // Upsert superadmin (insert or update if exists)
35        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    /// Seed demo data for production demonstration
77    pub async fn seed_demo_data(&self) -> Result<String, String> {
78        log::info!("🌱 Starting demo data seeding...");
79
80        // Check if demo data already exists
81        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        // ORGANIZATION 1
91        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        // Create demo users ORG 1
118        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        // Create demo buildings ORG 1
165        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        // Create demo owners
194        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        // Create demo units
239        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        // Create demo expenses
294        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        // Create meetings ORG 1
349        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        // Create documents ORG 1
372        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        // ORGANIZATION 2 - Bruxelles
395        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        // ORGANIZATION 3 - Liège
454        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    /// Seed realistic data for load testing (optimized for 1 vCPU / 2GB RAM)
809    /// Generates: 3 orgs, ~23 buildings, ~190 units, ~127 owners, ~60 expenses
810    pub async fn seed_realistic_data(&self) -> Result<String, String> {
811        log::info!("🌱 Starting realistic data seeding...");
812
813        // Check if data already exists
814        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        // Belgian cities for variety
829        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        // Create 3 organizations with different sizes
854        let org_configs = [
855            ("Petite Copropriété SPRL", "small", 5, 30), // 5 buildings, ~30 units
856            ("Copropriété Moyenne SA", "medium", 8, 60), // 8 buildings, ~60 units
857            ("Grande Résidence NV", "large", 10, 100),   // 10 buildings, ~100 units
858        ];
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            // Create organization
878            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            // Create admin user for this org
898            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            // Create owners pool for this org
921            let num_owners = (target_units * 2 / 3) as usize; // ~66% occupancy
922            let mut owner_ids = Vec::new();
923
924            for o in 0..num_owners {
925                let owner_id = Uuid::new_v4();
926
927                // Use faker for realistic Belgian data
928                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            // Create buildings for this org
965            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                // Create units for this building
995                let units_this_building = if b == num_buildings - 1 {
996                    // Last building gets remainder
997                    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; // 4 units per floor
1004                    let unit_number = format!("{}.{}", floor, (u % 4) + 1);
1005
1006                    // 66% chance to have an owner
1007                    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                    // Valid unit_type ENUM values: apartment, parking, cellar, commercial, other
1014                    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                // Create 2-3 expenses per building
1040                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                    // Valid expense_category ENUM: maintenance, repairs, insurance, utilities, cleaning, administration, works, other
1057                    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                    // Valid payment_status ENUM: pending, paid, overdue, cancelled
1069                    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    /// Clear all data (DANGEROUS - use with caution!)
1124    pub async fn clear_demo_data(&self) -> Result<String, String> {
1125        log::warn!("⚠️  Clearing all demo data...");
1126
1127        // Delete in correct order due to foreign key constraints
1128        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}