koprogo_api/infrastructure/database/
seed.rs

1use crate::domain::entities::{Account, AccountType, User, UserRole};
2use bcrypt::{hash, DEFAULT_COST};
3use chrono::{NaiveDate, 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        // Upsert superadmin role (preserve if exists, create if missing)
61        // Note: Use INSERT ... ON CONFLICT DO NOTHING for idempotency
62        // The migration backfill (20250130000000) already creates the role with is_primary=true
63        sqlx::query(
64            r#"
65            INSERT INTO user_roles (id, user_id, role, organization_id, is_primary, created_at, updated_at)
66            VALUES (
67                gen_random_uuid(),
68                $1,
69                'superadmin',
70                NULL,
71                NOT EXISTS (SELECT 1 FROM user_roles WHERE user_id = $1 AND is_primary = true),
72                NOW(),
73                NOW()
74            )
75            ON CONFLICT DO NOTHING
76            "#,
77        )
78        .bind(superadmin_id)
79        .execute(&self.pool)
80        .await
81        .map_err(|e| format!("Failed to upsert superadmin role: {}", e))?;
82
83        log::info!("✅ Superadmin ready: {}", superadmin_email);
84
85        Ok(User {
86            id: superadmin_id,
87            email: superadmin_email.to_string(),
88            password_hash,
89            first_name: "Super".to_string(),
90            last_name: "Admin".to_string(),
91            role: UserRole::SuperAdmin,
92            organization_id: None,
93            is_active: true,
94            processing_restricted: false,
95            processing_restricted_at: None,
96            marketing_opt_out: false,
97            marketing_opt_out_at: None,
98            created_at: now,
99            updated_at: now,
100        })
101    }
102
103    /// Seed Belgian PCMN (Plan Comptable Minimum Normalisé) for all organizations
104    /// This ensures every organization has the base chart of accounts
105    pub async fn seed_belgian_pcmn_for_all_organizations(&self) -> Result<String, String> {
106        log::info!("🌱 Seeding Belgian PCMN for all organizations...");
107
108        // Get all organizations
109        let organizations = sqlx::query!("SELECT id FROM organizations")
110            .fetch_all(&self.pool)
111            .await
112            .map_err(|e| format!("Failed to fetch organizations: {}", e))?;
113
114        let mut total_created = 0;
115        let mut orgs_seeded = 0;
116
117        for org in organizations {
118            let org_id = org.id;
119
120            // Check if this organization already has accounts
121            let existing_count = sqlx::query!(
122                "SELECT COUNT(*) as count FROM accounts WHERE organization_id = $1",
123                org_id
124            )
125            .fetch_one(&self.pool)
126            .await
127            .map_err(|e| format!("Failed to count accounts: {}", e))?;
128
129            if existing_count.count.unwrap_or(0) > 0 {
130                log::debug!(
131                    "Organization {} already has {} accounts, skipping",
132                    org_id,
133                    existing_count.count.unwrap_or(0)
134                );
135                continue;
136            }
137
138            // Seed PCMN for this organization
139            let created = self.seed_belgian_pcmn_for_org(org_id).await?;
140            total_created += created;
141            orgs_seeded += 1;
142        }
143
144        let message = format!(
145            "✅ Seeded {} accounts across {} organizations",
146            total_created, orgs_seeded
147        );
148        log::info!("{}", message);
149        Ok(message)
150    }
151
152    /// Seed Belgian PCMN for a specific organization (idempotent)
153    async fn seed_belgian_pcmn_for_org(&self, organization_id: Uuid) -> Result<i64, String> {
154        // Base PCMN accounts based on Belgian accounting standards
155        let base_accounts = vec![
156            // Class 6: Charges (Expenses)
157            (
158                "6100",
159                "Charges courantes",
160                None,
161                AccountType::Expense,
162                true,
163            ),
164            (
165                "6110",
166                "Entretien et réparations",
167                None,
168                AccountType::Expense,
169                true,
170            ),
171            ("6120", "Personnel", None, AccountType::Expense, true),
172            (
173                "6130",
174                "Services extérieurs",
175                None,
176                AccountType::Expense,
177                true,
178            ),
179            (
180                "6140",
181                "Honoraires et commissions",
182                None,
183                AccountType::Expense,
184                true,
185            ),
186            ("6150", "Assurances", None, AccountType::Expense, true),
187            (
188                "6200",
189                "Travaux extraordinaires",
190                None,
191                AccountType::Expense,
192                true,
193            ),
194            // Class 7: Produits (Revenue)
195            (
196                "7000",
197                "Produits de gestion",
198                None,
199                AccountType::Revenue,
200                true,
201            ),
202            (
203                "7100",
204                "Appels de fonds",
205                Some("7000"),
206                AccountType::Revenue,
207                true,
208            ),
209            (
210                "7200",
211                "Autres produits",
212                Some("7000"),
213                AccountType::Revenue,
214                true,
215            ),
216            // Class 4: Tiers (Third parties)
217            (
218                "4000",
219                "Comptes de tiers",
220                None,
221                AccountType::Liability,
222                false,
223            ),
224            (
225                "4100",
226                "TVA à récupérer",
227                Some("4000"),
228                AccountType::Asset,
229                true,
230            ),
231            (
232                "4110",
233                "TVA récupérable",
234                Some("4100"),
235                AccountType::Asset,
236                true,
237            ),
238            (
239                "4400",
240                "Fournisseurs",
241                Some("4000"),
242                AccountType::Liability,
243                true,
244            ),
245            (
246                "4500",
247                "Copropriétaires",
248                Some("4000"),
249                AccountType::Asset,
250                true,
251            ),
252            // Class 5: Trésorerie (Cash/Bank)
253            ("5500", "Banque", None, AccountType::Asset, true),
254            ("5700", "Caisse", None, AccountType::Asset, true),
255        ];
256
257        let mut created_count = 0;
258
259        for (code, label, parent_code, account_type, direct_use) in base_accounts {
260            // Create account using domain entity
261            let account = Account::new(
262                code.to_string(),
263                label.to_string(),
264                parent_code.map(|s| s.to_string()),
265                account_type,
266                direct_use,
267                organization_id,
268            )?;
269
270            // Insert into database (idempotent - skip if exists)
271            let result = sqlx::query!(
272                r#"
273                INSERT INTO accounts (id, code, label, parent_code, account_type, direct_use, organization_id, created_at, updated_at)
274                VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
275                ON CONFLICT (code, organization_id) DO NOTHING
276                "#,
277                account.id,
278                account.code,
279                account.label,
280                account.parent_code,
281                account.account_type as AccountType,
282                account.direct_use,
283                account.organization_id,
284                account.created_at,
285                account.updated_at
286            )
287            .execute(&self.pool)
288            .await
289            .map_err(|e| format!("Failed to insert account {}: {}", code, e))?;
290
291            if result.rows_affected() > 0 {
292                created_count += 1;
293            }
294        }
295
296        log::info!(
297            "Created {} PCMN accounts for organization {}",
298            created_count,
299            organization_id
300        );
301        Ok(created_count)
302    }
303
304    /// Seed demo data for production demonstration
305    pub async fn seed_demo_data(&self) -> Result<String, String> {
306        log::info!("🌱 Starting demo data seeding...");
307
308        // Check if seed data already exists (only check for seed organizations, not all)
309        let existing_seed_orgs =
310            sqlx::query!("SELECT COUNT(*) as count FROM organizations WHERE is_seed_data = true")
311                .fetch_one(&self.pool)
312                .await
313                .map_err(|e| format!("Failed to count seed organizations: {}", e))?;
314
315        if existing_seed_orgs.count.unwrap_or(0) > 0 {
316            return Err(
317                "Seed data already exists. Please use 'Clear Seed Data' first.".to_string(),
318            );
319        }
320
321        // ORGANIZATION 1
322        let org1_id = Uuid::new_v4();
323        let now = Utc::now();
324
325        sqlx::query(
326            r#"
327            INSERT INTO organizations (id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, is_seed_data, created_at, updated_at)
328            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
329            "#
330        )
331        .bind(org1_id)
332        .bind("Résidence Grand Place SPRL")
333        .bind("residence-grand-place")
334        .bind("contact@grandplace.be")
335        .bind("+32 2 501 23 45")
336        .bind("professional")
337        .bind(20)
338        .bind(50)
339        .bind(true) // is_active
340        .bind(true) // is_seed_data
341        .bind(now)
342        .bind(now)
343        .execute(&self.pool)
344        .await
345        .map_err(|e| format!("Failed to create demo organization 1: {}", e))?;
346
347        log::info!("✅ Organization 1 created: Résidence Grand Place SPRL");
348
349        // Create demo users ORG 1
350        let syndic1_id = self
351            .create_demo_user(
352                "syndic@grandplace.be",
353                "syndic123",
354                "Jean",
355                "Dupont",
356                "syndic",
357                Some(org1_id),
358            )
359            .await?;
360
361        let _accountant_id = self
362            .create_demo_user(
363                "comptable@grandplace.be",
364                "comptable123",
365                "Marie",
366                "Martin",
367                "accountant",
368                Some(org1_id),
369            )
370            .await?;
371
372        let owner1_user_id = self
373            .create_demo_user(
374                "proprietaire1@grandplace.be",
375                "owner123",
376                "Pierre",
377                "Durand",
378                "owner",
379                Some(org1_id),
380            )
381            .await?;
382
383        let owner2_user_id = self
384            .create_demo_user(
385                "proprietaire2@grandplace.be",
386                "owner123",
387                "Sophie",
388                "Bernard",
389                "owner",
390                Some(org1_id),
391            )
392            .await?;
393
394        log::info!("✅ Demo users created");
395
396        // Create demo buildings ORG 1
397        let building1_id = self
398            .create_demo_building(
399                org1_id,
400                "Résidence Grand Place",
401                "Grand Place 15",
402                "Bruxelles",
403                "1000",
404                "Belgique",
405                15,
406                1995,
407            )
408            .await?;
409
410        let building2_id = self
411            .create_demo_building(
412                org1_id,
413                "Les Jardins d'Ixelles",
414                "Rue du Trône 85",
415                "Bruxelles",
416                "1050",
417                "Belgique",
418                8,
419                2010,
420            )
421            .await?;
422
423        log::info!("✅ Demo buildings created");
424
425        // Create demo owners
426        let owner1_db_id = self
427            .create_demo_owner(
428                org1_id,
429                "Pierre",
430                "Durand",
431                "pierre.durand@email.be",
432                "+32 476 12 34 56",
433                "Avenue Louise 15",
434                "Bruxelles",
435                "1050",
436                "Belgique",
437            )
438            .await?;
439
440        let owner2_db_id = self
441            .create_demo_owner(
442                org1_id,
443                "Sophie",
444                "Bernard",
445                "sophie.bernard@email.be",
446                "+32 495 98 76 54",
447                "Rue Royale 28",
448                "Bruxelles",
449                "1000",
450                "Belgique",
451            )
452            .await?;
453
454        let owner3_db_id = self
455            .create_demo_owner(
456                org1_id,
457                "Michel",
458                "Lefebvre",
459                "michel.lefebvre@email.be",
460                "+32 477 11 22 33",
461                "Boulevard d'Avroy 42",
462                "Liège",
463                "4000",
464                "Belgique",
465            )
466            .await?;
467
468        log::info!("✅ Demo owners created");
469
470        // Link users to owners (for portal access)
471        sqlx::query("UPDATE owners SET user_id = $1 WHERE id = $2")
472            .bind(owner1_user_id)
473            .bind(owner1_db_id)
474            .execute(&self.pool)
475            .await
476            .map_err(|e| format!("Failed to link owner1 to user: {}", e))?;
477
478        sqlx::query("UPDATE owners SET user_id = $1 WHERE id = $2")
479            .bind(owner2_user_id)
480            .bind(owner2_db_id)
481            .execute(&self.pool)
482            .await
483            .map_err(|e| format!("Failed to link owner2 to user: {}", e))?;
484
485        log::info!("✅ Users linked to owners");
486
487        // Create demo units (owner_id is now deprecated, set to None)
488        let unit1_id = self
489            .create_demo_unit(
490                org1_id,
491                building1_id,
492                None, // owner_id deprecated
493                "101",
494                "apartment",
495                Some(1),
496                75.5,
497                250.0,
498            )
499            .await?;
500
501        let unit2_id = self
502            .create_demo_unit(
503                org1_id,
504                building1_id,
505                None, // owner_id deprecated
506                "102",
507                "apartment",
508                Some(1),
509                62.0,
510                200.0,
511            )
512            .await?;
513
514        let unit3_id = self
515            .create_demo_unit(
516                org1_id,
517                building1_id,
518                None, // owner_id deprecated
519                "103",
520                "apartment",
521                Some(1),
522                85.0,
523                300.0,
524            )
525            .await?;
526
527        let unit4_id = self
528            .create_demo_unit(
529                org1_id,
530                building2_id,
531                None, // owner_id deprecated
532                "201",
533                "apartment",
534                Some(2),
535                95.0,
536                350.0,
537            )
538            .await?;
539
540        log::info!("✅ Demo units created");
541
542        // Create unit_owners relationships
543        // Scenario 1: Unit 101 - Single owner (Pierre Durand 100%)
544        self.create_demo_unit_owner(
545            unit1_id,
546            owner1_db_id,
547            1.0,  // 100%
548            true, // primary contact
549            None, // no end_date (active)
550        )
551        .await?;
552
553        // Scenario 2: Unit 102 - Co-ownership (Sophie Bernard 60%, Michel Lefebvre 40%)
554        self.create_demo_unit_owner(
555            unit2_id,
556            owner2_db_id,
557            0.6,  // 60%
558            true, // primary contact
559            None,
560        )
561        .await?;
562
563        self.create_demo_unit_owner(
564            unit2_id,
565            owner3_db_id,
566            0.4,   // 40%
567            false, // not primary contact
568            None,
569        )
570        .await?;
571
572        // Scenario 3: Unit 103 - Co-ownership with 3 owners (50%, 30%, 20%)
573        self.create_demo_unit_owner(
574            unit3_id,
575            owner1_db_id,
576            0.5,  // 50%
577            true, // primary contact
578            None,
579        )
580        .await?;
581
582        self.create_demo_unit_owner(
583            unit3_id,
584            owner2_db_id,
585            0.3, // 30%
586            false,
587            None,
588        )
589        .await?;
590
591        self.create_demo_unit_owner(
592            unit3_id,
593            owner3_db_id,
594            0.2, // 20%
595            false,
596            None,
597        )
598        .await?;
599
600        // Scenario 4: Unit 201 - Michel Lefebvre owns multiple units (100% of this one)
601        self.create_demo_unit_owner(
602            unit4_id,
603            owner3_db_id,
604            1.0,  // 100%
605            true, // primary contact
606            None,
607        )
608        .await?;
609
610        log::info!("✅ Demo unit_owners relationships created");
611
612        // Seed Belgian PCMN accounts for this organization
613        self.seed_pcmn_accounts(org1_id).await?;
614        log::info!("✅ Belgian PCMN accounts seeded");
615
616        // Create demo expenses with realistic Belgian VAT rates and accounting links
617        // Expense 1: Quarterly condo fees (paid) - 21% VAT
618        let expense1_id = self
619            .create_demo_expense_with_vat(
620                building1_id,
621                org1_id,
622                "Charges copropriété T1 2025",
623                4132.23, // HT
624                21.0,    // VAT 21%
625                "2025-01-15",
626                "2025-02-15", // due date
627                "administration",
628                "paid",
629                Some("Syndic Services SPRL"),
630                Some("SYN-2025-001"),
631                Some("6100"), // PCMN: Charges courantes
632            )
633            .await?;
634
635        // Expense 2: Elevator repair (paid) - 21% VAT
636        let expense2_id = self
637            .create_demo_expense_with_vat(
638                building1_id,
639                org1_id,
640                "Réparation ascenseur - Remplacement moteur",
641                2066.12, // HT
642                21.0,    // VAT 21%
643                "2025-02-10",
644                "2025-03-10",
645                "maintenance",
646                "paid",
647                Some("Ascenseurs Plus SA"),
648                Some("ASC-2025-023"),
649                Some("6110"), // PCMN: Entretien et réparations
650            )
651            .await?;
652
653        // Expense 3: Quarterly condo fees building 2 (pending, will become overdue) - 21% VAT
654        let expense3_id = self
655            .create_demo_expense_with_vat(
656                building2_id,
657                org1_id,
658                "Charges copropriété T1 2025",
659                2479.34, // HT
660                21.0,    // VAT 21%
661                "2025-01-15",
662                "2025-02-15", // OVERDUE (due 2 months ago)
663                "administration",
664                "overdue",
665                Some("Syndic Services SPRL"),
666                Some("SYN-2025-002"),
667                Some("6100"), // PCMN: Charges courantes
668            )
669            .await?;
670
671        // Expense 4: Cleaning (paid) - 6% VAT (reduced rate for certain services)
672        let expense4_id = self
673            .create_demo_expense_with_vat(
674                building2_id,
675                org1_id,
676                "Nettoyage parties communes - Forfait annuel",
677                1132.08, // HT
678                6.0,     // VAT 6% (reduced rate)
679                "2025-01-01",
680                "2025-01-31",
681                "cleaning",
682                "paid",
683                Some("CleanPro Belgium SPRL"),
684                Some("CLN-2025-156"),
685                Some("6130"), // PCMN: Services extérieurs
686            )
687            .await?;
688
689        // Expense 5: Insurance (pending) - 0% VAT (insurance exempt)
690        let expense5_id = self
691            .create_demo_expense_with_vat(
692                building1_id,
693                org1_id,
694                "Assurance incendie immeuble 2025",
695                1850.00, // HT (no VAT)
696                0.0,     // VAT 0% (exempt)
697                "2025-01-05",
698                "2025-02-05",
699                "insurance",
700                "pending",
701                Some("AXA Belgium"),
702                Some("AXA-2025-8472"),
703                Some("6150"), // PCMN: Assurances
704            )
705            .await?;
706
707        // Expense 6: Facade works (pending approval) - 21% VAT
708        let expense6_id = self
709            .create_demo_expense_with_vat(
710                building1_id,
711                org1_id,
712                "Rénovation façade - Devis Entreprise Martin",
713                12396.69, // HT
714                21.0,     // VAT 21%
715                "2025-03-01",
716                "2025-04-30",
717                "works",
718                "pending",
719                Some("Entreprise Martin & Fils SPRL"),
720                Some("MART-2025-042"),
721                Some("6200"), // PCMN: Travaux extraordinaires
722            )
723            .await?;
724
725        // ===== CURRENT MONTH EXPENSES =====
726        // Use relative dates based on today's date
727        let now = Utc::now();
728        let current_month = now.format("%B %Y").to_string();
729        let month_start = format!("{}", now.format("%Y-%m-01"));
730        let day_3 = format!("{}", now.format("%Y-%m-03"));
731        let day_5 = format!("{}", now.format("%Y-%m-05"));
732        let day_8 = format!("{}", now.format("%Y-%m-08"));
733        let day_10 = format!("{}", now.format("%Y-%m-10"));
734        let month_end = format!("{}", (now + chrono::Duration::days(30)).format("%Y-%m-%d"));
735
736        // Expense 7: Elevator maintenance (current month) - paid - 21% VAT
737        let expense7_id = self
738            .create_demo_expense_with_vat(
739                building1_id,
740                org1_id,
741                &format!("Maintenance ascenseur {}", current_month),
742                826.45, // HT
743                21.0,   // VAT 21%
744                &day_5,
745                &month_end,
746                "maintenance",
747                "paid",
748                Some("Ascenseurs Plus SA"),
749                Some(&format!("ASC-{}-001", now.format("%Y-%m"))),
750                Some("6110"), // PCMN: Entretien et réparations
751            )
752            .await?;
753
754        // Expense 8: Electricity bill (current month) - paid - 21% VAT
755        let expense8_id = self
756            .create_demo_expense_with_vat(
757                building1_id,
758                org1_id,
759                &format!("Électricité communs {}", current_month),
760                387.60, // HT
761                21.0,   // VAT 21%
762                &day_3,
763                &format!("{}", (now + chrono::Duration::days(25)).format("%Y-%m-%d")),
764                "utilities",
765                "paid",
766                Some("Engie Electrabel"),
767                Some(&format!("ENGIE-{}-3847", now.format("%Y-%m"))),
768                Some("6100"), // PCMN: Charges courantes (Électricité)
769            )
770            .await?;
771
772        // Expense 9: Cleaning service (current month) - paid - 6% VAT
773        let expense9_id = self
774            .create_demo_expense_with_vat(
775                building1_id,
776                org1_id,
777                &format!("Nettoyage communs {}", current_month),
778                471.70, // HT
779                6.0,    // VAT 6% (labor-intensive services)
780                &month_start,
781                &format!("{}", (now + chrono::Duration::days(20)).format("%Y-%m-%d")),
782                "cleaning",
783                "paid",
784                Some("NetClean Services SPRL"),
785                Some(&format!("CLEAN-{}-074", now.format("%Y-%m"))),
786                Some("6130"), // PCMN: Services extérieurs (Nettoyage)
787            )
788            .await?;
789
790        // Expense 10: Water bill (current month) - pending - 6% VAT
791        let expense10_id = self
792            .create_demo_expense_with_vat(
793                building1_id,
794                org1_id,
795                &format!("Eau communs {}", current_month),
796                156.60, // HT
797                6.0,    // VAT 6%
798                &day_8,
799                &format!("{}", (now + chrono::Duration::days(30)).format("%Y-%m-%d")),
800                "utilities",
801                "pending",
802                Some("Vivaqua"),
803                Some(&format!("VIVA-{}-9284", now.format("%Y-%m"))),
804                Some("6100"), // PCMN: Charges courantes (Eau)
805            )
806            .await?;
807
808        // Expense 11: Heating gas (current month) - paid - 21% VAT
809        let expense11_id = self
810            .create_demo_expense_with_vat(
811                building1_id,
812                org1_id,
813                &format!("Chauffage gaz {}", current_month),
814                1240.00, // HT
815                21.0,    // VAT 21%
816                &day_10,
817                &format!("{}", (now + chrono::Duration::days(30)).format("%Y-%m-%d")),
818                "utilities",
819                "paid",
820                Some("Sibelga"),
821                Some(&format!("SIBEL-{}-7453", now.format("%Y-%m"))),
822                Some("6100"), // PCMN: Charges courantes (Chauffage)
823            )
824            .await?;
825
826        log::info!("✅ Demo expenses with VAT created (including current month)");
827
828        // Calculate and save charge distributions
829        self.create_demo_distributions(expense1_id, org1_id).await?;
830        self.create_demo_distributions(expense2_id, org1_id).await?;
831        self.create_demo_distributions(expense3_id, org1_id).await?;
832        self.create_demo_distributions(expense4_id, org1_id).await?;
833        self.create_demo_distributions(expense5_id, org1_id).await?;
834        self.create_demo_distributions(expense6_id, org1_id).await?;
835        self.create_demo_distributions(expense7_id, org1_id).await?;
836        self.create_demo_distributions(expense8_id, org1_id).await?;
837        self.create_demo_distributions(expense9_id, org1_id).await?;
838        self.create_demo_distributions(expense10_id, org1_id)
839            .await?;
840        self.create_demo_distributions(expense11_id, org1_id)
841            .await?;
842        log::info!("✅ Charge distributions calculated");
843
844        // Create payment reminders for overdue expense
845        self.create_demo_payment_reminder(
846            expense3_id,
847            owner2_db_id, // Sophie Bernard
848            org1_id,
849            "FirstReminder",
850            20, // 20 days overdue
851        )
852        .await?;
853
854        self.create_demo_payment_reminder(
855            expense3_id,
856            owner3_db_id, // Michel Lefebvre
857            org1_id,
858            "SecondReminder",
859            35, // 35 days overdue
860        )
861        .await?;
862
863        log::info!("✅ Payment reminders created");
864
865        // Create owner contributions (revenue) for current month
866        log::info!("Creating owner contributions...");
867
868        // Get quarter number for current month (using Datelike trait)
869        use chrono::Datelike;
870        let quarter = ((now.month() - 1) / 3) + 1;
871        let year = now.year();
872
873        // Regular contributions (appels de fonds) for current month
874        // Each owner pays quarterly fees
875        self.create_demo_owner_contribution(
876            org1_id,
877            owner1_db_id, // Jean Dupont
878            Some(unit1_id),
879            &format!("Appel de fonds T{} {} - Charges courantes", quarter, year),
880            650.0,
881            "regular",
882            &month_start,
883            "paid",
884            Some(&day_5),
885            Some("7000"), // PCMN: Regular contributions
886        )
887        .await?;
888
889        self.create_demo_owner_contribution(
890            org1_id,
891            owner2_db_id, // Sophie Bernard
892            Some(unit2_id),
893            &format!("Appel de fonds T{} {} - Charges courantes", quarter, year),
894            750.0,
895            "regular",
896            &month_start,
897            "paid",
898            Some(&day_8),
899            Some("7000"),
900        )
901        .await?;
902
903        self.create_demo_owner_contribution(
904            org1_id,
905            owner3_db_id, // Michel Lefebvre
906            Some(unit3_id),
907            &format!("Appel de fonds T{} {} - Charges courantes", quarter, year),
908            600.0,
909            "regular",
910            &month_start,
911            "pending",
912            None, // Not paid yet
913            Some("7000"),
914        )
915        .await?;
916
917        // Note: Only 3 owners in seed data, so we skip owner4
918
919        // Extraordinary contribution for roof repairs (previous month)
920        let prev_month = (now - chrono::Duration::days(20))
921            .format("%Y-%m-05")
922            .to_string();
923        let prev_month_payment = (now - chrono::Duration::days(15))
924            .format("%Y-%m-10")
925            .to_string();
926
927        self.create_demo_owner_contribution(
928            org1_id,
929            owner1_db_id, // Jean Dupont
930            Some(unit1_id),
931            "Appel de fonds extraordinaire - Réfection toiture",
932            1200.0,
933            "extraordinary",
934            &prev_month,
935            "paid",
936            Some(&prev_month_payment),
937            Some("7100"), // PCMN: Extraordinary contributions
938        )
939        .await?;
940
941        self.create_demo_owner_contribution(
942            org1_id,
943            owner2_db_id, // Sophie Bernard
944            Some(unit2_id),
945            "Appel de fonds extraordinaire - Réfection toiture",
946            1400.0,
947            "extraordinary",
948            &prev_month,
949            "pending",
950            None, // Not paid yet
951            Some("7100"),
952        )
953        .await?;
954
955        log::info!("✅ Owner contributions created");
956
957        // Create meetings ORG 1 (in the past, 3-6 months ago)
958        let meeting1_date = (now - chrono::Duration::days(90))
959            .format("%Y-%m-%d")
960            .to_string();
961        let meeting2_date = (now - chrono::Duration::days(60))
962            .format("%Y-%m-%d")
963            .to_string();
964
965        let meeting1_id = self
966            .create_demo_meeting(
967                building1_id,
968                org1_id,
969                &format!("Assemblée Générale Ordinaire {}", year),
970                "ordinary",
971                &meeting1_date,
972                "completed",
973            )
974            .await?;
975
976        let meeting2_id = self
977            .create_demo_meeting(
978                building2_id,
979                org1_id,
980                "Assemblée Générale Extraordinaire - Travaux",
981                "extraordinary",
982                &meeting2_date,
983                "completed",
984            )
985            .await?;
986
987        log::info!("✅ Demo meetings created");
988
989        // Create board members ORG 1
990        // Board mandates are for 1 year from meeting1_date
991        let mandate_start = meeting1_date.clone();
992        let mandate_end = (now + chrono::Duration::days(275))
993            .format("%Y-%m-%d")
994            .to_string(); // ~9 months from now
995
996        // Elect owner1 as president for building1 (mandate: ~1 year as per Belgian law)
997        self.create_demo_board_member(
998            owner1_db_id,
999            building1_id,
1000            org1_id,
1001            meeting1_id,
1002            "president",
1003            &mandate_start,
1004            &mandate_end,
1005        )
1006        .await?;
1007
1008        // Elect owner2 as treasurer for building2 (mandate: ~1 year)
1009        self.create_demo_board_member(
1010            owner2_db_id,
1011            building2_id,
1012            org1_id,
1013            meeting2_id,
1014            "treasurer",
1015            &meeting2_date,
1016            &format!("{}", (now + chrono::Duration::days(305)).format("%Y-%m-%d")),
1017        )
1018        .await?;
1019
1020        log::info!("✅ Demo board members elected");
1021
1022        // Create board decisions ORG 1
1023        // Decision 1: Pending with deadline in 25 days (medium urgency)
1024        self.create_demo_board_decision(
1025            building1_id,
1026            org1_id,
1027            meeting1_id,
1028            "Rénovation de la façade",
1029            "Approuver les devis pour la rénovation de la façade principale",
1030            Some("2025-11-26"), // ~25 days from 2025-11-01
1031            "pending",
1032        )
1033        .await?;
1034
1035        // Decision 2: In progress with deadline in 4 days (critical urgency)
1036        self.create_demo_board_decision(
1037            building1_id,
1038            org1_id,
1039            meeting1_id,
1040            "Contrat d'assurance",
1041            "Signer le nouveau contrat d'assurance avec AXA",
1042            Some("2025-11-05"), // 4 days from 2025-11-01
1043            "in_progress",
1044        )
1045        .await?;
1046
1047        // Decision 3: Overdue (deadline passed)
1048        self.create_demo_board_decision(
1049            building1_id,
1050            org1_id,
1051            meeting1_id,
1052            "Nettoyage des gouttières",
1053            "Engager une entreprise pour le nettoyage annuel des gouttières",
1054            Some("2025-10-15"), // Past deadline
1055            "pending",
1056        )
1057        .await?;
1058
1059        // Decision 4: Completed
1060        self.create_demo_board_decision(
1061            building1_id,
1062            org1_id,
1063            meeting1_id,
1064            "Installation caméras",
1065            "Installation du système de vidéosurveillance dans le hall",
1066            Some("2025-10-01"),
1067            "completed",
1068        )
1069        .await?;
1070
1071        // Decision 5: Pending with deadline in 10 days (high urgency)
1072        self.create_demo_board_decision(
1073            building2_id,
1074            org1_id,
1075            meeting2_id,
1076            "Remplacement chaudière",
1077            "Valider le choix du fournisseur pour la nouvelle chaudière",
1078            Some("2025-11-11"), // 10 days from 2025-11-01
1079            "pending",
1080        )
1081        .await?;
1082
1083        // Decision 6: In progress with deadline in 20 days (medium urgency)
1084        self.create_demo_board_decision(
1085            building2_id,
1086            org1_id,
1087            meeting2_id,
1088            "Aménagement parking vélos",
1089            "Organiser l'aménagement du parking à vélos au rez-de-chaussée",
1090            Some("2025-11-21"), // 20 days from 2025-11-01
1091            "in_progress",
1092        )
1093        .await?;
1094
1095        log::info!("✅ Demo board decisions created");
1096
1097        // Create documents ORG 1
1098        self.create_demo_document(
1099            building1_id,
1100            org1_id,
1101            "Procès-Verbal AG 2024",
1102            "meeting_minutes",
1103            "/uploads/demo/pv-ag-2024.pdf",
1104            syndic1_id,
1105        )
1106        .await?;
1107
1108        self.create_demo_document(
1109            building1_id,
1110            org1_id,
1111            "Règlement de copropriété",
1112            "regulation",
1113            "/uploads/demo/reglement.pdf",
1114            syndic1_id,
1115        )
1116        .await?;
1117
1118        log::info!("✅ Demo documents created");
1119
1120        // ORGANIZATION 2 - Bruxelles
1121        let org2_id = Uuid::new_v4();
1122        sqlx::query(
1123            r#"
1124            INSERT INTO organizations (id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, is_seed_data, created_at, updated_at)
1125            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
1126            "#
1127        )
1128        .bind(org2_id)
1129        .bind("Copropriété Bruxelles SPRL")
1130        .bind("copro-bruxelles")
1131        .bind("info@copro-bruxelles.be")
1132        .bind("+32 2 123 45 67")
1133        .bind("starter")
1134        .bind(5)
1135        .bind(10)
1136        .bind(true) // is_active
1137        .bind(true) // is_seed_data
1138        .bind(now)
1139        .bind(now)
1140        .execute(&self.pool)
1141        .await
1142        .map_err(|e| format!("Failed to create demo organization 2: {}", e))?;
1143
1144        let _syndic2_id = self
1145            .create_demo_user(
1146                "syndic@copro-bruxelles.be",
1147                "syndic123",
1148                "Marc",
1149                "Dubois",
1150                "syndic",
1151                Some(org2_id),
1152            )
1153            .await?;
1154
1155        let building3_id = self
1156            .create_demo_building(
1157                org2_id,
1158                "Résidence Européenne",
1159                "Avenue Louise 123",
1160                "Bruxelles",
1161                "1050",
1162                "Belgique",
1163                12,
1164                2005,
1165            )
1166            .await?;
1167
1168        self.create_demo_meeting(
1169            building3_id,
1170            org2_id,
1171            "AG Annuelle 2025",
1172            "ordinary",
1173            "2025-05-10",
1174            "scheduled",
1175        )
1176        .await?;
1177
1178        log::info!("✅ Organization 2 created");
1179
1180        // ORGANIZATION 3 - Liège
1181        let org3_id = Uuid::new_v4();
1182        sqlx::query(
1183            r#"
1184            INSERT INTO organizations (id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, is_seed_data, created_at, updated_at)
1185            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
1186            "#
1187        )
1188        .bind(org3_id)
1189        .bind("Syndic Liège SA")
1190        .bind("syndic-liege")
1191        .bind("contact@syndic-liege.be")
1192        .bind("+32 4 222 33 44")
1193        .bind("enterprise")
1194        .bind(50)
1195        .bind(100)
1196        .bind(true) // is_active
1197        .bind(true) // is_seed_data
1198        .bind(now)
1199        .bind(now)
1200        .execute(&self.pool)
1201        .await
1202        .map_err(|e| format!("Failed to create demo organization 3: {}", e))?;
1203
1204        let _syndic3_id = self
1205            .create_demo_user(
1206                "syndic@syndic-liege.be",
1207                "syndic123",
1208                "Sophie",
1209                "Lambert",
1210                "syndic",
1211                Some(org3_id),
1212            )
1213            .await?;
1214
1215        let _building4_id = self
1216            .create_demo_building(
1217                org3_id,
1218                "Les Terrasses de Liège",
1219                "Boulevard de la Sauvenière 45",
1220                "Liège",
1221                "4000",
1222                "Belgique",
1223                8,
1224                2018,
1225            )
1226            .await?;
1227
1228        log::info!("✅ Organization 3 created");
1229
1230        Ok("✅ Demo data seeded successfully!\n\n\
1231            📊 Summary:\n\
1232            - 3 Organizations: Grand Place (Bruxelles), Bruxelles Louise, Liège\n\
1233            - 6+ Users: 3 Syndics, 1 Accountant, 2+ Owners\n\
1234            - 4 Buildings across Belgium\n\
1235            - 3 Owners (database records)\n\
1236            - 4 Units\n\
1237            - 4 Expenses\n\
1238            - 3 Meetings\n\
1239            - 2 Documents\n\n\
1240            🇧🇪 Belgian Demo - Credentials:\n\
1241            - Org 1 (Grand Place): syndic@grandplace.be / syndic123\n\
1242            - Org 2 (Bruxelles): syndic@copro-bruxelles.be / syndic123\n\
1243            - Org 3 (Liège): syndic@syndic-liege.be / syndic123\n\
1244            - SuperAdmin: admin@koprogo.com / admin123"
1245            .to_string())
1246    }
1247
1248    async fn create_demo_user(
1249        &self,
1250        email: &str,
1251        password: &str,
1252        first_name: &str,
1253        last_name: &str,
1254        role: &str,
1255        organization_id: Option<Uuid>,
1256    ) -> Result<Uuid, String> {
1257        let password_hash =
1258            hash(password, DEFAULT_COST).map_err(|e| format!("Failed to hash password: {}", e))?;
1259
1260        let user_id = Uuid::new_v4();
1261        let now = Utc::now();
1262
1263        sqlx::query!(
1264            r#"
1265            INSERT INTO users (id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at)
1266            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
1267            "#,
1268            user_id,
1269            email,
1270            password_hash,
1271            first_name,
1272            last_name,
1273            role,
1274            organization_id,
1275            true,
1276            now,
1277            now
1278        )
1279        .execute(&self.pool)
1280        .await
1281        .map_err(|e| format!("Failed to create user {}: {}", email, e))?;
1282
1283        sqlx::query(
1284            r#"
1285            INSERT INTO user_roles (id, user_id, role, organization_id, is_primary, created_at, updated_at)
1286            VALUES (gen_random_uuid(), $1, $2, $3, true, $4, $4)
1287            ON CONFLICT (user_id, role, organization_id)
1288            DO UPDATE SET is_primary = true, updated_at = EXCLUDED.updated_at
1289            "#,
1290        )
1291        .bind(user_id)
1292        .bind(role)
1293        .bind(organization_id)
1294        .bind(now)
1295        .execute(&self.pool)
1296        .await
1297        .map_err(|e| format!("Failed to assign role {} to user {}: {}", role, email, e))?;
1298
1299        Ok(user_id)
1300    }
1301
1302    #[allow(clippy::too_many_arguments)]
1303    async fn create_demo_building(
1304        &self,
1305        org_id: Uuid,
1306        name: &str,
1307        address: &str,
1308        city: &str,
1309        postal_code: &str,
1310        country: &str,
1311        total_units: i32,
1312        construction_year: i32,
1313    ) -> Result<Uuid, String> {
1314        let building_id = Uuid::new_v4();
1315        let now = Utc::now();
1316
1317        sqlx::query!(
1318            r#"
1319            INSERT INTO buildings (id, organization_id, name, address, city, postal_code, country, total_units, construction_year, created_at, updated_at)
1320            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
1321            "#,
1322            building_id,
1323            org_id,
1324            name,
1325            address,
1326            city,
1327            postal_code,
1328            country,
1329            total_units,
1330            construction_year,
1331            now,
1332            now
1333        )
1334        .execute(&self.pool)
1335        .await
1336        .map_err(|e| format!("Failed to create building {}: {}", name, e))?;
1337
1338        Ok(building_id)
1339    }
1340
1341    #[allow(clippy::too_many_arguments)]
1342    async fn create_demo_owner(
1343        &self,
1344        organization_id: Uuid,
1345        first_name: &str,
1346        last_name: &str,
1347        email: &str,
1348        phone: &str,
1349        address: &str,
1350        city: &str,
1351        postal_code: &str,
1352        country: &str,
1353    ) -> Result<Uuid, String> {
1354        let owner_id = Uuid::new_v4();
1355        let now = Utc::now();
1356
1357        sqlx::query!(
1358            r#"
1359            INSERT INTO owners (id, organization_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at)
1360            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
1361            "#,
1362            owner_id,
1363            organization_id,
1364            first_name,
1365            last_name,
1366            email,
1367            phone,
1368            address,
1369            city,
1370            postal_code,
1371            country,
1372            now,
1373            now
1374        )
1375        .execute(&self.pool)
1376        .await
1377        .map_err(|e| format!("Failed to create owner {} {}: {}", first_name, last_name, e))?;
1378
1379        Ok(owner_id)
1380    }
1381
1382    #[allow(clippy::too_many_arguments)]
1383    async fn create_demo_unit(
1384        &self,
1385        organization_id: Uuid,
1386        building_id: Uuid,
1387        owner_id: Option<Uuid>,
1388        unit_number: &str,
1389        unit_type: &str,
1390        floor: Option<i32>,
1391        surface_area: f64,
1392        quota: f64,
1393    ) -> Result<Uuid, String> {
1394        let unit_id = Uuid::new_v4();
1395        let now = Utc::now();
1396
1397        sqlx::query(
1398            r#"
1399            INSERT INTO units (id, organization_id, building_id, owner_id, unit_number, unit_type, floor, surface_area, quota, created_at, updated_at)
1400            VALUES ($1, $2, $3, $4, $5, $6::unit_type, $7, $8, $9, $10, $11)
1401            "#
1402        )
1403        .bind(unit_id)
1404        .bind(organization_id)
1405        .bind(building_id)
1406        .bind(owner_id)
1407        .bind(unit_number)
1408        .bind(unit_type)
1409        .bind(floor)
1410        .bind(surface_area)
1411        .bind(quota)
1412        .bind(now)
1413        .bind(now)
1414        .execute(&self.pool)
1415        .await
1416        .map_err(|e| format!("Failed to create unit {}: {}", unit_number, e))?;
1417
1418        Ok(unit_id)
1419    }
1420
1421    async fn create_demo_unit_owner(
1422        &self,
1423        unit_id: Uuid,
1424        owner_id: Uuid,
1425        ownership_percentage: f64,
1426        is_primary_contact: bool,
1427        end_date: Option<chrono::DateTime<Utc>>,
1428    ) -> Result<Uuid, String> {
1429        let unit_owner_id = Uuid::new_v4();
1430        let now = Utc::now();
1431
1432        sqlx::query!(
1433            r#"
1434            INSERT INTO unit_owners (id, unit_id, owner_id, ownership_percentage, start_date, end_date, is_primary_contact, created_at, updated_at)
1435            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
1436            "#,
1437            unit_owner_id,
1438            unit_id,
1439            owner_id,
1440            ownership_percentage,
1441            now, // start_date
1442            end_date,
1443            is_primary_contact,
1444            now, // created_at
1445            now  // updated_at
1446        )
1447        .execute(&self.pool)
1448        .await
1449        .map_err(|e| format!("Failed to create unit_owner relationship: {}", e))?;
1450
1451        Ok(unit_owner_id)
1452    }
1453
1454    #[allow(dead_code)]
1455    #[allow(clippy::too_many_arguments)]
1456    async fn create_demo_expense(
1457        &self,
1458        building_id: Uuid,
1459        organization_id: Uuid,
1460        description: &str,
1461        amount: f64,
1462        expense_date: &str,
1463        category: &str,
1464        payment_status: &str,
1465        supplier: Option<&str>,
1466        invoice_number: Option<&str>,
1467    ) -> Result<Uuid, String> {
1468        let expense_id = Uuid::new_v4();
1469        let now = Utc::now();
1470        let expense_date_parsed =
1471            chrono::DateTime::parse_from_rfc3339(&format!("{}T00:00:00Z", expense_date))
1472                .map_err(|e| format!("Failed to parse date: {}", e))?
1473                .with_timezone(&Utc);
1474
1475        // Set approval_status based on payment_status
1476        let approval_status = if payment_status == "paid" {
1477            "approved" // If already paid, it must be approved
1478        } else {
1479            "draft" // Otherwise, start as draft
1480        };
1481
1482        // Set paid_date if already paid
1483        let paid_date = if payment_status == "paid" {
1484            Some(expense_date_parsed)
1485        } else {
1486            None
1487        };
1488
1489        // Check if expense already exists (idempotency)
1490        let existing: Option<(Uuid,)> =
1491            sqlx::query_as("SELECT id FROM expenses WHERE description = $1 AND building_id = $2")
1492                .bind(description)
1493                .bind(building_id)
1494                .fetch_optional(&self.pool)
1495                .await
1496                .map_err(|e| format!("Failed to check existing expense: {}", e))?;
1497
1498        let final_expense_id = if let Some((existing_id,)) = existing {
1499            // Update existing expense
1500            sqlx::query(
1501                r#"
1502                UPDATE expenses SET
1503                    category = $1::expense_category,
1504                    amount = $2,
1505                    expense_date = $3,
1506                    payment_status = $4::payment_status,
1507                    approval_status = $5::approval_status,
1508                    paid_date = $6,
1509                    supplier = $7,
1510                    invoice_number = $8,
1511                    updated_at = $9
1512                WHERE id = $10
1513                "#,
1514            )
1515            .bind(category)
1516            .bind(amount)
1517            .bind(expense_date_parsed)
1518            .bind(payment_status)
1519            .bind(approval_status)
1520            .bind(paid_date)
1521            .bind(supplier)
1522            .bind(invoice_number)
1523            .bind(now)
1524            .bind(existing_id)
1525            .execute(&self.pool)
1526            .await
1527            .map_err(|e| format!("Failed to update expense: {}", e))?;
1528            existing_id
1529        } else {
1530            // Insert new expense
1531            sqlx::query(
1532                r#"
1533                INSERT INTO expenses (id, organization_id, building_id, category, description, amount, expense_date, payment_status, approval_status, paid_date, supplier, invoice_number, created_at, updated_at)
1534                VALUES ($1, $2, $3, $4::expense_category, $5, $6, $7, $8::payment_status, $9::approval_status, $10, $11, $12, $13, $14)
1535                "#
1536            )
1537            .bind(expense_id)
1538            .bind(organization_id)
1539            .bind(building_id)
1540            .bind(category)
1541            .bind(description)
1542            .bind(amount)
1543            .bind(expense_date_parsed)
1544            .bind(payment_status)
1545            .bind(approval_status)
1546            .bind(paid_date)
1547            .bind(supplier)
1548            .bind(invoice_number)
1549            .bind(now)
1550            .bind(now)
1551            .execute(&self.pool)
1552            .await
1553            .map_err(|e| format!("Failed to create expense: {}", e))?;
1554            expense_id
1555        };
1556
1557        Ok(final_expense_id)
1558    }
1559
1560    #[allow(clippy::too_many_arguments)]
1561    async fn create_demo_meeting(
1562        &self,
1563        building_id: Uuid,
1564        org_id: Uuid,
1565        title: &str,
1566        meeting_type: &str,
1567        scheduled_date: &str,
1568        status: &str,
1569    ) -> Result<Uuid, String> {
1570        let meeting_id = Uuid::new_v4();
1571        let now = Utc::now();
1572        let scheduled_date_parsed =
1573            chrono::DateTime::parse_from_rfc3339(&format!("{}T10:00:00Z", scheduled_date))
1574                .map_err(|e| format!("Failed to parse date: {}", e))?
1575                .with_timezone(&Utc);
1576
1577        let agenda_json = serde_json::json!([
1578            "Approbation des comptes",
1579            "Travaux à prévoir",
1580            "Questions diverses"
1581        ]);
1582
1583        sqlx::query(
1584            r#"
1585            INSERT INTO meetings (id, building_id, organization_id, meeting_type, title, description, scheduled_date, location, status, agenda, created_at, updated_at)
1586            VALUES ($1, $2, $3, $4::meeting_type, $5, $6, $7, $8, $9::meeting_status, $10, $11, $12)
1587            "#
1588        )
1589        .bind(meeting_id)
1590        .bind(building_id)
1591        .bind(org_id)
1592        .bind(meeting_type)
1593        .bind(title)
1594        .bind(Some("Assemblée générale annuelle"))
1595        .bind(scheduled_date_parsed)
1596        .bind("Salle polyvalente")
1597        .bind(status)
1598        .bind(agenda_json)
1599        .bind(now)
1600        .bind(now)
1601        .execute(&self.pool)
1602        .await
1603        .map_err(|e| format!("Failed to create meeting: {}", e))?;
1604
1605        Ok(meeting_id)
1606    }
1607
1608    #[allow(clippy::too_many_arguments)]
1609    async fn create_demo_document(
1610        &self,
1611        building_id: Uuid,
1612        org_id: Uuid,
1613        title: &str,
1614        document_type: &str,
1615        file_path: &str,
1616        uploaded_by: Uuid,
1617    ) -> Result<Uuid, String> {
1618        let document_id = Uuid::new_v4();
1619        let now = Utc::now();
1620
1621        sqlx::query(
1622            r#"
1623            INSERT INTO documents (id, building_id, organization_id, document_type, title, description, file_path, file_size, mime_type, uploaded_by, created_at, updated_at)
1624            VALUES ($1, $2, $3, $4::document_type, $5, $6, $7, $8, $9, $10, $11, $12)
1625            "#
1626        )
1627        .bind(document_id)
1628        .bind(building_id)
1629        .bind(org_id)
1630        .bind(document_type)
1631        .bind(title)
1632        .bind(Some("Document de démonstration"))
1633        .bind(file_path)
1634        .bind(1024_i64)
1635        .bind("application/pdf")
1636        .bind(uploaded_by)
1637        .bind(now)
1638        .bind(now)
1639        .execute(&self.pool)
1640        .await
1641        .map_err(|e| format!("Failed to create document: {}", e))?;
1642
1643        Ok(document_id)
1644    }
1645
1646    #[allow(clippy::too_many_arguments)]
1647    async fn create_demo_board_member(
1648        &self,
1649        owner_id: Uuid,
1650        building_id: Uuid,
1651        org_id: Uuid,
1652        meeting_id: Uuid,
1653        position: &str,
1654        mandate_start: &str,
1655        mandate_end: &str,
1656    ) -> Result<Uuid, String> {
1657        let board_member_id = Uuid::new_v4();
1658        let now = Utc::now();
1659
1660        let mandate_start_parsed = NaiveDate::parse_from_str(mandate_start, "%Y-%m-%d")
1661            .map_err(|e| format!("Failed to parse mandate_start date: {}", e))?
1662            .and_hms_opt(0, 0, 0)
1663            .ok_or("Failed to create datetime")?;
1664
1665        let mandate_end_parsed = NaiveDate::parse_from_str(mandate_end, "%Y-%m-%d")
1666            .map_err(|e| format!("Failed to parse mandate_end date: {}", e))?
1667            .and_hms_opt(0, 0, 0)
1668            .ok_or("Failed to create datetime")?;
1669
1670        sqlx::query(
1671            r#"
1672            INSERT INTO board_members (id, owner_id, building_id, organization_id, position, mandate_start, mandate_end, elected_by_meeting_id, is_active, created_at, updated_at)
1673            VALUES ($1, $2, $3, $4, $5::board_position, $6, $7, $8, $9, $10, $11)
1674            "#
1675        )
1676        .bind(board_member_id)
1677        .bind(owner_id)
1678        .bind(building_id)
1679        .bind(org_id)
1680        .bind(position)
1681        .bind(mandate_start_parsed)
1682        .bind(mandate_end_parsed)
1683        .bind(meeting_id)
1684        .bind(true)
1685        .bind(now)
1686        .bind(now)
1687        .execute(&self.pool)
1688        .await
1689        .map_err(|e| format!("Failed to create board member: {}", e))?;
1690
1691        Ok(board_member_id)
1692    }
1693
1694    #[allow(clippy::too_many_arguments)]
1695    async fn create_demo_board_decision(
1696        &self,
1697        building_id: Uuid,
1698        org_id: Uuid,
1699        meeting_id: Uuid,
1700        subject: &str,
1701        decision_text: &str,
1702        deadline: Option<&str>,
1703        status: &str,
1704    ) -> Result<Uuid, String> {
1705        let decision_id = Uuid::new_v4();
1706        let now = Utc::now();
1707
1708        let deadline_parsed = if let Some(deadline_str) = deadline {
1709            Some(
1710                NaiveDate::parse_from_str(deadline_str, "%Y-%m-%d")
1711                    .map_err(|e| format!("Failed to parse deadline date: {}", e))?
1712                    .and_hms_opt(0, 0, 0)
1713                    .ok_or("Failed to create datetime")?,
1714            )
1715        } else {
1716            None
1717        };
1718
1719        sqlx::query(
1720            r#"
1721            INSERT INTO board_decisions (id, building_id, organization_id, meeting_id, subject, decision_text, deadline, status, created_at, updated_at)
1722            VALUES ($1, $2, $3, $4, $5, $6, $7, $8::decision_status, $9, $10)
1723            "#
1724        )
1725        .bind(decision_id)
1726        .bind(building_id)
1727        .bind(org_id)
1728        .bind(meeting_id)
1729        .bind(subject)
1730        .bind(decision_text)
1731        .bind(deadline_parsed)
1732        .bind(status)
1733        .bind(now)
1734        .bind(now)
1735        .execute(&self.pool)
1736        .await
1737        .map_err(|e| format!("Failed to create board decision: {}", e))?;
1738
1739        Ok(decision_id)
1740    }
1741
1742    /// Seed realistic data for load testing (optimized for 1 vCPU / 2GB RAM)
1743    /// Generates: 3 orgs, ~23 buildings, ~190 units, ~127 owners, ~60 expenses
1744    pub async fn seed_realistic_data(&self) -> Result<String, String> {
1745        log::info!("🌱 Starting realistic data seeding...");
1746
1747        // Check if data already exists
1748        let existing_orgs = sqlx::query("SELECT COUNT(*) as count FROM organizations")
1749            .fetch_one(&self.pool)
1750            .await
1751            .map_err(|e| format!("Failed to count organizations: {}", e))?;
1752
1753        let count: i64 = existing_orgs
1754            .try_get("count")
1755            .map_err(|e| format!("Failed to get count: {}", e))?;
1756        if count > 0 {
1757            return Err("Data already exists. Please clear the database first.".to_string());
1758        }
1759
1760        let mut rng = rand::rng();
1761
1762        // Belgian cities for variety
1763        let cities = [
1764            "Bruxelles",
1765            "Anvers",
1766            "Gand",
1767            "Charleroi",
1768            "Liège",
1769            "Bruges",
1770            "Namur",
1771            "Louvain",
1772        ];
1773        let street_types = ["Rue", "Avenue", "Boulevard", "Place", "Chaussée"];
1774        let street_names = [
1775            "des Fleurs",
1776            "du Parc",
1777            "de la Gare",
1778            "Royale",
1779            "de l'Église",
1780            "du Commerce",
1781            "de la Liberté",
1782            "des Arts",
1783            "Victor Hugo",
1784            "Louise",
1785        ];
1786
1787        // Create 3 organizations with different sizes
1788        let org_configs = [
1789            ("Petite Copropriété SPRL", "small", 5, 30), // 5 buildings, ~30 units
1790            ("Copropriété Moyenne SA", "medium", 8, 60), // 8 buildings, ~60 units
1791            ("Grande Résidence NV", "large", 10, 100),   // 10 buildings, ~100 units
1792        ];
1793
1794        let mut total_buildings = 0;
1795        let mut total_units = 0;
1796        let mut total_owners = 0;
1797        let mut total_expenses = 0;
1798
1799        for (idx, (org_name, size, num_buildings, target_units)) in org_configs.iter().enumerate() {
1800            let org_id = Uuid::new_v4();
1801            let now = Utc::now();
1802
1803            log::info!(
1804                "📍 Organization {}: {} ({} buildings, ~{} units)",
1805                idx + 1,
1806                org_name,
1807                num_buildings,
1808                target_units
1809            );
1810
1811            // Create organization
1812            sqlx::query(
1813                "INSERT INTO organizations (id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at, updated_at)
1814                 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)"
1815            )
1816            .bind(org_id)
1817            .bind(*org_name)
1818            .bind(format!("{}-{}", size, idx))
1819            .bind(format!("contact@{}.be", size))
1820            .bind(format!("+32 2 {} {} {}", rng.random_range(100..999), rng.random_range(10..99), rng.random_range(10..99)))
1821            .bind(if *size == "large" { "enterprise" } else if *size == "medium" { "professional" } else { "starter" })
1822            .bind(*num_buildings)
1823            .bind(if *size == "large" { 50 } else if *size == "medium" { 20 } else { 10 })
1824            .bind(true)
1825            .bind(now)
1826            .bind(now)
1827            .execute(&self.pool)
1828            .await
1829            .map_err(|e| format!("Failed to create organization: {}", e))?;
1830
1831            // Create admin user for this org
1832            let user_id = Uuid::new_v4();
1833            let password_hash = hash("admin123", DEFAULT_COST)
1834                .map_err(|e| format!("Failed to hash password: {}", e))?;
1835
1836            sqlx::query(
1837                "INSERT INTO users (id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at)
1838                 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"
1839            )
1840            .bind(user_id)
1841            .bind(format!("admin@{}.be", size))
1842            .bind(&password_hash)
1843            .bind("Admin")
1844            .bind(org_name.split_whitespace().next().unwrap_or("User"))
1845            .bind("syndic")
1846            .bind(Some(org_id))
1847            .bind(true)
1848            .bind(now)
1849            .bind(now)
1850            .execute(&self.pool)
1851            .await
1852            .map_err(|e| format!("Failed to create user: {}", e))?;
1853
1854            // Create owners pool for this org
1855            let num_owners = (target_units * 2 / 3) as usize; // ~66% occupancy
1856            let mut owner_ids = Vec::new();
1857
1858            for o in 0..num_owners {
1859                let owner_id = Uuid::new_v4();
1860
1861                // Use faker for realistic Belgian data
1862                let first_name: String = FirstName().fake();
1863                let last_name: String = LastName().fake();
1864                let street: String = StreetName().fake();
1865                let city_idx = rng.random_range(0..cities.len());
1866                let owner_city = cities[city_idx];
1867
1868                sqlx::query(
1869                    "INSERT INTO owners (id, organization_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at)
1870                     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)"
1871                )
1872                .bind(owner_id)
1873                .bind(org_id)
1874                .bind(&first_name)
1875                .bind(&last_name)
1876                .bind(format!("{}. {}{}@{}.be", first_name.chars().next().unwrap_or('x'), last_name.to_lowercase(), o + 1, size))
1877                .bind(format!("+32 {} {} {} {}",
1878                    if rng.random_bool(0.5) { "2" } else { "4" },
1879                    rng.random_range(100..999),
1880                    rng.random_range(10..99),
1881                    rng.random_range(10..99)
1882                ))
1883                .bind(format!("{} {}", street, rng.random_range(1..200)))
1884                .bind(owner_city)
1885                .bind(format!("{}", rng.random_range(1000..9999)))
1886                .bind("Belgium")
1887                .bind(now)
1888                .bind(now)
1889                .execute(&self.pool)
1890                .await
1891                .map_err(|e| format!("Failed to create owner: {}", e))?;
1892
1893                owner_ids.push(owner_id);
1894            }
1895
1896            total_owners += num_owners;
1897
1898            // Create buildings for this org
1899            let units_per_building = target_units / num_buildings;
1900            let mut org_units = 0;
1901
1902            for b in 0..*num_buildings {
1903                let building_id = Uuid::new_v4();
1904                let city = cities[rng.random_range(0..cities.len())];
1905                let street_type = street_types[rng.random_range(0..street_types.len())];
1906                let street_name = street_names[rng.random_range(0..street_names.len())];
1907                let building_name = format!("Résidence {}", street_name);
1908
1909                sqlx::query(
1910                    "INSERT INTO buildings (id, organization_id, name, address, city, postal_code, country, total_units, construction_year, created_at, updated_at)
1911                     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)"
1912                )
1913                .bind(building_id)
1914                .bind(org_id)
1915                .bind(&building_name)
1916                .bind(format!("{} {} {}", street_type, street_name, rng.random_range(1..200)))
1917                .bind(city)
1918                .bind(format!("{}", rng.random_range(1000..9999)))
1919                .bind("Belgium")
1920                .bind(units_per_building)
1921                .bind(rng.random_range(1960..2024))
1922                .bind(now)
1923                .bind(now)
1924                .execute(&self.pool)
1925                .await
1926                .map_err(|e| format!("Failed to create building: {}", e))?;
1927
1928                // Create units for this building
1929                let units_this_building = if b == num_buildings - 1 {
1930                    // Last building gets remainder
1931                    target_units - org_units
1932                } else {
1933                    units_per_building
1934                };
1935
1936                for u in 0..units_this_building {
1937                    let floor = u / 4; // 4 units per floor
1938                    let unit_number = format!("{}.{}", floor, (u % 4) + 1);
1939
1940                    // 66% chance to have an owner
1941                    let owner_id = if rng.random_bool(0.66) && !owner_ids.is_empty() {
1942                        Some(owner_ids[rng.random_range(0..owner_ids.len())])
1943                    } else {
1944                        None
1945                    };
1946
1947                    // Valid unit_type ENUM values: apartment, parking, cellar, commercial, other
1948                    let unit_types = ["apartment", "apartment", "apartment", "parking", "cellar"];
1949                    let unit_type = unit_types[rng.random_range(0..unit_types.len())];
1950
1951                    sqlx::query(
1952                        "INSERT INTO units (id, organization_id, building_id, unit_number, unit_type, floor, surface_area, quota, owner_id, created_at, updated_at)
1953                         VALUES ($1, $2, $3, $4, $5::unit_type, $6, $7, $8, $9, $10, $11)"
1954                    )
1955                    .bind(Uuid::new_v4())
1956                    .bind(org_id)
1957                    .bind(building_id)
1958                    .bind(&unit_number)
1959                    .bind(unit_type)
1960                    .bind(floor)
1961                    .bind(rng.random_range(45.0..150.0))
1962                    .bind(rng.random_range(50..200) as i32)
1963                    .bind(owner_id)
1964                    .bind(now)
1965                    .bind(now)
1966                    .execute(&self.pool)
1967                    .await
1968                    .map_err(|e| format!("Failed to create unit: {}", e))?;
1969                }
1970
1971                org_units += units_this_building;
1972
1973                // Check if expenses already exist for this building (idempotency)
1974                let existing_expenses: (i64,) =
1975                    sqlx::query_as("SELECT COUNT(*) FROM expenses WHERE building_id = $1")
1976                        .bind(building_id)
1977                        .fetch_one(&self.pool)
1978                        .await
1979                        .map_err(|e| format!("Failed to check existing expenses: {}", e))?;
1980
1981                // Only create random expenses if none exist yet
1982                if existing_expenses.0 == 0 {
1983                    // Create 2-3 expenses per building
1984                    let num_expenses = rng.random_range(2..=3);
1985                    let expense_types = [
1986                        ("Entretien ascenseur", 450.0, 800.0),
1987                        ("Nettoyage parties communes", 300.0, 600.0),
1988                        ("Chauffage collectif", 1500.0, 3000.0),
1989                        ("Assurance immeuble", 800.0, 1500.0),
1990                        ("Travaux façade", 5000.0, 15000.0),
1991                    ];
1992
1993                    for _ in 0..num_expenses {
1994                        let (desc, min_amount, max_amount) =
1995                            expense_types[rng.random_range(0..expense_types.len())];
1996                        let amount = rng.random_range(min_amount..max_amount);
1997                        let days_ago = rng.random_range(0..90);
1998                        let expense_date = Utc::now() - chrono::Duration::days(days_ago);
1999
2000                        // Valid expense_category ENUM: maintenance, repairs, insurance, utilities, cleaning, administration, works, other
2001                        let categories = [
2002                            "maintenance",
2003                            "repairs",
2004                            "insurance",
2005                            "utilities",
2006                            "cleaning",
2007                            "administration",
2008                            "works",
2009                        ];
2010                        let category = categories[rng.random_range(0..categories.len())];
2011
2012                        // Valid payment_status ENUM: pending, paid, overdue, cancelled
2013                        let payment_status = if rng.random_bool(0.7) {
2014                            "paid"
2015                        } else {
2016                            "pending"
2017                        };
2018
2019                        // Set approval_status based on payment_status
2020                        let approval_status = if payment_status == "paid" {
2021                            "approved" // If already paid, it must be approved
2022                        } else {
2023                            "draft" // Otherwise, start as draft
2024                        };
2025
2026                        // Set paid_date if already paid
2027                        let paid_date = if payment_status == "paid" {
2028                            Some(expense_date)
2029                        } else {
2030                            None
2031                        };
2032
2033                        sqlx::query(
2034                        "INSERT INTO expenses (id, organization_id, building_id, category, description, amount, expense_date, payment_status, approval_status, paid_date, created_at, updated_at)
2035                         VALUES ($1, $2, $3, $4::expense_category, $5, $6, $7, $8::payment_status, $9::approval_status, $10, $11, $12)"
2036                    )
2037                    .bind(Uuid::new_v4())
2038                    .bind(org_id)
2039                    .bind(building_id)
2040                    .bind(category)
2041                    .bind(desc)
2042                    .bind(amount)
2043                    .bind(expense_date)
2044                    .bind(payment_status)
2045                    .bind(approval_status)
2046                    .bind(paid_date)
2047                    .bind(now)
2048                    .bind(now)
2049                    .execute(&self.pool)
2050                    .await
2051                    .map_err(|e| format!("Failed to create expense: {}", e))?;
2052
2053                        total_expenses += 1;
2054                    }
2055                } // End if existing_expenses.0 == 0
2056            }
2057
2058            total_buildings += num_buildings;
2059            total_units += org_units as usize;
2060
2061            log::info!(
2062                "  ✅ Created {} buildings, {} units, {} owners",
2063                num_buildings,
2064                org_units,
2065                num_owners
2066            );
2067        }
2068
2069        Ok(format!(
2070            "✅ Realistic seed data created successfully!\n\
2071             Total: {} orgs, {} buildings, {} units, {} owners, {} expenses\n\
2072             \nTest credentials:\n\
2073             - Small org:  admin@small.be / admin123\n\
2074             - Medium org: admin@medium.be / admin123\n\
2075             - Large org:  admin@large.be / admin123",
2076            org_configs.len(),
2077            total_buildings,
2078            total_units,
2079            total_owners,
2080            total_expenses
2081        ))
2082    }
2083
2084    /// Seed Belgian PCMN accounts for an organization
2085    async fn seed_pcmn_accounts(&self, organization_id: Uuid) -> Result<(), String> {
2086        // Call the existing account seeding endpoint logic
2087        // We'll seed the essential accounts for demo purposes
2088        let accounts = vec![
2089            // Class 6: Charges (Expenses)
2090            ("6100", "Charges courantes", "EXPENSE"),
2091            ("6110", "Entretien et réparations", "EXPENSE"),
2092            ("6120", "Personnel", "EXPENSE"),
2093            ("6130", "Services extérieurs", "EXPENSE"),
2094            ("6140", "Honoraires et commissions", "EXPENSE"),
2095            ("6150", "Assurances", "EXPENSE"),
2096            ("6200", "Travaux extraordinaires", "EXPENSE"),
2097            // Class 7: Produits (Revenue)
2098            ("7000", "Appels de fonds ordinaires", "REVENUE"),
2099            ("7100", "Appels de fonds extraordinaires", "REVENUE"),
2100            ("7200", "Autres produits", "REVENUE"),
2101            // Class 4: Créances et dettes (Assets/Liabilities)
2102            ("4000", "Copropriétaires débiteurs", "ASSET"),
2103            ("4110", "TVA à récupérer", "ASSET"),
2104            ("4400", "Fournisseurs", "LIABILITY"),
2105            ("4500", "TVA à payer", "LIABILITY"),
2106            // Class 5: Trésorerie (Assets)
2107            ("5500", "Banque compte courant", "ASSET"),
2108            ("5700", "Caisse", "ASSET"),
2109        ];
2110
2111        let now = Utc::now();
2112
2113        for (code, label, account_type_str) in accounts {
2114            sqlx::query(
2115                r#"
2116                INSERT INTO accounts (id, code, label, parent_code, account_type, direct_use, organization_id, created_at, updated_at)
2117                VALUES ($1, $2, $3, $4, $5::account_type, $6, $7, $8, $9)
2118                ON CONFLICT (code, organization_id) DO NOTHING
2119                "#
2120            )
2121            .bind(Uuid::new_v4())
2122            .bind(code)
2123            .bind(label)
2124            .bind(None::<String>) // parent_code
2125            .bind(account_type_str)
2126            .bind(true) // direct_use
2127            .bind(organization_id)
2128            .bind(now)
2129            .bind(now)
2130            .execute(&self.pool)
2131            .await
2132            .map_err(|e| format!("Failed to seed account {}: {}", code, e))?;
2133        }
2134
2135        Ok(())
2136    }
2137
2138    /// Create a demo expense with VAT calculation
2139    #[allow(clippy::too_many_arguments)]
2140    async fn create_demo_expense_with_vat(
2141        &self,
2142        building_id: Uuid,
2143        organization_id: Uuid,
2144        description: &str,
2145        amount_excl_vat: f64,
2146        vat_rate: f64,
2147        expense_date: &str,
2148        due_date: &str,
2149        category: &str,
2150        payment_status: &str,
2151        supplier: Option<&str>,
2152        invoice_number: Option<&str>,
2153        account_code: Option<&str>,
2154    ) -> Result<Uuid, String> {
2155        let expense_id = Uuid::new_v4();
2156        let now = Utc::now();
2157
2158        // Calculate VAT and total
2159        let vat_amount = (amount_excl_vat * vat_rate / 100.0 * 100.0).round() / 100.0;
2160        let amount = amount_excl_vat + vat_amount;
2161
2162        let expense_date_parsed =
2163            chrono::DateTime::parse_from_rfc3339(&format!("{}T00:00:00Z", expense_date))
2164                .map_err(|e| format!("Failed to parse expense_date: {}", e))?
2165                .with_timezone(&Utc);
2166
2167        let due_date_parsed =
2168            chrono::DateTime::parse_from_rfc3339(&format!("{}T00:00:00Z", due_date))
2169                .map_err(|e| format!("Failed to parse due_date: {}", e))?
2170                .with_timezone(&Utc);
2171
2172        // Set paid_date if payment_status is "paid"
2173        let paid_date = if payment_status == "paid" {
2174            Some(expense_date_parsed) // Use expense_date as paid_date
2175        } else {
2176            None
2177        };
2178
2179        // Check if expense already exists (idempotency)
2180        let existing: Option<(Uuid,)> =
2181            sqlx::query_as("SELECT id FROM expenses WHERE description = $1 AND building_id = $2")
2182                .bind(description)
2183                .bind(building_id)
2184                .fetch_optional(&self.pool)
2185                .await
2186                .map_err(|e| format!("Failed to check existing expense: {}", e))?;
2187
2188        let expense_id = if let Some((existing_id,)) = existing {
2189            // Update existing expense
2190            sqlx::query(
2191                r#"
2192                UPDATE expenses SET
2193                    category = $1::expense_category,
2194                    amount = $2,
2195                    amount_excl_vat = $3,
2196                    vat_rate = $4,
2197                    expense_date = $5,
2198                    due_date = $6,
2199                    payment_status = $7::payment_status,
2200                    paid_date = $8,
2201                    approval_status = $9::approval_status,
2202                    supplier = $10,
2203                    invoice_number = $11,
2204                    account_code = $12,
2205                    updated_at = $13
2206                WHERE id = $14
2207                "#,
2208            )
2209            .bind(category)
2210            .bind(amount)
2211            .bind(amount_excl_vat)
2212            .bind(vat_rate)
2213            .bind(expense_date_parsed)
2214            .bind(due_date_parsed)
2215            .bind(payment_status)
2216            .bind(paid_date)
2217            .bind("approved")
2218            .bind(supplier)
2219            .bind(invoice_number)
2220            .bind(account_code)
2221            .bind(now)
2222            .bind(existing_id)
2223            .execute(&self.pool)
2224            .await
2225            .map_err(|e| format!("Failed to update expense: {}", e))?;
2226            existing_id
2227        } else {
2228            // Insert new expense
2229            sqlx::query(
2230                r#"
2231                INSERT INTO expenses (
2232                    id, organization_id, building_id, category, description,
2233                    amount, amount_excl_vat, vat_rate, expense_date, due_date,
2234                    payment_status, paid_date, approval_status, supplier, invoice_number,
2235                    account_code, created_at, updated_at
2236                )
2237                VALUES ($1, $2, $3, $4::expense_category, $5, $6, $7, $8, $9, $10, $11::payment_status, $12, $13::approval_status, $14, $15, $16, $17, $18)
2238                "#
2239            )
2240            .bind(expense_id)
2241            .bind(organization_id)
2242            .bind(building_id)
2243            .bind(category)
2244            .bind(description)
2245            .bind(amount)
2246            .bind(amount_excl_vat)
2247            .bind(vat_rate)
2248            .bind(expense_date_parsed)
2249            .bind(due_date_parsed)
2250            .bind(payment_status)
2251            .bind(paid_date)
2252            .bind("approved")
2253            .bind(supplier)
2254            .bind(invoice_number)
2255            .bind(account_code)
2256            .bind(now)
2257            .bind(now)
2258            .execute(&self.pool)
2259            .await
2260            .map_err(|e| format!("Failed to create expense with VAT: {}", e))?;
2261            expense_id
2262        };
2263
2264        // Generate journal entry for this expense (double-entry bookkeeping)
2265        if let Some(acc_code) = account_code {
2266            self.generate_journal_entry_for_expense(
2267                expense_id,
2268                organization_id,
2269                building_id,
2270                description,
2271                amount_excl_vat,
2272                vat_rate,
2273                amount,
2274                expense_date_parsed,
2275                acc_code,
2276                supplier,
2277                invoice_number,
2278            )
2279            .await?;
2280        }
2281
2282        Ok(expense_id)
2283    }
2284
2285    /// Generate journal entry for an expense (double-entry bookkeeping)
2286    ///
2287    /// This creates the accounting entries following Belgian PCMN:
2288    /// - Debit: Expense account (class 6)
2289    /// - Debit: VAT recoverable (4110)
2290    /// - Credit: Supplier account (4400)
2291    #[allow(clippy::too_many_arguments)]
2292    async fn generate_journal_entry_for_expense(
2293        &self,
2294        expense_id: Uuid,
2295        organization_id: Uuid,
2296        _building_id: Uuid,
2297        description: &str,
2298        amount_excl_vat: f64,
2299        vat_rate: f64,
2300        total_amount: f64,
2301        expense_date: chrono::DateTime<Utc>,
2302        account_code: &str,
2303        supplier: Option<&str>,
2304        invoice_number: Option<&str>,
2305    ) -> Result<(), String> {
2306        let journal_entry_id = Uuid::new_v4();
2307        let now = Utc::now();
2308
2309        // Calculate VAT amount
2310        let vat_amount = total_amount - amount_excl_vat;
2311
2312        // Start a transaction - the deferred trigger will only check at COMMIT
2313        let mut tx = self
2314            .pool
2315            .begin()
2316            .await
2317            .map_err(|e| format!("Failed to begin transaction: {}", e))?;
2318
2319        // Insert journal entry header
2320        sqlx::query!(
2321            r#"
2322            INSERT INTO journal_entries (
2323                id, organization_id, entry_date, description,
2324                document_ref, expense_id, created_at, updated_at
2325            )
2326            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
2327            "#,
2328            journal_entry_id,
2329            organization_id,
2330            expense_date,
2331            format!("{} - {}", description, supplier.unwrap_or("Fournisseur")),
2332            invoice_number,
2333            expense_id,
2334            now,
2335            now
2336        )
2337        .execute(&mut *tx)
2338        .await
2339        .map_err(|e| format!("Failed to create journal entry: {}", e))?;
2340
2341        // Line 1: Debit expense account (class 6)
2342        sqlx::query!(
2343            r#"
2344            INSERT INTO journal_entry_lines (
2345                journal_entry_id, organization_id, account_code,
2346                debit, credit, description
2347            )
2348            VALUES ($1, $2, $3, $4, $5, $6)
2349            "#,
2350            journal_entry_id,
2351            organization_id,
2352            account_code,
2353            rust_decimal::Decimal::from_f64_retain(amount_excl_vat).unwrap_or_default(),
2354            rust_decimal::Decimal::from_f64_retain(0.0).unwrap_or_default(),
2355            format!("Dépense: {}", description)
2356        )
2357        .execute(&mut *tx)
2358        .await
2359        .map_err(|e| format!("Failed to create expense debit line: {}", e))?;
2360
2361        // Line 2: Debit VAT recoverable (4110) if VAT > 0
2362        if vat_amount > 0.01 {
2363            sqlx::query!(
2364                r#"
2365                INSERT INTO journal_entry_lines (
2366                    journal_entry_id, organization_id, account_code,
2367                    debit, credit, description
2368                )
2369                VALUES ($1, $2, $3, $4, $5, $6)
2370                "#,
2371                journal_entry_id,
2372                organization_id,
2373                "4110", // VAT Recoverable account
2374                rust_decimal::Decimal::from_f64_retain(vat_amount).unwrap_or_default(),
2375                rust_decimal::Decimal::from_f64_retain(0.0).unwrap_or_default(),
2376                format!("TVA récupérable {}%", vat_rate)
2377            )
2378            .execute(&mut *tx)
2379            .await
2380            .map_err(|e| format!("Failed to create VAT debit line: {}", e))?;
2381        }
2382
2383        // Line 3: Credit supplier account (4400)
2384        sqlx::query!(
2385            r#"
2386            INSERT INTO journal_entry_lines (
2387                journal_entry_id, organization_id, account_code,
2388                debit, credit, description
2389            )
2390            VALUES ($1, $2, $3, $4, $5, $6)
2391            "#,
2392            journal_entry_id,
2393            organization_id,
2394            "4400", // Suppliers account
2395            rust_decimal::Decimal::from_f64_retain(0.0).unwrap_or_default(),
2396            rust_decimal::Decimal::from_f64_retain(total_amount).unwrap_or_default(),
2397            supplier.map(|s| format!("Fournisseur: {}", s))
2398        )
2399        .execute(&mut *tx)
2400        .await
2401        .map_err(|e| format!("Failed to create supplier credit line: {}", e))?;
2402
2403        // Commit transaction - trigger will validate balance here
2404        tx.commit()
2405            .await
2406            .map_err(|e| format!("Failed to commit journal entry transaction: {}", e))?;
2407
2408        Ok(())
2409    }
2410
2411    /// Create demo charge distributions for an expense
2412    async fn create_demo_distributions(
2413        &self,
2414        expense_id: Uuid,
2415        _organization_id: Uuid,
2416    ) -> Result<(), String> {
2417        // Get all units for the expense's building
2418        let expense_row = sqlx::query!(
2419            "SELECT building_id, amount FROM expenses WHERE id = $1",
2420            expense_id
2421        )
2422        .fetch_one(&self.pool)
2423        .await
2424        .map_err(|e| format!("Failed to fetch expense: {}", e))?;
2425
2426        let building_id = expense_row.building_id;
2427        let total_amount = expense_row.amount;
2428
2429        // Get all units with their quotas (NOT unit_owners - one record per unit)
2430        let units = sqlx::query!(
2431            r#"
2432            SELECT u.id as unit_id, u.quota
2433            FROM units u
2434            WHERE u.building_id = $1
2435            "#,
2436            building_id
2437        )
2438        .fetch_all(&self.pool)
2439        .await
2440        .map_err(|e| format!("Failed to fetch units: {}", e))?;
2441
2442        if units.is_empty() {
2443            return Ok(()); // No units to distribute to
2444        }
2445
2446        // Calculate total quotas for the building
2447        let total_quota: f64 = units.iter().map(|u| u.quota).sum();
2448
2449        let now = Utc::now();
2450
2451        // Create ONE distribution per unit (not per owner)
2452        // The primary owner will be responsible for collecting from co-owners
2453        for unit in units {
2454            // Get the primary contact owner for this unit
2455            let primary_owner = sqlx::query!(
2456                r#"
2457                SELECT owner_id
2458                FROM unit_owners
2459                WHERE unit_id = $1 AND end_date IS NULL AND is_primary_contact = true
2460                ORDER BY created_at ASC
2461                LIMIT 1
2462                "#,
2463                unit.unit_id
2464            )
2465            .fetch_optional(&self.pool)
2466            .await
2467            .map_err(|e| format!("Failed to fetch primary owner: {}", e))?;
2468
2469            // Skip if no owner found for this unit
2470            let owner_id = match primary_owner {
2471                Some(owner) => owner.owner_id,
2472                None => continue, // Skip this unit if no owner
2473            };
2474
2475            let quota_percentage = if total_quota > 0.0 {
2476                unit.quota / total_quota
2477            } else {
2478                0.0
2479            };
2480
2481            let amount_due = if total_quota > 0.0 {
2482                (quota_percentage * total_amount * 100.0).round() / 100.0
2483            } else {
2484                0.0
2485            };
2486
2487            sqlx::query(
2488                r#"
2489                INSERT INTO charge_distributions (
2490                    id, expense_id, unit_id, owner_id,
2491                    quota_percentage, amount_due, created_at
2492                )
2493                VALUES ($1, $2, $3, $4, $5, $6, $7)
2494                "#,
2495            )
2496            .bind(Uuid::new_v4())
2497            .bind(expense_id)
2498            .bind(unit.unit_id)
2499            .bind(owner_id)
2500            .bind(quota_percentage)
2501            .bind(amount_due)
2502            .bind(now)
2503            .execute(&self.pool)
2504            .await
2505            .map_err(|e| format!("Failed to create charge distribution: {}", e))?;
2506        }
2507
2508        Ok(())
2509    }
2510
2511    /// Create a demo owner contribution (revenue)
2512    #[allow(clippy::too_many_arguments)]
2513    async fn create_demo_owner_contribution(
2514        &self,
2515        organization_id: Uuid,
2516        owner_id: Uuid,
2517        unit_id: Option<Uuid>,
2518        description: &str,
2519        amount: f64,
2520        contribution_type: &str,
2521        contribution_date: &str,
2522        payment_status: &str,
2523        payment_date: Option<&str>,
2524        account_code: Option<&str>,
2525    ) -> Result<Uuid, String> {
2526        let contribution_id = Uuid::new_v4();
2527        let contribution_date = NaiveDate::parse_from_str(contribution_date, "%Y-%m-%d")
2528            .map_err(|e| format!("Invalid contribution date: {}", e))?
2529            .and_hms_opt(10, 0, 0)
2530            .ok_or("Invalid contribution time")?
2531            .and_local_timezone(Utc)
2532            .unwrap();
2533
2534        let payment_date_tz = payment_date
2535            .map(|date_str| {
2536                NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
2537                    .map_err(|e| format!("Invalid payment date: {}", e))
2538                    .and_then(|date| {
2539                        date.and_hms_opt(10, 0, 0)
2540                            .ok_or("Invalid payment time".to_string())
2541                    })
2542                    .map(|dt| dt.and_local_timezone(Utc).unwrap())
2543            })
2544            .transpose()?;
2545
2546        let payment_method = if payment_status == "paid" {
2547            Some("bank_transfer")
2548        } else {
2549            None
2550        };
2551
2552        let now = Utc::now();
2553
2554        sqlx::query(
2555            r#"
2556            INSERT INTO owner_contributions (
2557                id, organization_id, owner_id, unit_id,
2558                description, amount, account_code,
2559                contribution_type, contribution_date, payment_date,
2560                payment_method, payment_status,
2561                created_at, updated_at
2562            )
2563            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
2564            "#,
2565        )
2566        .bind(contribution_id)
2567        .bind(organization_id)
2568        .bind(owner_id)
2569        .bind(unit_id)
2570        .bind(description)
2571        .bind(amount)
2572        .bind(account_code)
2573        .bind(contribution_type)
2574        .bind(contribution_date)
2575        .bind(payment_date_tz)
2576        .bind(payment_method)
2577        .bind(payment_status)
2578        .bind(now)
2579        .bind(now)
2580        .execute(&self.pool)
2581        .await
2582        .map_err(|e| format!("Failed to create owner contribution: {}", e))?;
2583
2584        // Generate journal entry for this contribution (double-entry bookkeeping)
2585        if let Some(acc_code) = account_code {
2586            self.generate_journal_entry_for_contribution(
2587                contribution_id,
2588                organization_id,
2589                description,
2590                amount,
2591                contribution_date,
2592                acc_code,
2593            )
2594            .await?;
2595        }
2596
2597        Ok(contribution_id)
2598    }
2599
2600    /// Generate journal entry for an owner contribution (double-entry bookkeeping)
2601    ///
2602    /// This creates the accounting entries following Belgian PCMN:
2603    /// - Debit: Owner receivables (4000) - Money owed by owner
2604    /// - Credit: Revenue account (class 7) - Income for ACP
2605    async fn generate_journal_entry_for_contribution(
2606        &self,
2607        contribution_id: Uuid,
2608        organization_id: Uuid,
2609        description: &str,
2610        amount: f64,
2611        contribution_date: chrono::DateTime<Utc>,
2612        account_code: &str,
2613    ) -> Result<(), String> {
2614        let journal_entry_id = Uuid::new_v4();
2615        let now = Utc::now();
2616
2617        // Start a transaction with deferred constraints
2618        let mut tx = self
2619            .pool
2620            .begin()
2621            .await
2622            .map_err(|e| format!("Failed to begin transaction: {}", e))?;
2623
2624        // Set constraints to deferred for this transaction
2625        sqlx::query("SET CONSTRAINTS ALL DEFERRED")
2626            .execute(&mut *tx)
2627            .await
2628            .map_err(|e| format!("Failed to defer constraints: {}", e))?;
2629
2630        // Create journal entry header
2631        sqlx::query(
2632            r#"
2633            INSERT INTO journal_entries (
2634                id, organization_id, entry_date, description,
2635                contribution_id, created_at, updated_at
2636            )
2637            VALUES ($1, $2, $3, $4, $5, $6, $7)
2638            "#,
2639        )
2640        .bind(journal_entry_id)
2641        .bind(organization_id)
2642        .bind(contribution_date)
2643        .bind(description)
2644        .bind(contribution_id)
2645        .bind(now)
2646        .bind(now)
2647        .execute(&mut *tx)
2648        .await
2649        .map_err(|e| format!("Failed to create journal entry: {}", e))?;
2650
2651        // Line 1: DEBIT - Owner receivables (4000 = Copropriétaires débiteurs)
2652        sqlx::query(
2653            r#"
2654            INSERT INTO journal_entry_lines (
2655                id, journal_entry_id, organization_id, account_code,
2656                description, debit, credit, created_at
2657            )
2658            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
2659            "#,
2660        )
2661        .bind(Uuid::new_v4())
2662        .bind(journal_entry_id)
2663        .bind(organization_id)
2664        .bind("4000") // Owner receivables
2665        .bind(format!("Créance - {}", description))
2666        .bind(amount) // Debit
2667        .bind(0.0) // Credit
2668        .bind(now)
2669        .execute(&mut *tx)
2670        .await
2671        .map_err(|e| format!("Failed to create debit line (4000): {}", e))?;
2672
2673        // Line 2: CREDIT - Revenue account (class 7)
2674        sqlx::query(
2675            r#"
2676            INSERT INTO journal_entry_lines (
2677                id, journal_entry_id, organization_id, account_code,
2678                description, debit, credit, created_at
2679            )
2680            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
2681            "#,
2682        )
2683        .bind(Uuid::new_v4())
2684        .bind(journal_entry_id)
2685        .bind(organization_id)
2686        .bind(account_code) // Revenue account (e.g., 7000)
2687        .bind(format!("Produit - {}", description))
2688        .bind(0.0) // Debit
2689        .bind(amount) // Credit
2690        .bind(now)
2691        .execute(&mut *tx)
2692        .await
2693        .map_err(|e| format!("Failed to create credit line ({}): {}", account_code, e))?;
2694
2695        // Commit transaction (constraints will be checked here)
2696        tx.commit()
2697            .await
2698            .map_err(|e| format!("Failed to commit transaction: {}", e))?;
2699
2700        Ok(())
2701    }
2702
2703    /// Create a demo payment reminder
2704    #[allow(clippy::too_many_arguments)]
2705    async fn create_demo_payment_reminder(
2706        &self,
2707        expense_id: Uuid,
2708        owner_id: Uuid,
2709        organization_id: Uuid,
2710        reminder_level: &str,
2711        days_overdue: i64,
2712    ) -> Result<Uuid, String> {
2713        let reminder_id = Uuid::new_v4();
2714        let now = Utc::now();
2715
2716        // Get expense amount and due date
2717        let expense = sqlx::query!(
2718            "SELECT amount, due_date FROM expenses WHERE id = $1",
2719            expense_id
2720        )
2721        .fetch_one(&self.pool)
2722        .await
2723        .map_err(|e| format!("Failed to fetch expense: {}", e))?;
2724
2725        let amount_owed = expense.amount;
2726        let due_date = expense
2727            .due_date
2728            .expect("Due date required for payment reminder");
2729
2730        // Calculate penalty (8% annual rate)
2731        let penalty_amount = if days_overdue > 0 {
2732            let yearly_penalty = amount_owed * 0.08;
2733            let daily_penalty = yearly_penalty / 365.0;
2734            ((daily_penalty * days_overdue as f64) * 100.0).round() / 100.0
2735        } else {
2736            0.0
2737        };
2738
2739        let total_amount = amount_owed + penalty_amount;
2740        let sent_date = now - chrono::Duration::days(5); // Sent 5 days ago
2741
2742        sqlx::query(
2743            r#"
2744            INSERT INTO payment_reminders (
2745                id, organization_id, expense_id, owner_id,
2746                level, status, amount_owed, penalty_amount, total_amount,
2747                due_date, days_overdue, delivery_method, sent_date,
2748                created_at, updated_at
2749            )
2750            VALUES ($1, $2, $3, $4, $5::reminder_level, $6::reminder_status, $7, $8, $9, $10, $11, $12::delivery_method, $13, $14, $15)
2751            "#
2752        )
2753        .bind(reminder_id)
2754        .bind(organization_id)
2755        .bind(expense_id)
2756        .bind(owner_id)
2757        .bind(reminder_level) // FirstReminder, SecondReminder, etc.
2758        .bind("Sent") // status
2759        .bind(amount_owed)
2760        .bind(penalty_amount)
2761        .bind(total_amount)
2762        .bind(due_date)
2763        .bind(days_overdue as i32)
2764        .bind("Email") // delivery_method
2765        .bind(sent_date)
2766        .bind(now)
2767        .bind(now)
2768        .execute(&self.pool)
2769        .await
2770        .map_err(|e| format!("Failed to create payment reminder: {}", e))?;
2771
2772        Ok(reminder_id)
2773    }
2774
2775    /// Clear all data (DANGEROUS - use with caution!)
2776    pub async fn clear_demo_data(&self) -> Result<String, String> {
2777        log::warn!("⚠️  Clearing seed data only (preserving production data)...");
2778
2779        // Get seed organization IDs
2780        let seed_org_ids: Vec<Uuid> =
2781            sqlx::query_scalar!("SELECT id FROM organizations WHERE is_seed_data = true")
2782                .fetch_all(&self.pool)
2783                .await
2784                .map_err(|e| format!("Failed to fetch seed organizations: {}", e))?;
2785
2786        if seed_org_ids.is_empty() {
2787            return Ok("ℹ️  No seed data found to clear.".to_string());
2788        }
2789
2790        log::info!("Found {} seed organizations to clean", seed_org_ids.len());
2791
2792        // Delete in correct order due to foreign key constraints
2793        // 1. Board decisions (reference board_members and meetings)
2794        sqlx::query!(
2795            "DELETE FROM board_decisions WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2796            &seed_org_ids
2797        )
2798        .execute(&self.pool)
2799        .await
2800        .map_err(|e| format!("Failed to delete board_decisions: {}", e))?;
2801
2802        // 2. Board members (reference meetings)
2803        sqlx::query!(
2804            "DELETE FROM board_members WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2805            &seed_org_ids
2806        )
2807        .execute(&self.pool)
2808        .await
2809        .map_err(|e| format!("Failed to delete board_members: {}", e))?;
2810
2811        // 3. Payment reminders (reference expenses and owners)
2812        sqlx::query!(
2813            "DELETE FROM payment_reminders WHERE organization_id = ANY($1)",
2814            &seed_org_ids
2815        )
2816        .execute(&self.pool)
2817        .await
2818        .map_err(|e| format!("Failed to delete payment_reminders: {}", e))?;
2819
2820        // 3b. Owner contributions (revenue)
2821        sqlx::query!(
2822            "DELETE FROM owner_contributions WHERE organization_id = ANY($1)",
2823            &seed_org_ids
2824        )
2825        .execute(&self.pool)
2826        .await
2827        .map_err(|e| format!("Failed to delete owner_contributions: {}", e))?;
2828
2829        // 4. Charge distributions (reference expenses)
2830        sqlx::query(
2831            "DELETE FROM charge_distributions WHERE expense_id IN (SELECT id FROM expenses WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1)))"
2832        )
2833        .bind(&seed_org_ids)
2834        .execute(&self.pool)
2835        .await
2836        .map_err(|e| format!("Failed to delete charge_distributions: {}", e))?;
2837
2838        // 5. Invoice line items (reference expenses)
2839        sqlx::query(
2840            "DELETE FROM invoice_line_items WHERE expense_id IN (SELECT id FROM expenses WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1)))"
2841        )
2842        .bind(&seed_org_ids)
2843        .execute(&self.pool)
2844        .await
2845        .map_err(|e| format!("Failed to delete invoice_line_items: {}", e))?;
2846
2847        // 6. Documents linked to buildings or expenses
2848        sqlx::query!(
2849            "DELETE FROM documents WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2850            &seed_org_ids
2851        )
2852        .execute(&self.pool)
2853        .await
2854        .map_err(|e| format!("Failed to delete documents: {}", e))?;
2855
2856        // 7. Meetings (now safe to delete after board members)
2857        sqlx::query!(
2858            "DELETE FROM meetings WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2859            &seed_org_ids
2860        )
2861        .execute(&self.pool)
2862        .await
2863        .map_err(|e| format!("Failed to delete meetings: {}", e))?;
2864
2865        // 8. Journal entry lines (reference accounts) - MUST be deleted before accounts
2866        sqlx::query!(
2867            "DELETE FROM journal_entry_lines WHERE organization_id = ANY($1)",
2868            &seed_org_ids
2869        )
2870        .execute(&self.pool)
2871        .await
2872        .map_err(|e| format!("Failed to delete journal_entry_lines: {}", e))?;
2873
2874        // 9. Journal entries (now safe after lines are deleted)
2875        sqlx::query!(
2876            "DELETE FROM journal_entries WHERE organization_id = ANY($1)",
2877            &seed_org_ids
2878        )
2879        .execute(&self.pool)
2880        .await
2881        .map_err(|e| format!("Failed to delete journal_entries: {}", e))?;
2882
2883        // 10. Expenses (now safe to delete after distributions, line items, and journal entries)
2884        sqlx::query!(
2885            "DELETE FROM expenses WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2886            &seed_org_ids
2887        )
2888        .execute(&self.pool)
2889        .await
2890        .map_err(|e| format!("Failed to delete expenses: {}", e))?;
2891
2892        // Unit owners (junction table)
2893        sqlx::query(
2894            "DELETE FROM unit_owners WHERE unit_id IN (SELECT u.id FROM units u INNER JOIN buildings b ON u.building_id = b.id WHERE b.organization_id = ANY($1))"
2895        )
2896        .bind(&seed_org_ids)
2897        .execute(&self.pool)
2898        .await
2899        .map_err(|e| format!("Failed to delete unit_owners: {}", e))?;
2900
2901        // Units
2902        sqlx::query!(
2903            "DELETE FROM units WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2904            &seed_org_ids
2905        )
2906        .execute(&self.pool)
2907        .await
2908        .map_err(|e| format!("Failed to delete units: {}", e))?;
2909
2910        // Owners (only those linked to seed organizations through unit_owners)
2911        sqlx::query!(
2912            "DELETE FROM owners WHERE organization_id = ANY($1)",
2913            &seed_org_ids
2914        )
2915        .execute(&self.pool)
2916        .await
2917        .map_err(|e| format!("Failed to delete owners: {}", e))?;
2918
2919        // Buildings
2920        sqlx::query!(
2921            "DELETE FROM buildings WHERE organization_id = ANY($1)",
2922            &seed_org_ids
2923        )
2924        .execute(&self.pool)
2925        .await
2926        .map_err(|e| format!("Failed to delete buildings: {}", e))?;
2927
2928        // PCMN Accounts
2929        sqlx::query!(
2930            "DELETE FROM accounts WHERE organization_id = ANY($1)",
2931            &seed_org_ids
2932        )
2933        .execute(&self.pool)
2934        .await
2935        .map_err(|e| format!("Failed to delete accounts: {}", e))?;
2936
2937        // User roles (before deleting users)
2938        sqlx::query(
2939            "DELETE FROM user_roles WHERE user_id IN (SELECT id FROM users WHERE organization_id = ANY($1) AND role != 'superadmin')"
2940        )
2941        .bind(&seed_org_ids)
2942        .execute(&self.pool)
2943        .await
2944        .map_err(|e| format!("Failed to delete user_roles: {}", e))?;
2945
2946        // Users (except superadmin)
2947        sqlx::query!(
2948            "DELETE FROM users WHERE organization_id = ANY($1) AND role != 'superadmin'",
2949            &seed_org_ids
2950        )
2951        .execute(&self.pool)
2952        .await
2953        .map_err(|e| format!("Failed to delete users: {}", e))?;
2954
2955        // Finally, delete seed organizations
2956        sqlx::query!("DELETE FROM organizations WHERE is_seed_data = true")
2957            .execute(&self.pool)
2958            .await
2959            .map_err(|e| format!("Failed to delete organizations: {}", e))?;
2960
2961        log::info!("✅ Seed data cleared (production data and superadmin preserved)");
2962
2963        Ok(format!(
2964            "✅ Seed data cleared successfully! ({} organizations removed)",
2965            seed_org_ids.len()
2966        ))
2967    }
2968}
2969
2970#[cfg(test)]
2971mod tests {
2972    use super::*;
2973    use sqlx::PgPool;
2974
2975    /// Test that seed_superadmin() is idempotent - can be called multiple times without errors
2976    ///
2977    /// This test ensures production deployments can safely restart without constraint violations
2978    #[sqlx::test]
2979    async fn test_seed_superadmin_is_idempotent(pool: PgPool) -> sqlx::Result<()> {
2980        let seeder = DatabaseSeeder::new(pool.clone());
2981
2982        // First call: Create superadmin
2983        let result1 = seeder.seed_superadmin().await;
2984        assert!(result1.is_ok(), "First seed_superadmin call should succeed");
2985        let user1 = result1.unwrap();
2986        assert_eq!(user1.email, "admin@koprogo.com");
2987        assert_eq!(user1.role, UserRole::SuperAdmin);
2988
2989        // Second call: Should succeed (idempotent upsert)
2990        let result2 = seeder.seed_superadmin().await;
2991        assert!(
2992            result2.is_ok(),
2993            "Second seed_superadmin call should succeed (idempotent): {:?}",
2994            result2.err()
2995        );
2996        let user2 = result2.unwrap();
2997        assert_eq!(user2.email, "admin@koprogo.com");
2998        assert_eq!(user2.id, user1.id, "Superadmin UUID should remain the same");
2999
3000        // Third call: Should still succeed
3001        let result3 = seeder.seed_superadmin().await;
3002        assert!(
3003            result3.is_ok(),
3004            "Third seed_superadmin call should succeed (idempotent): {:?}",
3005            result3.err()
3006        );
3007
3008        // Verify only ONE primary role exists for superadmin
3009        let primary_role_count = sqlx::query_scalar::<_, i64>(
3010            r#"
3011            SELECT COUNT(*)
3012            FROM user_roles
3013            WHERE user_id = $1 AND is_primary = true
3014            "#,
3015        )
3016        .bind(user1.id)
3017        .fetch_one(&pool)
3018        .await?;
3019
3020        assert_eq!(
3021            primary_role_count, 1,
3022            "Superadmin should have exactly ONE primary role, found {}",
3023            primary_role_count
3024        );
3025
3026        // Verify the superadmin role exists in user_roles
3027        let role_count = sqlx::query_scalar::<_, i64>(
3028            r#"
3029            SELECT COUNT(*)
3030            FROM user_roles
3031            WHERE user_id = $1 AND role = 'superadmin' AND organization_id IS NULL
3032            "#,
3033        )
3034        .bind(user1.id)
3035        .fetch_one(&pool)
3036        .await?;
3037
3038        assert_eq!(
3039            role_count, 1,
3040            "Superadmin should have exactly ONE superadmin role, found {}",
3041            role_count
3042        );
3043
3044        Ok(())
3045    }
3046
3047    /// Test that seed_superadmin() handles existing user_roles correctly
3048    ///
3049    /// Ensures the UPSERT doesn't violate the idx_user_roles_primary_per_user constraint
3050    #[sqlx::test]
3051    async fn test_seed_superadmin_preserves_existing_primary_role(
3052        pool: PgPool,
3053    ) -> sqlx::Result<()> {
3054        let seeder = DatabaseSeeder::new(pool.clone());
3055
3056        // First call: Create superadmin
3057        seeder.seed_superadmin().await.unwrap();
3058
3059        // Manually check is_primary state before second call
3060        let is_primary_before = sqlx::query_scalar::<_, bool>(
3061            r#"
3062            SELECT is_primary
3063            FROM user_roles
3064            WHERE user_id = '00000000-0000-0000-0000-000000000001'
3065              AND role = 'superadmin'
3066              AND organization_id IS NULL
3067            "#,
3068        )
3069        .fetch_one(&pool)
3070        .await?;
3071
3072        assert!(
3073            is_primary_before,
3074            "Superadmin role should be primary after first seed"
3075        );
3076
3077        // Second call: Should not violate unique constraint
3078        let result = seeder.seed_superadmin().await;
3079        assert!(
3080            result.is_ok(),
3081            "Second seed should not violate idx_user_roles_primary_per_user constraint: {:?}",
3082            result.err()
3083        );
3084
3085        // Verify is_primary is still true (preserved, not updated)
3086        let is_primary_after = sqlx::query_scalar::<_, bool>(
3087            r#"
3088            SELECT is_primary
3089            FROM user_roles
3090            WHERE user_id = '00000000-0000-0000-0000-000000000001'
3091              AND role = 'superadmin'
3092              AND organization_id IS NULL
3093            "#,
3094        )
3095        .fetch_one(&pool)
3096        .await?;
3097
3098        assert!(
3099            is_primary_after,
3100            "Superadmin role should remain primary after second seed"
3101        );
3102
3103        Ok(())
3104    }
3105}