Skip to main content

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::RngExt;
8use rust_decimal::prelude::FromPrimitive;
9use rust_decimal::Decimal;
10use serde::Serialize;
11use sqlx::{PgPool, Row};
12use uuid::Uuid;
13
14/// Result struct returned by `seed_scenario_world` with all created IDs and credentials.
15#[derive(Debug, Clone, Serialize)]
16pub struct ScenarioWorldResult {
17    pub organization_id: Uuid,
18    pub building_id: Uuid,
19    pub meeting_id: Uuid,
20    pub resolution_id: Uuid,
21    pub users: Vec<ScenarioUserResult>,
22    pub owners: Vec<ScenarioOwnerResult>,
23    pub units: Vec<ScenarioUnitResult>,
24    pub building2_id: Uuid,
25    pub building2_name: String,
26    pub building2_owners: Vec<ScenarioOwnerResult>,
27    pub building2_units: Vec<ScenarioUnitResult>,
28    pub building3_id: Uuid,
29    pub building3_name: String,
30    pub building3_owners: Vec<ScenarioOwnerResult>,
31    pub building3_units: Vec<ScenarioUnitResult>,
32}
33
34#[derive(Debug, Clone, Serialize)]
35pub struct ScenarioUserResult {
36    pub user_id: Uuid,
37    pub email: String,
38    pub password: String,
39    pub role: String,
40    pub first_name: String,
41    pub last_name: String,
42}
43
44#[derive(Debug, Clone, Serialize)]
45pub struct ScenarioOwnerResult {
46    pub owner_id: Uuid,
47    pub user_id: Uuid,
48    pub first_name: String,
49    pub last_name: String,
50    pub email: String,
51}
52
53#[derive(Debug, Clone, Serialize)]
54pub struct ScenarioUnitResult {
55    pub unit_id: Uuid,
56    pub unit_number: String,
57    pub owner_id: Uuid,
58    pub tantièmes: f64,
59}
60
61pub struct DatabaseSeeder {
62    pool: PgPool,
63}
64
65impl DatabaseSeeder {
66    pub fn new(pool: PgPool) -> Self {
67        Self { pool }
68    }
69
70    /// Create or update the default superadmin user
71    pub async fn seed_superadmin(&self) -> Result<User, String> {
72        let superadmin_email = "admin@koprogo.com";
73        let superadmin_password = "admin123"; // Change in production!
74
75        // Hash password
76        let password_hash = hash(superadmin_password, DEFAULT_COST)
77            .map_err(|e| format!("Failed to hash password: {}", e))?;
78
79        let superadmin_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001")
80            .map_err(|e| format!("Failed to parse UUID: {}", e))?;
81
82        let now = Utc::now();
83
84        // Upsert superadmin (insert or update if exists)
85        sqlx::query!(
86            r#"
87            INSERT INTO users (id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at)
88            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
89            ON CONFLICT (email)
90            DO UPDATE SET
91                password_hash = EXCLUDED.password_hash,
92                updated_at = EXCLUDED.updated_at,
93                is_active = true
94            "#,
95            superadmin_id,
96            superadmin_email,
97            password_hash,
98            "Super",
99            "Admin",
100            "superadmin",
101            None::<Uuid>,
102            true,
103            now,
104            now
105        )
106        .execute(&self.pool)
107        .await
108        .map_err(|e| format!("Failed to upsert superadmin: {}", e))?;
109
110        // Upsert superadmin role (preserve if exists, create if missing)
111        // Note: Use INSERT ... ON CONFLICT DO NOTHING for idempotency
112        // The migration backfill (20250130000000) already creates the role with is_primary=true
113        sqlx::query(
114            r#"
115            INSERT INTO user_roles (id, user_id, role, organization_id, is_primary, created_at, updated_at)
116            VALUES (
117                gen_random_uuid(),
118                $1,
119                'superadmin',
120                NULL,
121                NOT EXISTS (SELECT 1 FROM user_roles WHERE user_id = $1 AND is_primary = true),
122                NOW(),
123                NOW()
124            )
125            ON CONFLICT DO NOTHING
126            "#,
127        )
128        .bind(superadmin_id)
129        .execute(&self.pool)
130        .await
131        .map_err(|e| format!("Failed to upsert superadmin role: {}", e))?;
132
133        log::info!("✅ Superadmin ready: {}", superadmin_email);
134
135        Ok(User {
136            id: superadmin_id,
137            email: superadmin_email.to_string(),
138            password_hash,
139            first_name: "Super".to_string(),
140            last_name: "Admin".to_string(),
141            role: UserRole::SuperAdmin,
142            organization_id: None,
143            is_active: true,
144            processing_restricted: false,
145            processing_restricted_at: None,
146            marketing_opt_out: false,
147            marketing_opt_out_at: None,
148            created_at: now,
149            updated_at: now,
150        })
151    }
152
153    /// Seed Belgian PCMN (Plan Comptable Minimum Normalisé) for all organizations
154    /// This ensures every organization has the base chart of accounts
155    pub async fn seed_belgian_pcmn_for_all_organizations(&self) -> Result<String, String> {
156        log::info!("🌱 Seeding Belgian PCMN for all organizations...");
157
158        // Get all organizations
159        let organizations = sqlx::query!("SELECT id FROM organizations")
160            .fetch_all(&self.pool)
161            .await
162            .map_err(|e| format!("Failed to fetch organizations: {}", e))?;
163
164        let mut total_created = 0;
165        let mut orgs_seeded = 0;
166
167        for org in organizations {
168            let org_id = org.id;
169
170            // Check if this organization already has accounts
171            let existing_count = sqlx::query!(
172                "SELECT COUNT(*) as count FROM accounts WHERE organization_id = $1",
173                org_id
174            )
175            .fetch_one(&self.pool)
176            .await
177            .map_err(|e| format!("Failed to count accounts: {}", e))?;
178
179            if existing_count.count.unwrap_or(0) > 0 {
180                log::debug!(
181                    "Organization {} already has {} accounts, skipping",
182                    org_id,
183                    existing_count.count.unwrap_or(0)
184                );
185                continue;
186            }
187
188            // Seed PCMN for this organization
189            let created = self.seed_belgian_pcmn_for_org(org_id).await?;
190            total_created += created;
191            orgs_seeded += 1;
192        }
193
194        let message = format!(
195            "✅ Seeded {} accounts across {} organizations",
196            total_created, orgs_seeded
197        );
198        log::info!("{}", message);
199        Ok(message)
200    }
201
202    /// Seed Belgian PCMN for a specific organization (idempotent)
203    async fn seed_belgian_pcmn_for_org(&self, organization_id: Uuid) -> Result<i64, String> {
204        // Base PCMN accounts based on Belgian accounting standards
205        let base_accounts = vec![
206            // Class 6: Charges (Expenses)
207            (
208                "6100",
209                "Charges courantes",
210                None,
211                AccountType::Expense,
212                true,
213            ),
214            (
215                "6110",
216                "Entretien et réparations",
217                None,
218                AccountType::Expense,
219                true,
220            ),
221            ("6120", "Personnel", None, AccountType::Expense, true),
222            (
223                "6130",
224                "Services extérieurs",
225                None,
226                AccountType::Expense,
227                true,
228            ),
229            (
230                "6140",
231                "Honoraires et commissions",
232                None,
233                AccountType::Expense,
234                true,
235            ),
236            ("6150", "Assurances", None, AccountType::Expense, true),
237            (
238                "6200",
239                "Travaux extraordinaires",
240                None,
241                AccountType::Expense,
242                true,
243            ),
244            // Class 7: Produits (Revenue)
245            (
246                "7000",
247                "Produits de gestion",
248                None,
249                AccountType::Revenue,
250                true,
251            ),
252            (
253                "7100",
254                "Appels de fonds",
255                Some("7000"),
256                AccountType::Revenue,
257                true,
258            ),
259            (
260                "7200",
261                "Autres produits",
262                Some("7000"),
263                AccountType::Revenue,
264                true,
265            ),
266            // Class 4: Tiers (Third parties)
267            (
268                "4000",
269                "Comptes de tiers",
270                None,
271                AccountType::Liability,
272                false,
273            ),
274            (
275                "4100",
276                "TVA à récupérer",
277                Some("4000"),
278                AccountType::Asset,
279                true,
280            ),
281            (
282                "4110",
283                "TVA récupérable",
284                Some("4100"),
285                AccountType::Asset,
286                true,
287            ),
288            (
289                "4400",
290                "Fournisseurs",
291                Some("4000"),
292                AccountType::Liability,
293                true,
294            ),
295            (
296                "4500",
297                "Copropriétaires",
298                Some("4000"),
299                AccountType::Asset,
300                true,
301            ),
302            // Class 5: Trésorerie (Cash/Bank)
303            ("5500", "Banque", None, AccountType::Asset, true),
304            ("5700", "Caisse", None, AccountType::Asset, true),
305        ];
306
307        let mut created_count = 0;
308
309        for (code, label, parent_code, account_type, direct_use) in base_accounts {
310            // Create account using domain entity
311            let account = Account::new(
312                code.to_string(),
313                label.to_string(),
314                parent_code.map(|s| s.to_string()),
315                account_type,
316                direct_use,
317                organization_id,
318            )?;
319
320            // Insert into database (idempotent - skip if exists)
321            let result = sqlx::query!(
322                r#"
323                INSERT INTO accounts (id, code, label, parent_code, account_type, direct_use, organization_id, created_at, updated_at)
324                VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
325                ON CONFLICT (code, organization_id) DO NOTHING
326                "#,
327                account.id,
328                account.code,
329                account.label,
330                account.parent_code,
331                account.account_type as AccountType,
332                account.direct_use,
333                account.organization_id,
334                account.created_at,
335                account.updated_at
336            )
337            .execute(&self.pool)
338            .await
339            .map_err(|e| format!("Failed to insert account {}: {}", code, e))?;
340
341            if result.rows_affected() > 0 {
342                created_count += 1;
343            }
344        }
345
346        log::info!(
347            "Created {} PCMN accounts for organization {}",
348            created_count,
349            organization_id
350        );
351        Ok(created_count)
352    }
353
354    /// Seed demo data for production demonstration
355    pub async fn seed_demo_data(&self) -> Result<String, String> {
356        log::info!("🌱 Starting demo data seeding...");
357
358        // Check if seed data already exists (only check for seed organizations, not all)
359        let existing_seed_orgs =
360            sqlx::query!("SELECT COUNT(*) as count FROM organizations WHERE is_seed_data = true")
361                .fetch_one(&self.pool)
362                .await
363                .map_err(|e| format!("Failed to count seed organizations: {}", e))?;
364
365        if existing_seed_orgs.count.unwrap_or(0) > 0 {
366            return Err(
367                "Seed data already exists. Please use 'Clear Seed Data' first.".to_string(),
368            );
369        }
370
371        // ORGANIZATION 1
372        let org1_id = Uuid::new_v4();
373        let now = Utc::now();
374
375        sqlx::query(
376            r#"
377            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)
378            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
379            "#
380        )
381        .bind(org1_id)
382        .bind("Résidence Grand Place SPRL")
383        .bind("residence-grand-place")
384        .bind("contact@grandplace.be")
385        .bind("+32 2 501 23 45")
386        .bind("professional")
387        .bind(20)
388        .bind(50)
389        .bind(true) // is_active
390        .bind(true) // is_seed_data
391        .bind(now)
392        .bind(now)
393        .execute(&self.pool)
394        .await
395        .map_err(|e| format!("Failed to create demo organization 1: {}", e))?;
396
397        log::info!("✅ Organization 1 created: Résidence Grand Place SPRL");
398
399        // Create demo users ORG 1
400        let syndic1_id = self
401            .create_demo_user(
402                "syndic@grandplace.be",
403                "syndic123",
404                "Jean",
405                "Dupont",
406                "syndic",
407                Some(org1_id),
408            )
409            .await?;
410
411        let _accountant_id = self
412            .create_demo_user(
413                "comptable@grandplace.be",
414                "comptable123",
415                "Marie",
416                "Martin",
417                "accountant",
418                Some(org1_id),
419            )
420            .await?;
421
422        let owner1_user_id = self
423            .create_demo_user(
424                "proprietaire1@grandplace.be",
425                "owner123",
426                "Pierre",
427                "Durand",
428                "owner",
429                Some(org1_id),
430            )
431            .await?;
432
433        let owner2_user_id = self
434            .create_demo_user(
435                "proprietaire2@grandplace.be",
436                "owner123",
437                "Sophie",
438                "Bernard",
439                "owner",
440                Some(org1_id),
441            )
442            .await?;
443
444        log::info!("✅ Demo users created");
445
446        // Create demo buildings ORG 1
447        let building1_id = self
448            .create_demo_building(
449                org1_id,
450                "Résidence Grand Place",
451                "Grand Place 15",
452                "Bruxelles",
453                "1000",
454                "Belgique",
455                15,
456                1995,
457            )
458            .await?;
459
460        let building2_id = self
461            .create_demo_building(
462                org1_id,
463                "Les Jardins d'Ixelles",
464                "Rue du Trône 85",
465                "Bruxelles",
466                "1050",
467                "Belgique",
468                8,
469                2010,
470            )
471            .await?;
472
473        log::info!("✅ Demo buildings created");
474
475        // Create demo owners
476        let owner1_db_id = self
477            .create_demo_owner(
478                org1_id,
479                "Pierre",
480                "Durand",
481                "pierre.durand@email.be",
482                "+32 476 12 34 56",
483                "Avenue Louise 15",
484                "Bruxelles",
485                "1050",
486                "Belgique",
487            )
488            .await?;
489
490        let owner2_db_id = self
491            .create_demo_owner(
492                org1_id,
493                "Sophie",
494                "Bernard",
495                "sophie.bernard@email.be",
496                "+32 495 98 76 54",
497                "Rue Royale 28",
498                "Bruxelles",
499                "1000",
500                "Belgique",
501            )
502            .await?;
503
504        let owner3_db_id = self
505            .create_demo_owner(
506                org1_id,
507                "Michel",
508                "Lefebvre",
509                "michel.lefebvre@email.be",
510                "+32 477 11 22 33",
511                "Boulevard d'Avroy 42",
512                "Liège",
513                "4000",
514                "Belgique",
515            )
516            .await?;
517
518        log::info!("✅ Demo owners created");
519
520        // Link users to owners (for portal access)
521        sqlx::query("UPDATE owners SET user_id = $1 WHERE id = $2")
522            .bind(owner1_user_id)
523            .bind(owner1_db_id)
524            .execute(&self.pool)
525            .await
526            .map_err(|e| format!("Failed to link owner1 to user: {}", e))?;
527
528        sqlx::query("UPDATE owners SET user_id = $1 WHERE id = $2")
529            .bind(owner2_user_id)
530            .bind(owner2_db_id)
531            .execute(&self.pool)
532            .await
533            .map_err(|e| format!("Failed to link owner2 to user: {}", e))?;
534
535        log::info!("✅ Users linked to owners");
536
537        // Create demo units (owner_id is now deprecated, set to None)
538        let unit1_id = self
539            .create_demo_unit(
540                org1_id,
541                building1_id,
542                None, // owner_id deprecated
543                "101",
544                "apartment",
545                Some(1),
546                75.5,
547                250.0,
548            )
549            .await?;
550
551        let unit2_id = self
552            .create_demo_unit(
553                org1_id,
554                building1_id,
555                None, // owner_id deprecated
556                "102",
557                "apartment",
558                Some(1),
559                62.0,
560                200.0,
561            )
562            .await?;
563
564        let unit3_id = self
565            .create_demo_unit(
566                org1_id,
567                building1_id,
568                None, // owner_id deprecated
569                "103",
570                "apartment",
571                Some(1),
572                85.0,
573                300.0,
574            )
575            .await?;
576
577        let unit4_id = self
578            .create_demo_unit(
579                org1_id,
580                building2_id,
581                None, // owner_id deprecated
582                "201",
583                "apartment",
584                Some(2),
585                95.0,
586                350.0,
587            )
588            .await?;
589
590        log::info!("✅ Demo units created");
591
592        // Create unit_owners relationships
593        // Scenario 1: Unit 101 - Single owner (Pierre Durand 100%)
594        self.create_demo_unit_owner(
595            unit1_id,
596            owner1_db_id,
597            rust_decimal_macros::dec!(1), // 100%
598            true,                         // primary contact
599            None,                         // no end_date (active)
600        )
601        .await?;
602
603        // Scenario 2: Unit 102 - Co-ownership (Sophie Bernard 60%, Michel Lefebvre 40%)
604        self.create_demo_unit_owner(
605            unit2_id,
606            owner2_db_id,
607            rust_decimal_macros::dec!(0.6), // 60%
608            true,                           // primary contact
609            None,
610        )
611        .await?;
612
613        self.create_demo_unit_owner(
614            unit2_id,
615            owner3_db_id,
616            rust_decimal_macros::dec!(0.4), // 40%
617            false,                          // not primary contact
618            None,
619        )
620        .await?;
621
622        // Scenario 3: Unit 103 - Co-ownership with 3 owners (50%, 30%, 20%)
623        self.create_demo_unit_owner(
624            unit3_id,
625            owner1_db_id,
626            rust_decimal_macros::dec!(0.5), // 50%
627            true,                           // primary contact
628            None,
629        )
630        .await?;
631
632        self.create_demo_unit_owner(
633            unit3_id,
634            owner2_db_id,
635            rust_decimal_macros::dec!(0.3), // 30%
636            false,
637            None,
638        )
639        .await?;
640
641        self.create_demo_unit_owner(
642            unit3_id,
643            owner3_db_id,
644            rust_decimal_macros::dec!(0.2), // 20%
645            false,
646            None,
647        )
648        .await?;
649
650        // Scenario 4: Unit 201 - Michel Lefebvre owns multiple units (100% of this one)
651        self.create_demo_unit_owner(
652            unit4_id,
653            owner3_db_id,
654            rust_decimal_macros::dec!(1), // 100%
655            true,                         // primary contact
656            None,
657        )
658        .await?;
659
660        log::info!("✅ Demo unit_owners relationships created");
661
662        // Seed Belgian PCMN accounts for this organization
663        self.seed_pcmn_accounts(org1_id).await?;
664        log::info!("✅ Belgian PCMN accounts seeded");
665
666        // Create demo expenses with realistic Belgian VAT rates and accounting links
667        // Expense 1: Quarterly condo fees (paid) - 21% VAT
668        let expense1_id = self
669            .create_demo_expense_with_vat(
670                building1_id,
671                org1_id,
672                "Charges copropriété T1 2025",
673                4132.23, // HT
674                21.0,    // VAT 21%
675                "2025-01-15",
676                "2025-02-15", // due date
677                "administration",
678                "paid",
679                Some("Syndic Services SPRL"),
680                Some("SYN-2025-001"),
681                Some("6100"), // PCMN: Charges courantes
682            )
683            .await?;
684
685        // Expense 2: Elevator repair (paid) - 21% VAT
686        let expense2_id = self
687            .create_demo_expense_with_vat(
688                building1_id,
689                org1_id,
690                "Réparation ascenseur - Remplacement moteur",
691                2066.12, // HT
692                21.0,    // VAT 21%
693                "2025-02-10",
694                "2025-03-10",
695                "maintenance",
696                "paid",
697                Some("Ascenseurs Plus SA"),
698                Some("ASC-2025-023"),
699                Some("6110"), // PCMN: Entretien et réparations
700            )
701            .await?;
702
703        // Expense 3: Quarterly condo fees building 2 (pending, will become overdue) - 21% VAT
704        let expense3_id = self
705            .create_demo_expense_with_vat(
706                building2_id,
707                org1_id,
708                "Charges copropriété T1 2025",
709                2479.34, // HT
710                21.0,    // VAT 21%
711                "2025-01-15",
712                "2025-02-15", // OVERDUE (due 2 months ago)
713                "administration",
714                "overdue",
715                Some("Syndic Services SPRL"),
716                Some("SYN-2025-002"),
717                Some("6100"), // PCMN: Charges courantes
718            )
719            .await?;
720
721        // Expense 4: Cleaning (paid) - 6% VAT (reduced rate for certain services)
722        let expense4_id = self
723            .create_demo_expense_with_vat(
724                building2_id,
725                org1_id,
726                "Nettoyage parties communes - Forfait annuel",
727                1132.08, // HT
728                6.0,     // VAT 6% (reduced rate)
729                "2025-01-01",
730                "2025-01-31",
731                "cleaning",
732                "paid",
733                Some("CleanPro Belgium SPRL"),
734                Some("CLN-2025-156"),
735                Some("6130"), // PCMN: Services extérieurs
736            )
737            .await?;
738
739        // Expense 5: Insurance (pending) - 0% VAT (insurance exempt)
740        let expense5_id = self
741            .create_demo_expense_with_vat(
742                building1_id,
743                org1_id,
744                "Assurance incendie immeuble 2025",
745                1850.00, // HT (no VAT)
746                0.0,     // VAT 0% (exempt)
747                "2025-01-05",
748                "2025-02-05",
749                "insurance",
750                "pending",
751                Some("AXA Belgium"),
752                Some("AXA-2025-8472"),
753                Some("6150"), // PCMN: Assurances
754            )
755            .await?;
756
757        // Expense 6: Facade works (pending approval) - 21% VAT
758        let expense6_id = self
759            .create_demo_expense_with_vat(
760                building1_id,
761                org1_id,
762                "Rénovation façade - Devis Entreprise Martin",
763                12396.69, // HT
764                21.0,     // VAT 21%
765                "2025-03-01",
766                "2025-04-30",
767                "works",
768                "pending",
769                Some("Entreprise Martin & Fils SPRL"),
770                Some("MART-2025-042"),
771                Some("6200"), // PCMN: Travaux extraordinaires
772            )
773            .await?;
774
775        // ===== CURRENT MONTH EXPENSES =====
776        // Use relative dates based on today's date
777        let now = Utc::now();
778        let current_month = now.format("%B %Y").to_string();
779        let month_start = format!("{}", now.format("%Y-%m-01"));
780        let day_3 = format!("{}", now.format("%Y-%m-03"));
781        let day_5 = format!("{}", now.format("%Y-%m-05"));
782        let day_8 = format!("{}", now.format("%Y-%m-08"));
783        let day_10 = format!("{}", now.format("%Y-%m-10"));
784        let month_end = format!("{}", (now + chrono::Duration::days(30)).format("%Y-%m-%d"));
785
786        // Expense 7: Elevator maintenance (current month) - paid - 21% VAT
787        let expense7_id = self
788            .create_demo_expense_with_vat(
789                building1_id,
790                org1_id,
791                &format!("Maintenance ascenseur {}", current_month),
792                826.45, // HT
793                21.0,   // VAT 21%
794                &day_5,
795                &month_end,
796                "maintenance",
797                "paid",
798                Some("Ascenseurs Plus SA"),
799                Some(&format!("ASC-{}-001", now.format("%Y-%m"))),
800                Some("6110"), // PCMN: Entretien et réparations
801            )
802            .await?;
803
804        // Expense 8: Electricity bill (current month) - paid - 21% VAT
805        let expense8_id = self
806            .create_demo_expense_with_vat(
807                building1_id,
808                org1_id,
809                &format!("Électricité communs {}", current_month),
810                387.60, // HT
811                21.0,   // VAT 21%
812                &day_3,
813                &format!("{}", (now + chrono::Duration::days(25)).format("%Y-%m-%d")),
814                "utilities",
815                "paid",
816                Some("Engie Electrabel"),
817                Some(&format!("ENGIE-{}-3847", now.format("%Y-%m"))),
818                Some("6100"), // PCMN: Charges courantes (Électricité)
819            )
820            .await?;
821
822        // Expense 9: Cleaning service (current month) - paid - 6% VAT
823        let expense9_id = self
824            .create_demo_expense_with_vat(
825                building1_id,
826                org1_id,
827                &format!("Nettoyage communs {}", current_month),
828                471.70, // HT
829                6.0,    // VAT 6% (labor-intensive services)
830                &month_start,
831                &format!("{}", (now + chrono::Duration::days(20)).format("%Y-%m-%d")),
832                "cleaning",
833                "paid",
834                Some("NetClean Services SPRL"),
835                Some(&format!("CLEAN-{}-074", now.format("%Y-%m"))),
836                Some("6130"), // PCMN: Services extérieurs (Nettoyage)
837            )
838            .await?;
839
840        // Expense 10: Water bill (current month) - pending - 6% VAT
841        let expense10_id = self
842            .create_demo_expense_with_vat(
843                building1_id,
844                org1_id,
845                &format!("Eau communs {}", current_month),
846                156.60, // HT
847                6.0,    // VAT 6%
848                &day_8,
849                &format!("{}", (now + chrono::Duration::days(30)).format("%Y-%m-%d")),
850                "utilities",
851                "pending",
852                Some("Vivaqua"),
853                Some(&format!("VIVA-{}-9284", now.format("%Y-%m"))),
854                Some("6100"), // PCMN: Charges courantes (Eau)
855            )
856            .await?;
857
858        // Expense 11: Heating gas (current month) - paid - 21% VAT
859        let expense11_id = self
860            .create_demo_expense_with_vat(
861                building1_id,
862                org1_id,
863                &format!("Chauffage gaz {}", current_month),
864                1240.00, // HT
865                21.0,    // VAT 21%
866                &day_10,
867                &format!("{}", (now + chrono::Duration::days(30)).format("%Y-%m-%d")),
868                "utilities",
869                "paid",
870                Some("Sibelga"),
871                Some(&format!("SIBEL-{}-7453", now.format("%Y-%m"))),
872                Some("6100"), // PCMN: Charges courantes (Chauffage)
873            )
874            .await?;
875
876        log::info!("✅ Demo expenses with VAT created (including current month)");
877
878        // Calculate and save charge distributions
879        self.create_demo_distributions(expense1_id, org1_id).await?;
880        self.create_demo_distributions(expense2_id, org1_id).await?;
881        self.create_demo_distributions(expense3_id, org1_id).await?;
882        self.create_demo_distributions(expense4_id, org1_id).await?;
883        self.create_demo_distributions(expense5_id, org1_id).await?;
884        self.create_demo_distributions(expense6_id, org1_id).await?;
885        self.create_demo_distributions(expense7_id, org1_id).await?;
886        self.create_demo_distributions(expense8_id, org1_id).await?;
887        self.create_demo_distributions(expense9_id, org1_id).await?;
888        self.create_demo_distributions(expense10_id, org1_id)
889            .await?;
890        self.create_demo_distributions(expense11_id, org1_id)
891            .await?;
892        log::info!("✅ Charge distributions calculated");
893
894        // Create payment reminders for overdue expense
895        self.create_demo_payment_reminder(
896            expense3_id,
897            owner2_db_id, // Sophie Bernard
898            org1_id,
899            "FirstReminder",
900            20, // 20 days overdue
901        )
902        .await?;
903
904        self.create_demo_payment_reminder(
905            expense3_id,
906            owner3_db_id, // Michel Lefebvre
907            org1_id,
908            "SecondReminder",
909            35, // 35 days overdue
910        )
911        .await?;
912
913        log::info!("✅ Payment reminders created");
914
915        // Create owner contributions (revenue) for current month
916        log::info!("Creating owner contributions...");
917
918        // Get quarter number for current month (using Datelike trait)
919        use chrono::Datelike;
920        let quarter = ((now.month() - 1) / 3) + 1;
921        let year = now.year();
922
923        // Regular contributions (appels de fonds) for current month
924        // Each owner pays quarterly fees
925        self.create_demo_owner_contribution(
926            org1_id,
927            owner1_db_id, // Jean Dupont
928            Some(unit1_id),
929            &format!("Appel de fonds T{} {} - Charges courantes", quarter, year),
930            650.0,
931            "regular",
932            &month_start,
933            "paid",
934            Some(&day_5),
935            Some("7000"), // PCMN: Regular contributions
936        )
937        .await?;
938
939        self.create_demo_owner_contribution(
940            org1_id,
941            owner2_db_id, // Sophie Bernard
942            Some(unit2_id),
943            &format!("Appel de fonds T{} {} - Charges courantes", quarter, year),
944            750.0,
945            "regular",
946            &month_start,
947            "paid",
948            Some(&day_8),
949            Some("7000"),
950        )
951        .await?;
952
953        self.create_demo_owner_contribution(
954            org1_id,
955            owner3_db_id, // Michel Lefebvre
956            Some(unit3_id),
957            &format!("Appel de fonds T{} {} - Charges courantes", quarter, year),
958            600.0,
959            "regular",
960            &month_start,
961            "pending",
962            None, // Not paid yet
963            Some("7000"),
964        )
965        .await?;
966
967        // Note: Only 3 owners in seed data, so we skip owner4
968
969        // Extraordinary contribution for roof repairs (previous month)
970        let prev_month = (now - chrono::Duration::days(20))
971            .format("%Y-%m-05")
972            .to_string();
973        let prev_month_payment = (now - chrono::Duration::days(15))
974            .format("%Y-%m-10")
975            .to_string();
976
977        self.create_demo_owner_contribution(
978            org1_id,
979            owner1_db_id, // Jean Dupont
980            Some(unit1_id),
981            "Appel de fonds extraordinaire - Réfection toiture",
982            1200.0,
983            "extraordinary",
984            &prev_month,
985            "paid",
986            Some(&prev_month_payment),
987            Some("7100"), // PCMN: Extraordinary contributions
988        )
989        .await?;
990
991        self.create_demo_owner_contribution(
992            org1_id,
993            owner2_db_id, // Sophie Bernard
994            Some(unit2_id),
995            "Appel de fonds extraordinaire - Réfection toiture",
996            1400.0,
997            "extraordinary",
998            &prev_month,
999            "pending",
1000            None, // Not paid yet
1001            Some("7100"),
1002        )
1003        .await?;
1004
1005        log::info!("✅ Owner contributions created");
1006
1007        // Create meetings ORG 1 (in the past, 3-6 months ago)
1008        let meeting1_date = (now - chrono::Duration::days(90))
1009            .format("%Y-%m-%d")
1010            .to_string();
1011        let meeting2_date = (now - chrono::Duration::days(60))
1012            .format("%Y-%m-%d")
1013            .to_string();
1014
1015        let meeting1_id = self
1016            .create_demo_meeting(
1017                building1_id,
1018                org1_id,
1019                &format!("Assemblée Générale Ordinaire {}", year),
1020                "ordinary",
1021                &meeting1_date,
1022                "completed",
1023            )
1024            .await?;
1025
1026        let meeting2_id = self
1027            .create_demo_meeting(
1028                building2_id,
1029                org1_id,
1030                "Assemblée Générale Extraordinaire - Travaux",
1031                "extraordinary",
1032                &meeting2_date,
1033                "completed",
1034            )
1035            .await?;
1036
1037        log::info!("✅ Demo meetings created");
1038
1039        // Create board members ORG 1
1040        // Board mandates are for 1 year from meeting1_date
1041        let mandate_start = meeting1_date.clone();
1042        let mandate_end = (now + chrono::Duration::days(275))
1043            .format("%Y-%m-%d")
1044            .to_string(); // ~9 months from now
1045
1046        // Elect owner1 as president for building1 (mandate: ~1 year as per Belgian law)
1047        self.create_demo_board_member(
1048            owner1_db_id,
1049            building1_id,
1050            org1_id,
1051            meeting1_id,
1052            "president",
1053            &mandate_start,
1054            &mandate_end,
1055        )
1056        .await?;
1057
1058        // Elect owner2 as treasurer for building2 (mandate: ~1 year)
1059        self.create_demo_board_member(
1060            owner2_db_id,
1061            building2_id,
1062            org1_id,
1063            meeting2_id,
1064            "treasurer",
1065            &meeting2_date,
1066            &format!("{}", (now + chrono::Duration::days(305)).format("%Y-%m-%d")),
1067        )
1068        .await?;
1069
1070        log::info!("✅ Demo board members elected");
1071
1072        // Create board decisions ORG 1
1073        // Decision 1: Pending with deadline in 25 days (medium urgency)
1074        self.create_demo_board_decision(
1075            building1_id,
1076            org1_id,
1077            meeting1_id,
1078            "Rénovation de la façade",
1079            "Approuver les devis pour la rénovation de la façade principale",
1080            Some("2025-11-26"), // ~25 days from 2025-11-01
1081            "pending",
1082        )
1083        .await?;
1084
1085        // Decision 2: In progress with deadline in 4 days (critical urgency)
1086        self.create_demo_board_decision(
1087            building1_id,
1088            org1_id,
1089            meeting1_id,
1090            "Contrat d'assurance",
1091            "Signer le nouveau contrat d'assurance avec AXA",
1092            Some("2025-11-05"), // 4 days from 2025-11-01
1093            "in_progress",
1094        )
1095        .await?;
1096
1097        // Decision 3: Overdue (deadline passed)
1098        self.create_demo_board_decision(
1099            building1_id,
1100            org1_id,
1101            meeting1_id,
1102            "Nettoyage des gouttières",
1103            "Engager une entreprise pour le nettoyage annuel des gouttières",
1104            Some("2025-10-15"), // Past deadline
1105            "pending",
1106        )
1107        .await?;
1108
1109        // Decision 4: Completed
1110        self.create_demo_board_decision(
1111            building1_id,
1112            org1_id,
1113            meeting1_id,
1114            "Installation caméras",
1115            "Installation du système de vidéosurveillance dans le hall",
1116            Some("2025-10-01"),
1117            "completed",
1118        )
1119        .await?;
1120
1121        // Decision 5: Pending with deadline in 10 days (high urgency)
1122        self.create_demo_board_decision(
1123            building2_id,
1124            org1_id,
1125            meeting2_id,
1126            "Remplacement chaudière",
1127            "Valider le choix du fournisseur pour la nouvelle chaudière",
1128            Some("2025-11-11"), // 10 days from 2025-11-01
1129            "pending",
1130        )
1131        .await?;
1132
1133        // Decision 6: In progress with deadline in 20 days (medium urgency)
1134        self.create_demo_board_decision(
1135            building2_id,
1136            org1_id,
1137            meeting2_id,
1138            "Aménagement parking vélos",
1139            "Organiser l'aménagement du parking à vélos au rez-de-chaussée",
1140            Some("2025-11-21"), // 20 days from 2025-11-01
1141            "in_progress",
1142        )
1143        .await?;
1144
1145        log::info!("✅ Demo board decisions created");
1146
1147        // Create documents ORG 1
1148        self.create_demo_document(
1149            building1_id,
1150            org1_id,
1151            "Procès-Verbal AG 2024",
1152            "meeting_minutes",
1153            "/uploads/demo/pv-ag-2024.pdf",
1154            syndic1_id,
1155        )
1156        .await?;
1157
1158        self.create_demo_document(
1159            building1_id,
1160            org1_id,
1161            "Règlement de copropriété",
1162            "regulation",
1163            "/uploads/demo/reglement.pdf",
1164            syndic1_id,
1165        )
1166        .await?;
1167
1168        log::info!("✅ Demo documents created");
1169
1170        // ORGANIZATION 2 - Bruxelles
1171        let org2_id = Uuid::new_v4();
1172        sqlx::query(
1173            r#"
1174            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)
1175            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
1176            "#
1177        )
1178        .bind(org2_id)
1179        .bind("Copropriété Bruxelles SPRL")
1180        .bind("copro-bruxelles")
1181        .bind("info@copro-bruxelles.be")
1182        .bind("+32 2 123 45 67")
1183        .bind("starter")
1184        .bind(5)
1185        .bind(10)
1186        .bind(true) // is_active
1187        .bind(true) // is_seed_data
1188        .bind(now)
1189        .bind(now)
1190        .execute(&self.pool)
1191        .await
1192        .map_err(|e| format!("Failed to create demo organization 2: {}", e))?;
1193
1194        let _syndic2_id = self
1195            .create_demo_user(
1196                "syndic@copro-bruxelles.be",
1197                "syndic123",
1198                "Marc",
1199                "Dubois",
1200                "syndic",
1201                Some(org2_id),
1202            )
1203            .await?;
1204
1205        let building3_id = self
1206            .create_demo_building(
1207                org2_id,
1208                "Résidence Européenne",
1209                "Avenue Louise 123",
1210                "Bruxelles",
1211                "1050",
1212                "Belgique",
1213                12,
1214                2005,
1215            )
1216            .await?;
1217
1218        self.create_demo_meeting(
1219            building3_id,
1220            org2_id,
1221            "AG Annuelle 2025",
1222            "ordinary",
1223            "2025-05-10",
1224            "scheduled",
1225        )
1226        .await?;
1227
1228        log::info!("✅ Organization 2 created");
1229
1230        // ORGANIZATION 3 - Liège
1231        let org3_id = Uuid::new_v4();
1232        sqlx::query(
1233            r#"
1234            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)
1235            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
1236            "#
1237        )
1238        .bind(org3_id)
1239        .bind("Syndic Liège SA")
1240        .bind("syndic-liege")
1241        .bind("contact@syndic-liege.be")
1242        .bind("+32 4 222 33 44")
1243        .bind("enterprise")
1244        .bind(50)
1245        .bind(100)
1246        .bind(true) // is_active
1247        .bind(true) // is_seed_data
1248        .bind(now)
1249        .bind(now)
1250        .execute(&self.pool)
1251        .await
1252        .map_err(|e| format!("Failed to create demo organization 3: {}", e))?;
1253
1254        let _syndic3_id = self
1255            .create_demo_user(
1256                "syndic@syndic-liege.be",
1257                "syndic123",
1258                "Sophie",
1259                "Lambert",
1260                "syndic",
1261                Some(org3_id),
1262            )
1263            .await?;
1264
1265        let _building4_id = self
1266            .create_demo_building(
1267                org3_id,
1268                "Les Terrasses de Liège",
1269                "Boulevard de la Sauvenière 45",
1270                "Liège",
1271                "4000",
1272                "Belgique",
1273                8,
1274                2018,
1275            )
1276            .await?;
1277
1278        log::info!("✅ Organization 3 created");
1279
1280        Ok("✅ Demo data seeded successfully!\n\n\
1281            📊 Summary:\n\
1282            - 3 Organizations: Grand Place (Bruxelles), Bruxelles Louise, Liège\n\
1283            - 6+ Users: 3 Syndics, 1 Accountant, 2+ Owners\n\
1284            - 4 Buildings across Belgium\n\
1285            - 3 Owners (database records)\n\
1286            - 4 Units\n\
1287            - 4 Expenses\n\
1288            - 3 Meetings\n\
1289            - 2 Documents\n\n\
1290            🇧🇪 Belgian Demo - Credentials:\n\
1291            - Org 1 (Grand Place): syndic@grandplace.be / syndic123\n\
1292            - Org 2 (Bruxelles): syndic@copro-bruxelles.be / syndic123\n\
1293            - Org 3 (Liège): syndic@syndic-liege.be / syndic123\n\
1294            - SuperAdmin: admin@koprogo.com / admin123"
1295            .to_string())
1296    }
1297
1298    async fn create_demo_user(
1299        &self,
1300        email: &str,
1301        password: &str,
1302        first_name: &str,
1303        last_name: &str,
1304        role: &str,
1305        organization_id: Option<Uuid>,
1306    ) -> Result<Uuid, String> {
1307        let password_hash =
1308            hash(password, DEFAULT_COST).map_err(|e| format!("Failed to hash password: {}", e))?;
1309
1310        let user_id = Uuid::new_v4();
1311        let now = Utc::now();
1312
1313        sqlx::query!(
1314            r#"
1315            INSERT INTO users (id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at)
1316            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
1317            "#,
1318            user_id,
1319            email,
1320            password_hash,
1321            first_name,
1322            last_name,
1323            role,
1324            organization_id,
1325            true,
1326            now,
1327            now
1328        )
1329        .execute(&self.pool)
1330        .await
1331        .map_err(|e| format!("Failed to create user {}: {}", email, e))?;
1332
1333        sqlx::query(
1334            r#"
1335            INSERT INTO user_roles (id, user_id, role, organization_id, is_primary, created_at, updated_at)
1336            VALUES (gen_random_uuid(), $1, $2, $3, true, $4, $4)
1337            ON CONFLICT (user_id, role, organization_id)
1338            DO UPDATE SET is_primary = true, updated_at = EXCLUDED.updated_at
1339            "#,
1340        )
1341        .bind(user_id)
1342        .bind(role)
1343        .bind(organization_id)
1344        .bind(now)
1345        .execute(&self.pool)
1346        .await
1347        .map_err(|e| format!("Failed to assign role {} to user {}: {}", role, email, e))?;
1348
1349        Ok(user_id)
1350    }
1351
1352    #[allow(clippy::too_many_arguments)]
1353    async fn create_demo_building(
1354        &self,
1355        org_id: Uuid,
1356        name: &str,
1357        address: &str,
1358        city: &str,
1359        postal_code: &str,
1360        country: &str,
1361        total_units: i32,
1362        construction_year: i32,
1363    ) -> Result<Uuid, String> {
1364        let building_id = Uuid::new_v4();
1365        let now = Utc::now();
1366
1367        sqlx::query!(
1368            r#"
1369            INSERT INTO buildings (id, organization_id, name, address, city, postal_code, country, total_units, construction_year, created_at, updated_at)
1370            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
1371            "#,
1372            building_id,
1373            org_id,
1374            name,
1375            address,
1376            city,
1377            postal_code,
1378            country,
1379            total_units,
1380            construction_year,
1381            now,
1382            now
1383        )
1384        .execute(&self.pool)
1385        .await
1386        .map_err(|e| format!("Failed to create building {}: {}", name, e))?;
1387
1388        Ok(building_id)
1389    }
1390
1391    #[allow(clippy::too_many_arguments)]
1392    async fn create_demo_owner(
1393        &self,
1394        organization_id: Uuid,
1395        first_name: &str,
1396        last_name: &str,
1397        email: &str,
1398        phone: &str,
1399        address: &str,
1400        city: &str,
1401        postal_code: &str,
1402        country: &str,
1403    ) -> Result<Uuid, String> {
1404        let owner_id = Uuid::new_v4();
1405        let now = Utc::now();
1406
1407        sqlx::query!(
1408            r#"
1409            INSERT INTO owners (id, organization_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at)
1410            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
1411            "#,
1412            owner_id,
1413            organization_id,
1414            first_name,
1415            last_name,
1416            email,
1417            phone,
1418            address,
1419            city,
1420            postal_code,
1421            country,
1422            now,
1423            now
1424        )
1425        .execute(&self.pool)
1426        .await
1427        .map_err(|e| format!("Failed to create owner {} {}: {}", first_name, last_name, e))?;
1428
1429        Ok(owner_id)
1430    }
1431
1432    #[allow(clippy::too_many_arguments)]
1433    async fn create_demo_unit(
1434        &self,
1435        organization_id: Uuid,
1436        building_id: Uuid,
1437        owner_id: Option<Uuid>,
1438        unit_number: &str,
1439        unit_type: &str,
1440        floor: Option<i32>,
1441        surface_area: f64,
1442        quota: f64,
1443    ) -> Result<Uuid, String> {
1444        let unit_id = Uuid::new_v4();
1445        let now = Utc::now();
1446
1447        sqlx::query(
1448            r#"
1449            INSERT INTO units (id, organization_id, building_id, owner_id, unit_number, unit_type, floor, surface_area, quota, created_at, updated_at)
1450            VALUES ($1, $2, $3, $4, $5, $6::unit_type, $7, $8, $9, $10, $11)
1451            "#
1452        )
1453        .bind(unit_id)
1454        .bind(organization_id)
1455        .bind(building_id)
1456        .bind(owner_id)
1457        .bind(unit_number)
1458        .bind(unit_type)
1459        .bind(floor)
1460        .bind(surface_area)
1461        .bind(quota)
1462        .bind(now)
1463        .bind(now)
1464        .execute(&self.pool)
1465        .await
1466        .map_err(|e| format!("Failed to create unit {}: {}", unit_number, e))?;
1467
1468        Ok(unit_id)
1469    }
1470
1471    async fn create_demo_unit_owner(
1472        &self,
1473        unit_id: Uuid,
1474        owner_id: Uuid,
1475        ownership_percentage: rust_decimal::Decimal,
1476        is_primary_contact: bool,
1477        end_date: Option<chrono::DateTime<Utc>>,
1478    ) -> Result<Uuid, String> {
1479        let unit_owner_id = Uuid::new_v4();
1480        let now = Utc::now();
1481
1482        sqlx::query!(
1483            r#"
1484            INSERT INTO unit_owners (id, unit_id, owner_id, ownership_percentage, start_date, end_date, is_primary_contact, created_at, updated_at)
1485            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
1486            "#,
1487            unit_owner_id,
1488            unit_id,
1489            owner_id,
1490            ownership_percentage,
1491            now, // start_date
1492            end_date,
1493            is_primary_contact,
1494            now, // created_at
1495            now  // updated_at
1496        )
1497        .execute(&self.pool)
1498        .await
1499        .map_err(|e| format!("Failed to create unit_owner relationship: {}", e))?;
1500
1501        Ok(unit_owner_id)
1502    }
1503
1504    #[allow(dead_code)]
1505    #[allow(clippy::too_many_arguments)]
1506    async fn create_demo_expense(
1507        &self,
1508        building_id: Uuid,
1509        organization_id: Uuid,
1510        description: &str,
1511        amount: f64,
1512        expense_date: &str,
1513        category: &str,
1514        payment_status: &str,
1515        supplier: Option<&str>,
1516        invoice_number: Option<&str>,
1517    ) -> Result<Uuid, String> {
1518        let expense_id = Uuid::new_v4();
1519        let now = Utc::now();
1520        let expense_date_parsed =
1521            chrono::DateTime::parse_from_rfc3339(&format!("{}T00:00:00Z", expense_date))
1522                .map_err(|e| format!("Failed to parse date: {}", e))?
1523                .with_timezone(&Utc);
1524
1525        // Set approval_status based on payment_status
1526        let approval_status = if payment_status == "paid" {
1527            "approved" // If already paid, it must be approved
1528        } else {
1529            "draft" // Otherwise, start as draft
1530        };
1531
1532        // Set paid_date if already paid
1533        let paid_date = if payment_status == "paid" {
1534            Some(expense_date_parsed)
1535        } else {
1536            None
1537        };
1538
1539        // Check if expense already exists (idempotency)
1540        let existing: Option<(Uuid,)> =
1541            sqlx::query_as("SELECT id FROM expenses WHERE description = $1 AND building_id = $2")
1542                .bind(description)
1543                .bind(building_id)
1544                .fetch_optional(&self.pool)
1545                .await
1546                .map_err(|e| format!("Failed to check existing expense: {}", e))?;
1547
1548        let final_expense_id = if let Some((existing_id,)) = existing {
1549            // Update existing expense
1550            sqlx::query(
1551                r#"
1552                UPDATE expenses SET
1553                    category = $1::expense_category,
1554                    amount = $2,
1555                    expense_date = $3,
1556                    payment_status = $4::payment_status,
1557                    approval_status = $5::approval_status,
1558                    paid_date = $6,
1559                    supplier = $7,
1560                    invoice_number = $8,
1561                    updated_at = $9
1562                WHERE id = $10
1563                "#,
1564            )
1565            .bind(category)
1566            .bind(amount)
1567            .bind(expense_date_parsed)
1568            .bind(payment_status)
1569            .bind(approval_status)
1570            .bind(paid_date)
1571            .bind(supplier)
1572            .bind(invoice_number)
1573            .bind(now)
1574            .bind(existing_id)
1575            .execute(&self.pool)
1576            .await
1577            .map_err(|e| format!("Failed to update expense: {}", e))?;
1578            existing_id
1579        } else {
1580            // Insert new expense
1581            sqlx::query(
1582                r#"
1583                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)
1584                VALUES ($1, $2, $3, $4::expense_category, $5, $6, $7, $8::payment_status, $9::approval_status, $10, $11, $12, $13, $14)
1585                "#
1586            )
1587            .bind(expense_id)
1588            .bind(organization_id)
1589            .bind(building_id)
1590            .bind(category)
1591            .bind(description)
1592            .bind(amount)
1593            .bind(expense_date_parsed)
1594            .bind(payment_status)
1595            .bind(approval_status)
1596            .bind(paid_date)
1597            .bind(supplier)
1598            .bind(invoice_number)
1599            .bind(now)
1600            .bind(now)
1601            .execute(&self.pool)
1602            .await
1603            .map_err(|e| format!("Failed to create expense: {}", e))?;
1604            expense_id
1605        };
1606
1607        Ok(final_expense_id)
1608    }
1609
1610    #[allow(clippy::too_many_arguments)]
1611    async fn create_demo_meeting(
1612        &self,
1613        building_id: Uuid,
1614        org_id: Uuid,
1615        title: &str,
1616        meeting_type: &str,
1617        scheduled_date: &str,
1618        status: &str,
1619    ) -> Result<Uuid, String> {
1620        let meeting_id = Uuid::new_v4();
1621        let now = Utc::now();
1622        let scheduled_date_parsed =
1623            chrono::DateTime::parse_from_rfc3339(&format!("{}T10:00:00Z", scheduled_date))
1624                .map_err(|e| format!("Failed to parse date: {}", e))?
1625                .with_timezone(&Utc);
1626
1627        let agenda_json = serde_json::json!([
1628            "Approbation des comptes",
1629            "Travaux à prévoir",
1630            "Questions diverses"
1631        ]);
1632
1633        sqlx::query(
1634            r#"
1635            INSERT INTO meetings (id, building_id, organization_id, meeting_type, title, description, scheduled_date, location, status, agenda, created_at, updated_at)
1636            VALUES ($1, $2, $3, $4::meeting_type, $5, $6, $7, $8, $9::meeting_status, $10, $11, $12)
1637            "#
1638        )
1639        .bind(meeting_id)
1640        .bind(building_id)
1641        .bind(org_id)
1642        .bind(meeting_type)
1643        .bind(title)
1644        .bind(Some("Assemblée générale annuelle"))
1645        .bind(scheduled_date_parsed)
1646        .bind("Salle polyvalente")
1647        .bind(status)
1648        .bind(agenda_json)
1649        .bind(now)
1650        .bind(now)
1651        .execute(&self.pool)
1652        .await
1653        .map_err(|e| format!("Failed to create meeting: {}", e))?;
1654
1655        Ok(meeting_id)
1656    }
1657
1658    #[allow(clippy::too_many_arguments)]
1659    async fn create_demo_document(
1660        &self,
1661        building_id: Uuid,
1662        org_id: Uuid,
1663        title: &str,
1664        document_type: &str,
1665        file_path: &str,
1666        uploaded_by: Uuid,
1667    ) -> Result<Uuid, String> {
1668        let document_id = Uuid::new_v4();
1669        let now = Utc::now();
1670
1671        sqlx::query(
1672            r#"
1673            INSERT INTO documents (id, building_id, organization_id, document_type, title, description, file_path, file_size, mime_type, uploaded_by, created_at, updated_at)
1674            VALUES ($1, $2, $3, $4::document_type, $5, $6, $7, $8, $9, $10, $11, $12)
1675            "#
1676        )
1677        .bind(document_id)
1678        .bind(building_id)
1679        .bind(org_id)
1680        .bind(document_type)
1681        .bind(title)
1682        .bind(Some("Document de démonstration"))
1683        .bind(file_path)
1684        .bind(1024_i64)
1685        .bind("application/pdf")
1686        .bind(uploaded_by)
1687        .bind(now)
1688        .bind(now)
1689        .execute(&self.pool)
1690        .await
1691        .map_err(|e| format!("Failed to create document: {}", e))?;
1692
1693        Ok(document_id)
1694    }
1695
1696    #[allow(clippy::too_many_arguments)]
1697    async fn create_demo_board_member(
1698        &self,
1699        owner_id: Uuid,
1700        building_id: Uuid,
1701        org_id: Uuid,
1702        meeting_id: Uuid,
1703        position: &str,
1704        mandate_start: &str,
1705        mandate_end: &str,
1706    ) -> Result<Uuid, String> {
1707        let board_member_id = Uuid::new_v4();
1708        let now = Utc::now();
1709
1710        let mandate_start_parsed = NaiveDate::parse_from_str(mandate_start, "%Y-%m-%d")
1711            .map_err(|e| format!("Failed to parse mandate_start date: {}", e))?
1712            .and_hms_opt(0, 0, 0)
1713            .ok_or("Failed to create datetime")?;
1714
1715        let mandate_end_parsed = NaiveDate::parse_from_str(mandate_end, "%Y-%m-%d")
1716            .map_err(|e| format!("Failed to parse mandate_end date: {}", e))?
1717            .and_hms_opt(0, 0, 0)
1718            .ok_or("Failed to create datetime")?;
1719
1720        sqlx::query(
1721            r#"
1722            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)
1723            VALUES ($1, $2, $3, $4, $5::board_position, $6, $7, $8, $9, $10, $11)
1724            "#
1725        )
1726        .bind(board_member_id)
1727        .bind(owner_id)
1728        .bind(building_id)
1729        .bind(org_id)
1730        .bind(position)
1731        .bind(mandate_start_parsed)
1732        .bind(mandate_end_parsed)
1733        .bind(meeting_id)
1734        .bind(true)
1735        .bind(now)
1736        .bind(now)
1737        .execute(&self.pool)
1738        .await
1739        .map_err(|e| format!("Failed to create board member: {}", e))?;
1740
1741        Ok(board_member_id)
1742    }
1743
1744    #[allow(clippy::too_many_arguments)]
1745    async fn create_demo_board_decision(
1746        &self,
1747        building_id: Uuid,
1748        org_id: Uuid,
1749        meeting_id: Uuid,
1750        subject: &str,
1751        decision_text: &str,
1752        deadline: Option<&str>,
1753        status: &str,
1754    ) -> Result<Uuid, String> {
1755        let decision_id = Uuid::new_v4();
1756        let now = Utc::now();
1757
1758        let deadline_parsed = if let Some(deadline_str) = deadline {
1759            Some(
1760                NaiveDate::parse_from_str(deadline_str, "%Y-%m-%d")
1761                    .map_err(|e| format!("Failed to parse deadline date: {}", e))?
1762                    .and_hms_opt(0, 0, 0)
1763                    .ok_or("Failed to create datetime")?,
1764            )
1765        } else {
1766            None
1767        };
1768
1769        sqlx::query(
1770            r#"
1771            INSERT INTO board_decisions (id, building_id, organization_id, meeting_id, subject, decision_text, deadline, status, created_at, updated_at)
1772            VALUES ($1, $2, $3, $4, $5, $6, $7, $8::decision_status, $9, $10)
1773            "#
1774        )
1775        .bind(decision_id)
1776        .bind(building_id)
1777        .bind(org_id)
1778        .bind(meeting_id)
1779        .bind(subject)
1780        .bind(decision_text)
1781        .bind(deadline_parsed)
1782        .bind(status)
1783        .bind(now)
1784        .bind(now)
1785        .execute(&self.pool)
1786        .await
1787        .map_err(|e| format!("Failed to create board decision: {}", e))?;
1788
1789        Ok(decision_id)
1790    }
1791
1792    /// Seed realistic data for load testing (optimized for 1 vCPU / 2GB RAM)
1793    /// Generates: 3 orgs, ~23 buildings, ~190 units, ~127 owners, ~60 expenses
1794    pub async fn seed_realistic_data(&self) -> Result<String, String> {
1795        log::info!("🌱 Starting realistic data seeding...");
1796
1797        // Check if data already exists
1798        let existing_orgs = sqlx::query("SELECT COUNT(*) as count FROM organizations")
1799            .fetch_one(&self.pool)
1800            .await
1801            .map_err(|e| format!("Failed to count organizations: {}", e))?;
1802
1803        let count: i64 = existing_orgs
1804            .try_get("count")
1805            .map_err(|e| format!("Failed to get count: {}", e))?;
1806        if count > 0 {
1807            return Err("Data already exists. Please clear the database first.".to_string());
1808        }
1809
1810        let mut rng = rand::rng();
1811
1812        // Belgian cities for variety
1813        let cities = [
1814            "Bruxelles",
1815            "Anvers",
1816            "Gand",
1817            "Charleroi",
1818            "Liège",
1819            "Bruges",
1820            "Namur",
1821            "Louvain",
1822        ];
1823        let street_types = ["Rue", "Avenue", "Boulevard", "Place", "Chaussée"];
1824        let street_names = [
1825            "des Fleurs",
1826            "du Parc",
1827            "de la Gare",
1828            "Royale",
1829            "de l'Église",
1830            "du Commerce",
1831            "de la Liberté",
1832            "des Arts",
1833            "Victor Hugo",
1834            "Louise",
1835        ];
1836
1837        // Create 3 organizations with different sizes
1838        let org_configs = [
1839            ("Petite Copropriété SPRL", "small", 5, 30), // 5 buildings, ~30 units
1840            ("Copropriété Moyenne SA", "medium", 8, 60), // 8 buildings, ~60 units
1841            ("Grande Résidence NV", "large", 10, 100),   // 10 buildings, ~100 units
1842        ];
1843
1844        let mut total_buildings = 0;
1845        let mut total_units = 0;
1846        let mut total_owners = 0;
1847        let mut total_expenses = 0;
1848
1849        for (idx, (org_name, size, num_buildings, target_units)) in org_configs.iter().enumerate() {
1850            let org_id = Uuid::new_v4();
1851            let now = Utc::now();
1852
1853            log::info!(
1854                "📍 Organization {}: {} ({} buildings, ~{} units)",
1855                idx + 1,
1856                org_name,
1857                num_buildings,
1858                target_units
1859            );
1860
1861            // Create organization
1862            sqlx::query(
1863                "INSERT INTO organizations (id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at, updated_at)
1864                 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)"
1865            )
1866            .bind(org_id)
1867            .bind(*org_name)
1868            .bind(format!("{}-{}", size, idx))
1869            .bind(format!("contact@{}.be", size))
1870            .bind(format!("+32 2 {} {} {}", rng.random_range(100..999), rng.random_range(10..99), rng.random_range(10..99)))
1871            .bind(if *size == "large" { "enterprise" } else if *size == "medium" { "professional" } else { "starter" })
1872            .bind(*num_buildings)
1873            .bind(if *size == "large" { 50 } else if *size == "medium" { 20 } else { 10 })
1874            .bind(true)
1875            .bind(now)
1876            .bind(now)
1877            .execute(&self.pool)
1878            .await
1879            .map_err(|e| format!("Failed to create organization: {}", e))?;
1880
1881            // Create admin user for this org
1882            let user_id = Uuid::new_v4();
1883            let password_hash = hash("admin123", DEFAULT_COST)
1884                .map_err(|e| format!("Failed to hash password: {}", e))?;
1885
1886            sqlx::query(
1887                "INSERT INTO users (id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at)
1888                 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"
1889            )
1890            .bind(user_id)
1891            .bind(format!("admin@{}.be", size))
1892            .bind(&password_hash)
1893            .bind("Admin")
1894            .bind(org_name.split_whitespace().next().unwrap_or("User"))
1895            .bind("syndic")
1896            .bind(Some(org_id))
1897            .bind(true)
1898            .bind(now)
1899            .bind(now)
1900            .execute(&self.pool)
1901            .await
1902            .map_err(|e| format!("Failed to create user: {}", e))?;
1903
1904            // Create owners pool for this org
1905            let num_owners = (target_units * 2 / 3) as usize; // ~66% occupancy
1906            let mut owner_ids = Vec::new();
1907
1908            for o in 0..num_owners {
1909                let owner_id = Uuid::new_v4();
1910
1911                // Use faker for realistic Belgian data
1912                let first_name: String = FirstName().fake();
1913                let last_name: String = LastName().fake();
1914                let street: String = StreetName().fake();
1915                let city_idx = rng.random_range(0..cities.len());
1916                let owner_city = cities[city_idx];
1917
1918                sqlx::query(
1919                    "INSERT INTO owners (id, organization_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at)
1920                     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)"
1921                )
1922                .bind(owner_id)
1923                .bind(org_id)
1924                .bind(&first_name)
1925                .bind(&last_name)
1926                .bind(format!("{}. {}{}@{}.be", first_name.chars().next().unwrap_or('x'), last_name.to_lowercase(), o + 1, size))
1927                .bind(format!("+32 {} {} {} {}",
1928                    if rng.random_bool(0.5) { "2" } else { "4" },
1929                    rng.random_range(100..999),
1930                    rng.random_range(10..99),
1931                    rng.random_range(10..99)
1932                ))
1933                .bind(format!("{} {}", street, rng.random_range(1..200)))
1934                .bind(owner_city)
1935                .bind(format!("{}", rng.random_range(1000..9999)))
1936                .bind("Belgium")
1937                .bind(now)
1938                .bind(now)
1939                .execute(&self.pool)
1940                .await
1941                .map_err(|e| format!("Failed to create owner: {}", e))?;
1942
1943                owner_ids.push(owner_id);
1944            }
1945
1946            total_owners += num_owners;
1947
1948            // Create buildings for this org
1949            let units_per_building = target_units / num_buildings;
1950            let mut org_units = 0;
1951
1952            for b in 0..*num_buildings {
1953                let building_id = Uuid::new_v4();
1954                let city = cities[rng.random_range(0..cities.len())];
1955                let street_type = street_types[rng.random_range(0..street_types.len())];
1956                let street_name = street_names[rng.random_range(0..street_names.len())];
1957                let building_name = format!("Résidence {}", street_name);
1958
1959                sqlx::query(
1960                    "INSERT INTO buildings (id, organization_id, name, address, city, postal_code, country, total_units, construction_year, created_at, updated_at)
1961                     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)"
1962                )
1963                .bind(building_id)
1964                .bind(org_id)
1965                .bind(&building_name)
1966                .bind(format!("{} {} {}", street_type, street_name, rng.random_range(1..200)))
1967                .bind(city)
1968                .bind(format!("{}", rng.random_range(1000..9999)))
1969                .bind("Belgium")
1970                .bind(units_per_building)
1971                .bind(rng.random_range(1960..2024))
1972                .bind(now)
1973                .bind(now)
1974                .execute(&self.pool)
1975                .await
1976                .map_err(|e| format!("Failed to create building: {}", e))?;
1977
1978                // Create units for this building
1979                let units_this_building = if b == num_buildings - 1 {
1980                    // Last building gets remainder
1981                    target_units - org_units
1982                } else {
1983                    units_per_building
1984                };
1985
1986                for u in 0..units_this_building {
1987                    let floor = u / 4; // 4 units per floor
1988                    let unit_number = format!("{}.{}", floor, (u % 4) + 1);
1989
1990                    // 66% chance to have an owner
1991                    let owner_id = if rng.random_bool(0.66) && !owner_ids.is_empty() {
1992                        Some(owner_ids[rng.random_range(0..owner_ids.len())])
1993                    } else {
1994                        None
1995                    };
1996
1997                    // Valid unit_type ENUM values: apartment, parking, cellar, commercial, other
1998                    let unit_types = ["apartment", "apartment", "apartment", "parking", "cellar"];
1999                    let unit_type = unit_types[rng.random_range(0..unit_types.len())];
2000
2001                    sqlx::query(
2002                        "INSERT INTO units (id, organization_id, building_id, unit_number, unit_type, floor, surface_area, quota, owner_id, created_at, updated_at)
2003                         VALUES ($1, $2, $3, $4, $5::unit_type, $6, $7, $8, $9, $10, $11)"
2004                    )
2005                    .bind(Uuid::new_v4())
2006                    .bind(org_id)
2007                    .bind(building_id)
2008                    .bind(&unit_number)
2009                    .bind(unit_type)
2010                    .bind(floor)
2011                    .bind(rng.random_range(45.0..150.0))
2012                    .bind(rng.random_range(50..200) as i32)
2013                    .bind(owner_id)
2014                    .bind(now)
2015                    .bind(now)
2016                    .execute(&self.pool)
2017                    .await
2018                    .map_err(|e| format!("Failed to create unit: {}", e))?;
2019                }
2020
2021                org_units += units_this_building;
2022
2023                // Check if expenses already exist for this building (idempotency)
2024                let existing_expenses: (i64,) =
2025                    sqlx::query_as("SELECT COUNT(*) FROM expenses WHERE building_id = $1")
2026                        .bind(building_id)
2027                        .fetch_one(&self.pool)
2028                        .await
2029                        .map_err(|e| format!("Failed to check existing expenses: {}", e))?;
2030
2031                // Only create random expenses if none exist yet
2032                if existing_expenses.0 == 0 {
2033                    // Create 2-3 expenses per building
2034                    let num_expenses = rng.random_range(2..=3);
2035                    let expense_types = [
2036                        ("Entretien ascenseur", 450.0, 800.0),
2037                        ("Nettoyage parties communes", 300.0, 600.0),
2038                        ("Chauffage collectif", 1500.0, 3000.0),
2039                        ("Assurance immeuble", 800.0, 1500.0),
2040                        ("Travaux façade", 5000.0, 15000.0),
2041                    ];
2042
2043                    for _ in 0..num_expenses {
2044                        let (desc, min_amount, max_amount) =
2045                            expense_types[rng.random_range(0..expense_types.len())];
2046                        let amount = rng.random_range(min_amount..max_amount);
2047                        let days_ago = rng.random_range(0..90);
2048                        let expense_date = Utc::now() - chrono::Duration::days(days_ago);
2049
2050                        // Valid expense_category ENUM: maintenance, repairs, insurance, utilities, cleaning, administration, works, other
2051                        let categories = [
2052                            "maintenance",
2053                            "repairs",
2054                            "insurance",
2055                            "utilities",
2056                            "cleaning",
2057                            "administration",
2058                            "works",
2059                        ];
2060                        let category = categories[rng.random_range(0..categories.len())];
2061
2062                        // Valid payment_status ENUM: pending, paid, overdue, cancelled
2063                        let payment_status = if rng.random_bool(0.7) {
2064                            "paid"
2065                        } else {
2066                            "pending"
2067                        };
2068
2069                        // Set approval_status based on payment_status
2070                        let approval_status = if payment_status == "paid" {
2071                            "approved" // If already paid, it must be approved
2072                        } else {
2073                            "draft" // Otherwise, start as draft
2074                        };
2075
2076                        // Set paid_date if already paid
2077                        let paid_date = if payment_status == "paid" {
2078                            Some(expense_date)
2079                        } else {
2080                            None
2081                        };
2082
2083                        sqlx::query(
2084                        "INSERT INTO expenses (id, organization_id, building_id, category, description, amount, expense_date, payment_status, approval_status, paid_date, created_at, updated_at)
2085                         VALUES ($1, $2, $3, $4::expense_category, $5, $6, $7, $8::payment_status, $9::approval_status, $10, $11, $12)"
2086                    )
2087                    .bind(Uuid::new_v4())
2088                    .bind(org_id)
2089                    .bind(building_id)
2090                    .bind(category)
2091                    .bind(desc)
2092                    .bind(amount)
2093                    .bind(expense_date)
2094                    .bind(payment_status)
2095                    .bind(approval_status)
2096                    .bind(paid_date)
2097                    .bind(now)
2098                    .bind(now)
2099                    .execute(&self.pool)
2100                    .await
2101                    .map_err(|e| format!("Failed to create expense: {}", e))?;
2102
2103                        total_expenses += 1;
2104                    }
2105                } // End if existing_expenses.0 == 0
2106            }
2107
2108            total_buildings += num_buildings;
2109            total_units += org_units as usize;
2110
2111            log::info!(
2112                "  ✅ Created {} buildings, {} units, {} owners",
2113                num_buildings,
2114                org_units,
2115                num_owners
2116            );
2117        }
2118
2119        Ok(format!(
2120            "✅ Realistic seed data created successfully!\n\
2121             Total: {} orgs, {} buildings, {} units, {} owners, {} expenses\n\
2122             \nTest credentials:\n\
2123             - Small org:  admin@small.be / admin123\n\
2124             - Medium org: admin@medium.be / admin123\n\
2125             - Large org:  admin@large.be / admin123",
2126            org_configs.len(),
2127            total_buildings,
2128            total_units,
2129            total_owners,
2130            total_expenses
2131        ))
2132    }
2133
2134    /// Seed Belgian PCMN accounts for an organization
2135    async fn seed_pcmn_accounts(&self, organization_id: Uuid) -> Result<(), String> {
2136        // Call the existing account seeding endpoint logic
2137        // We'll seed the essential accounts for demo purposes
2138        let accounts = vec![
2139            // Class 6: Charges (Expenses)
2140            ("6100", "Charges courantes", "EXPENSE"),
2141            ("6110", "Entretien et réparations", "EXPENSE"),
2142            ("6120", "Personnel", "EXPENSE"),
2143            ("6130", "Services extérieurs", "EXPENSE"),
2144            ("6140", "Honoraires et commissions", "EXPENSE"),
2145            ("6150", "Assurances", "EXPENSE"),
2146            ("6200", "Travaux extraordinaires", "EXPENSE"),
2147            // Class 7: Produits (Revenue)
2148            ("7000", "Appels de fonds ordinaires", "REVENUE"),
2149            ("7100", "Appels de fonds extraordinaires", "REVENUE"),
2150            ("7200", "Autres produits", "REVENUE"),
2151            // Class 4: Créances et dettes (Assets/Liabilities)
2152            ("4000", "Copropriétaires débiteurs", "ASSET"),
2153            ("4110", "TVA à récupérer", "ASSET"),
2154            ("4400", "Fournisseurs", "LIABILITY"),
2155            ("4500", "TVA à payer", "LIABILITY"),
2156            // Class 5: Trésorerie (Assets)
2157            ("5500", "Banque compte courant", "ASSET"),
2158            ("5700", "Caisse", "ASSET"),
2159        ];
2160
2161        let now = Utc::now();
2162
2163        for (code, label, account_type_str) in accounts {
2164            sqlx::query(
2165                r#"
2166                INSERT INTO accounts (id, code, label, parent_code, account_type, direct_use, organization_id, created_at, updated_at)
2167                VALUES ($1, $2, $3, $4, $5::account_type, $6, $7, $8, $9)
2168                ON CONFLICT (code, organization_id) DO NOTHING
2169                "#
2170            )
2171            .bind(Uuid::new_v4())
2172            .bind(code)
2173            .bind(label)
2174            .bind(None::<String>) // parent_code
2175            .bind(account_type_str)
2176            .bind(true) // direct_use
2177            .bind(organization_id)
2178            .bind(now)
2179            .bind(now)
2180            .execute(&self.pool)
2181            .await
2182            .map_err(|e| format!("Failed to seed account {}: {}", code, e))?;
2183        }
2184
2185        Ok(())
2186    }
2187
2188    /// Create a demo expense with VAT calculation
2189    #[allow(clippy::too_many_arguments)]
2190    async fn create_demo_expense_with_vat(
2191        &self,
2192        building_id: Uuid,
2193        organization_id: Uuid,
2194        description: &str,
2195        amount_excl_vat: f64,
2196        vat_rate: f64,
2197        expense_date: &str,
2198        due_date: &str,
2199        category: &str,
2200        payment_status: &str,
2201        supplier: Option<&str>,
2202        invoice_number: Option<&str>,
2203        account_code: Option<&str>,
2204    ) -> Result<Uuid, String> {
2205        let expense_id = Uuid::new_v4();
2206        let now = Utc::now();
2207
2208        // Calculate VAT and total
2209        let vat_amount = (amount_excl_vat * vat_rate / 100.0 * 100.0).round() / 100.0;
2210        let amount = amount_excl_vat + vat_amount;
2211
2212        let expense_date_parsed =
2213            chrono::DateTime::parse_from_rfc3339(&format!("{}T00:00:00Z", expense_date))
2214                .map_err(|e| format!("Failed to parse expense_date: {}", e))?
2215                .with_timezone(&Utc);
2216
2217        let due_date_parsed =
2218            chrono::DateTime::parse_from_rfc3339(&format!("{}T00:00:00Z", due_date))
2219                .map_err(|e| format!("Failed to parse due_date: {}", e))?
2220                .with_timezone(&Utc);
2221
2222        // Set paid_date if payment_status is "paid"
2223        let paid_date = if payment_status == "paid" {
2224            Some(expense_date_parsed) // Use expense_date as paid_date
2225        } else {
2226            None
2227        };
2228
2229        // Check if expense already exists (idempotency)
2230        let existing: Option<(Uuid,)> =
2231            sqlx::query_as("SELECT id FROM expenses WHERE description = $1 AND building_id = $2")
2232                .bind(description)
2233                .bind(building_id)
2234                .fetch_optional(&self.pool)
2235                .await
2236                .map_err(|e| format!("Failed to check existing expense: {}", e))?;
2237
2238        let expense_id = if let Some((existing_id,)) = existing {
2239            // Update existing expense
2240            sqlx::query(
2241                r#"
2242                UPDATE expenses SET
2243                    category = $1::expense_category,
2244                    amount = $2,
2245                    amount_excl_vat = $3,
2246                    vat_rate = $4,
2247                    expense_date = $5,
2248                    due_date = $6,
2249                    payment_status = $7::payment_status,
2250                    paid_date = $8,
2251                    approval_status = $9::approval_status,
2252                    supplier = $10,
2253                    invoice_number = $11,
2254                    account_code = $12,
2255                    updated_at = $13
2256                WHERE id = $14
2257                "#,
2258            )
2259            .bind(category)
2260            .bind(amount)
2261            .bind(amount_excl_vat)
2262            .bind(vat_rate)
2263            .bind(expense_date_parsed)
2264            .bind(due_date_parsed)
2265            .bind(payment_status)
2266            .bind(paid_date)
2267            .bind("approved")
2268            .bind(supplier)
2269            .bind(invoice_number)
2270            .bind(account_code)
2271            .bind(now)
2272            .bind(existing_id)
2273            .execute(&self.pool)
2274            .await
2275            .map_err(|e| format!("Failed to update expense: {}", e))?;
2276            existing_id
2277        } else {
2278            // Insert new expense
2279            sqlx::query(
2280                r#"
2281                INSERT INTO expenses (
2282                    id, organization_id, building_id, category, description,
2283                    amount, amount_excl_vat, vat_rate, expense_date, due_date,
2284                    payment_status, paid_date, approval_status, supplier, invoice_number,
2285                    account_code, created_at, updated_at
2286                )
2287                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)
2288                "#
2289            )
2290            .bind(expense_id)
2291            .bind(organization_id)
2292            .bind(building_id)
2293            .bind(category)
2294            .bind(description)
2295            .bind(amount)
2296            .bind(amount_excl_vat)
2297            .bind(vat_rate)
2298            .bind(expense_date_parsed)
2299            .bind(due_date_parsed)
2300            .bind(payment_status)
2301            .bind(paid_date)
2302            .bind("approved")
2303            .bind(supplier)
2304            .bind(invoice_number)
2305            .bind(account_code)
2306            .bind(now)
2307            .bind(now)
2308            .execute(&self.pool)
2309            .await
2310            .map_err(|e| format!("Failed to create expense with VAT: {}", e))?;
2311            expense_id
2312        };
2313
2314        // Generate journal entry for this expense (double-entry bookkeeping)
2315        if let Some(acc_code) = account_code {
2316            self.generate_journal_entry_for_expense(
2317                expense_id,
2318                organization_id,
2319                building_id,
2320                description,
2321                amount_excl_vat,
2322                vat_rate,
2323                amount,
2324                expense_date_parsed,
2325                acc_code,
2326                supplier,
2327                invoice_number,
2328            )
2329            .await?;
2330        }
2331
2332        Ok(expense_id)
2333    }
2334
2335    /// Generate journal entry for an expense (double-entry bookkeeping)
2336    ///
2337    /// This creates the accounting entries following Belgian PCMN:
2338    /// - Debit: Expense account (class 6)
2339    /// - Debit: VAT recoverable (4110)
2340    /// - Credit: Supplier account (4400)
2341    #[allow(clippy::too_many_arguments)]
2342    async fn generate_journal_entry_for_expense(
2343        &self,
2344        expense_id: Uuid,
2345        organization_id: Uuid,
2346        _building_id: Uuid,
2347        description: &str,
2348        amount_excl_vat: f64,
2349        vat_rate: f64,
2350        total_amount: f64,
2351        expense_date: chrono::DateTime<Utc>,
2352        account_code: &str,
2353        supplier: Option<&str>,
2354        invoice_number: Option<&str>,
2355    ) -> Result<(), String> {
2356        let journal_entry_id = Uuid::new_v4();
2357        let now = Utc::now();
2358
2359        // Calculate VAT amount
2360        let vat_amount = total_amount - amount_excl_vat;
2361
2362        // Start a transaction - the deferred trigger will only check at COMMIT
2363        let mut tx = self
2364            .pool
2365            .begin()
2366            .await
2367            .map_err(|e| format!("Failed to begin transaction: {}", e))?;
2368
2369        // Insert journal entry header
2370        sqlx::query!(
2371            r#"
2372            INSERT INTO journal_entries (
2373                id, organization_id, entry_date, description,
2374                document_ref, expense_id, created_at, updated_at
2375            )
2376            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
2377            "#,
2378            journal_entry_id,
2379            organization_id,
2380            expense_date,
2381            format!("{} - {}", description, supplier.unwrap_or("Fournisseur")),
2382            invoice_number,
2383            expense_id,
2384            now,
2385            now
2386        )
2387        .execute(&mut *tx)
2388        .await
2389        .map_err(|e| format!("Failed to create journal entry: {}", e))?;
2390
2391        // Line 1: Debit expense account (class 6)
2392        sqlx::query!(
2393            r#"
2394            INSERT INTO journal_entry_lines (
2395                journal_entry_id, organization_id, account_code,
2396                debit, credit, description
2397            )
2398            VALUES ($1, $2, $3, $4, $5, $6)
2399            "#,
2400            journal_entry_id,
2401            organization_id,
2402            account_code,
2403            rust_decimal::Decimal::from_f64_retain(amount_excl_vat).unwrap_or_default(),
2404            rust_decimal::Decimal::from_f64_retain(0.0).unwrap_or_default(),
2405            format!("Dépense: {}", description)
2406        )
2407        .execute(&mut *tx)
2408        .await
2409        .map_err(|e| format!("Failed to create expense debit line: {}", e))?;
2410
2411        // Line 2: Debit VAT recoverable (4110) if VAT > 0
2412        if vat_amount > 0.01 {
2413            sqlx::query!(
2414                r#"
2415                INSERT INTO journal_entry_lines (
2416                    journal_entry_id, organization_id, account_code,
2417                    debit, credit, description
2418                )
2419                VALUES ($1, $2, $3, $4, $5, $6)
2420                "#,
2421                journal_entry_id,
2422                organization_id,
2423                "4110", // VAT Recoverable account
2424                rust_decimal::Decimal::from_f64_retain(vat_amount).unwrap_or_default(),
2425                rust_decimal::Decimal::from_f64_retain(0.0).unwrap_or_default(),
2426                format!("TVA récupérable {}%", vat_rate)
2427            )
2428            .execute(&mut *tx)
2429            .await
2430            .map_err(|e| format!("Failed to create VAT debit line: {}", e))?;
2431        }
2432
2433        // Line 3: Credit supplier account (4400)
2434        sqlx::query!(
2435            r#"
2436            INSERT INTO journal_entry_lines (
2437                journal_entry_id, organization_id, account_code,
2438                debit, credit, description
2439            )
2440            VALUES ($1, $2, $3, $4, $5, $6)
2441            "#,
2442            journal_entry_id,
2443            organization_id,
2444            "4400", // Suppliers account
2445            rust_decimal::Decimal::from_f64_retain(0.0).unwrap_or_default(),
2446            rust_decimal::Decimal::from_f64_retain(total_amount).unwrap_or_default(),
2447            supplier.map(|s| format!("Fournisseur: {}", s))
2448        )
2449        .execute(&mut *tx)
2450        .await
2451        .map_err(|e| format!("Failed to create supplier credit line: {}", e))?;
2452
2453        // Commit transaction - trigger will validate balance here
2454        tx.commit()
2455            .await
2456            .map_err(|e| format!("Failed to commit journal entry transaction: {}", e))?;
2457
2458        Ok(())
2459    }
2460
2461    /// Create demo charge distributions for an expense
2462    async fn create_demo_distributions(
2463        &self,
2464        expense_id: Uuid,
2465        _organization_id: Uuid,
2466    ) -> Result<(), String> {
2467        // Get all units for the expense's building
2468        let expense_row = sqlx::query!(
2469            "SELECT building_id, amount FROM expenses WHERE id = $1",
2470            expense_id
2471        )
2472        .fetch_one(&self.pool)
2473        .await
2474        .map_err(|e| format!("Failed to fetch expense: {}", e))?;
2475
2476        let building_id = expense_row.building_id;
2477        let total_amount: Decimal = expense_row.amount;
2478
2479        // Get all units with their quotas (NOT unit_owners - one record per unit)
2480        let units = sqlx::query!(
2481            r#"
2482            SELECT u.id as unit_id, u.quota
2483            FROM units u
2484            WHERE u.building_id = $1
2485            "#,
2486            building_id
2487        )
2488        .fetch_all(&self.pool)
2489        .await
2490        .map_err(|e| format!("Failed to fetch units: {}", e))?;
2491
2492        if units.is_empty() {
2493            return Ok(()); // No units to distribute to
2494        }
2495
2496        // Calculate total quotas for the building
2497        let total_quota: f64 = units.iter().map(|u| u.quota).sum();
2498
2499        let now = Utc::now();
2500
2501        // Create ONE distribution per unit (not per owner)
2502        // The primary owner will be responsible for collecting from co-owners
2503        for unit in units {
2504            // Get the primary contact owner for this unit
2505            let primary_owner = sqlx::query!(
2506                r#"
2507                SELECT owner_id
2508                FROM unit_owners
2509                WHERE unit_id = $1 AND end_date IS NULL AND is_primary_contact = true
2510                ORDER BY created_at ASC
2511                LIMIT 1
2512                "#,
2513                unit.unit_id
2514            )
2515            .fetch_optional(&self.pool)
2516            .await
2517            .map_err(|e| format!("Failed to fetch primary owner: {}", e))?;
2518
2519            // Skip if no owner found for this unit
2520            let owner_id = match primary_owner {
2521                Some(owner) => owner.owner_id,
2522                None => continue, // Skip this unit if no owner
2523            };
2524
2525            let quota_percentage = if total_quota > 0.0 {
2526                unit.quota / total_quota
2527            } else {
2528                0.0
2529            };
2530
2531            let amount_due: Decimal = if total_quota > 0.0 {
2532                let qp = Decimal::from_f64(quota_percentage).unwrap_or(Decimal::ZERO);
2533                (qp * total_amount).round_dp(2)
2534            } else {
2535                Decimal::ZERO
2536            };
2537
2538            sqlx::query(
2539                r#"
2540                INSERT INTO charge_distributions (
2541                    id, expense_id, unit_id, owner_id,
2542                    quota_percentage, amount_due, created_at
2543                )
2544                VALUES ($1, $2, $3, $4, $5, $6, $7)
2545                "#,
2546            )
2547            .bind(Uuid::new_v4())
2548            .bind(expense_id)
2549            .bind(unit.unit_id)
2550            .bind(owner_id)
2551            .bind(quota_percentage)
2552            .bind(amount_due)
2553            .bind(now)
2554            .execute(&self.pool)
2555            .await
2556            .map_err(|e| format!("Failed to create charge distribution: {}", e))?;
2557        }
2558
2559        Ok(())
2560    }
2561
2562    /// Create a demo owner contribution (revenue)
2563    #[allow(clippy::too_many_arguments)]
2564    async fn create_demo_owner_contribution(
2565        &self,
2566        organization_id: Uuid,
2567        owner_id: Uuid,
2568        unit_id: Option<Uuid>,
2569        description: &str,
2570        amount: f64,
2571        contribution_type: &str,
2572        contribution_date: &str,
2573        payment_status: &str,
2574        payment_date: Option<&str>,
2575        account_code: Option<&str>,
2576    ) -> Result<Uuid, String> {
2577        let contribution_id = Uuid::new_v4();
2578        let contribution_date = NaiveDate::parse_from_str(contribution_date, "%Y-%m-%d")
2579            .map_err(|e| format!("Invalid contribution date: {}", e))?
2580            .and_hms_opt(10, 0, 0)
2581            .ok_or("Invalid contribution time")?
2582            .and_local_timezone(Utc)
2583            .unwrap();
2584
2585        let payment_date_tz = payment_date
2586            .map(|date_str| {
2587                NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
2588                    .map_err(|e| format!("Invalid payment date: {}", e))
2589                    .and_then(|date| {
2590                        date.and_hms_opt(10, 0, 0)
2591                            .ok_or("Invalid payment time".to_string())
2592                    })
2593                    .map(|dt| dt.and_local_timezone(Utc).unwrap())
2594            })
2595            .transpose()?;
2596
2597        let payment_method = if payment_status == "paid" {
2598            Some("bank_transfer")
2599        } else {
2600            None
2601        };
2602
2603        let now = Utc::now();
2604
2605        sqlx::query(
2606            r#"
2607            INSERT INTO owner_contributions (
2608                id, organization_id, owner_id, unit_id,
2609                description, amount, account_code,
2610                contribution_type, contribution_date, payment_date,
2611                payment_method, payment_status,
2612                created_at, updated_at
2613            )
2614            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
2615            "#,
2616        )
2617        .bind(contribution_id)
2618        .bind(organization_id)
2619        .bind(owner_id)
2620        .bind(unit_id)
2621        .bind(description)
2622        .bind(amount)
2623        .bind(account_code)
2624        .bind(contribution_type)
2625        .bind(contribution_date)
2626        .bind(payment_date_tz)
2627        .bind(payment_method)
2628        .bind(payment_status)
2629        .bind(now)
2630        .bind(now)
2631        .execute(&self.pool)
2632        .await
2633        .map_err(|e| format!("Failed to create owner contribution: {}", e))?;
2634
2635        // Generate journal entry for this contribution (double-entry bookkeeping)
2636        if let Some(acc_code) = account_code {
2637            self.generate_journal_entry_for_contribution(
2638                contribution_id,
2639                organization_id,
2640                description,
2641                amount,
2642                contribution_date,
2643                acc_code,
2644            )
2645            .await?;
2646        }
2647
2648        Ok(contribution_id)
2649    }
2650
2651    /// Generate journal entry for an owner contribution (double-entry bookkeeping)
2652    ///
2653    /// This creates the accounting entries following Belgian PCMN:
2654    /// - Debit: Owner receivables (4000) - Money owed by owner
2655    /// - Credit: Revenue account (class 7) - Income for ACP
2656    async fn generate_journal_entry_for_contribution(
2657        &self,
2658        contribution_id: Uuid,
2659        organization_id: Uuid,
2660        description: &str,
2661        amount: f64,
2662        contribution_date: chrono::DateTime<Utc>,
2663        account_code: &str,
2664    ) -> Result<(), String> {
2665        let journal_entry_id = Uuid::new_v4();
2666        let now = Utc::now();
2667
2668        // Start a transaction with deferred constraints
2669        let mut tx = self
2670            .pool
2671            .begin()
2672            .await
2673            .map_err(|e| format!("Failed to begin transaction: {}", e))?;
2674
2675        // Set constraints to deferred for this transaction
2676        sqlx::query("SET CONSTRAINTS ALL DEFERRED")
2677            .execute(&mut *tx)
2678            .await
2679            .map_err(|e| format!("Failed to defer constraints: {}", e))?;
2680
2681        // Create journal entry header
2682        sqlx::query(
2683            r#"
2684            INSERT INTO journal_entries (
2685                id, organization_id, entry_date, description,
2686                contribution_id, created_at, updated_at
2687            )
2688            VALUES ($1, $2, $3, $4, $5, $6, $7)
2689            "#,
2690        )
2691        .bind(journal_entry_id)
2692        .bind(organization_id)
2693        .bind(contribution_date)
2694        .bind(description)
2695        .bind(contribution_id)
2696        .bind(now)
2697        .bind(now)
2698        .execute(&mut *tx)
2699        .await
2700        .map_err(|e| format!("Failed to create journal entry: {}", e))?;
2701
2702        // Line 1: DEBIT - Owner receivables (4000 = Copropriétaires débiteurs)
2703        sqlx::query(
2704            r#"
2705            INSERT INTO journal_entry_lines (
2706                id, journal_entry_id, organization_id, account_code,
2707                description, debit, credit, created_at
2708            )
2709            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
2710            "#,
2711        )
2712        .bind(Uuid::new_v4())
2713        .bind(journal_entry_id)
2714        .bind(organization_id)
2715        .bind("4000") // Owner receivables
2716        .bind(format!("Créance - {}", description))
2717        .bind(amount) // Debit
2718        .bind(0.0) // Credit
2719        .bind(now)
2720        .execute(&mut *tx)
2721        .await
2722        .map_err(|e| format!("Failed to create debit line (4000): {}", e))?;
2723
2724        // Line 2: CREDIT - Revenue account (class 7)
2725        sqlx::query(
2726            r#"
2727            INSERT INTO journal_entry_lines (
2728                id, journal_entry_id, organization_id, account_code,
2729                description, debit, credit, created_at
2730            )
2731            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
2732            "#,
2733        )
2734        .bind(Uuid::new_v4())
2735        .bind(journal_entry_id)
2736        .bind(organization_id)
2737        .bind(account_code) // Revenue account (e.g., 7000)
2738        .bind(format!("Produit - {}", description))
2739        .bind(0.0) // Debit
2740        .bind(amount) // Credit
2741        .bind(now)
2742        .execute(&mut *tx)
2743        .await
2744        .map_err(|e| format!("Failed to create credit line ({}): {}", account_code, e))?;
2745
2746        // Commit transaction (constraints will be checked here)
2747        tx.commit()
2748            .await
2749            .map_err(|e| format!("Failed to commit transaction: {}", e))?;
2750
2751        Ok(())
2752    }
2753
2754    /// Create a demo payment reminder
2755    #[allow(clippy::too_many_arguments)]
2756    async fn create_demo_payment_reminder(
2757        &self,
2758        expense_id: Uuid,
2759        owner_id: Uuid,
2760        organization_id: Uuid,
2761        reminder_level: &str,
2762        days_overdue: i64,
2763    ) -> Result<Uuid, String> {
2764        let reminder_id = Uuid::new_v4();
2765        let now = Utc::now();
2766
2767        // Get expense amount and due date
2768        let expense = sqlx::query!(
2769            "SELECT amount, due_date FROM expenses WHERE id = $1",
2770            expense_id
2771        )
2772        .fetch_one(&self.pool)
2773        .await
2774        .map_err(|e| format!("Failed to fetch expense: {}", e))?;
2775
2776        let amount_owed: Decimal = expense.amount;
2777        let due_date = expense
2778            .due_date
2779            .expect("Due date required for payment reminder");
2780
2781        // Calculate penalty (8% annual rate)
2782        let penalty_amount: Decimal = if days_overdue > 0 {
2783            let yearly_penalty = amount_owed * Decimal::new(8, 2); // 0.08
2784            let daily_penalty = yearly_penalty / Decimal::from(365);
2785            (daily_penalty * Decimal::from(days_overdue)).round_dp(2)
2786        } else {
2787            Decimal::ZERO
2788        };
2789
2790        let total_amount = amount_owed + penalty_amount;
2791        let sent_date = now - chrono::Duration::days(5); // Sent 5 days ago
2792
2793        sqlx::query(
2794            r#"
2795            INSERT INTO payment_reminders (
2796                id, organization_id, expense_id, owner_id,
2797                level, status, amount_owed, penalty_amount, total_amount,
2798                due_date, days_overdue, delivery_method, sent_date,
2799                created_at, updated_at
2800            )
2801            VALUES ($1, $2, $3, $4, $5::reminder_level, $6::reminder_status, $7, $8, $9, $10, $11, $12::delivery_method, $13, $14, $15)
2802            "#
2803        )
2804        .bind(reminder_id)
2805        .bind(organization_id)
2806        .bind(expense_id)
2807        .bind(owner_id)
2808        .bind(reminder_level) // FirstReminder, SecondReminder, etc.
2809        .bind("Sent") // status
2810        .bind(amount_owed)
2811        .bind(penalty_amount)
2812        .bind(total_amount)
2813        .bind(due_date)
2814        .bind(days_overdue as i32)
2815        .bind("Email") // delivery_method
2816        .bind(sent_date)
2817        .bind(now)
2818        .bind(now)
2819        .execute(&self.pool)
2820        .await
2821        .map_err(|e| format!("Failed to create payment reminder: {}", e))?;
2822
2823        Ok(reminder_id)
2824    }
2825
2826    /// Clear all data (DANGEROUS - use with caution!)
2827    pub async fn clear_demo_data(&self) -> Result<String, String> {
2828        log::warn!("⚠️  Clearing seed data only (preserving production data)...");
2829
2830        // Get seed organization IDs
2831        let seed_org_ids: Vec<Uuid> =
2832            sqlx::query_scalar!("SELECT id FROM organizations WHERE is_seed_data = true")
2833                .fetch_all(&self.pool)
2834                .await
2835                .map_err(|e| format!("Failed to fetch seed organizations: {}", e))?;
2836
2837        if seed_org_ids.is_empty() {
2838            return Ok("ℹ️  No seed data found to clear.".to_string());
2839        }
2840
2841        log::info!("Found {} seed organizations to clean", seed_org_ids.len());
2842
2843        // Delete in correct order due to foreign key constraints
2844        // 1. Board decisions (reference board_members and meetings)
2845        sqlx::query!(
2846            "DELETE FROM board_decisions WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2847            &seed_org_ids
2848        )
2849        .execute(&self.pool)
2850        .await
2851        .map_err(|e| format!("Failed to delete board_decisions: {}", e))?;
2852
2853        // 2. Board members (reference meetings)
2854        sqlx::query!(
2855            "DELETE FROM board_members WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2856            &seed_org_ids
2857        )
2858        .execute(&self.pool)
2859        .await
2860        .map_err(|e| format!("Failed to delete board_members: {}", e))?;
2861
2862        // 3. Payment reminders (reference expenses and owners)
2863        sqlx::query!(
2864            "DELETE FROM payment_reminders WHERE organization_id = ANY($1)",
2865            &seed_org_ids
2866        )
2867        .execute(&self.pool)
2868        .await
2869        .map_err(|e| format!("Failed to delete payment_reminders: {}", e))?;
2870
2871        // 3b. Owner contributions (revenue)
2872        sqlx::query!(
2873            "DELETE FROM owner_contributions WHERE organization_id = ANY($1)",
2874            &seed_org_ids
2875        )
2876        .execute(&self.pool)
2877        .await
2878        .map_err(|e| format!("Failed to delete owner_contributions: {}", e))?;
2879
2880        // 4. Charge distributions (reference expenses)
2881        sqlx::query(
2882            "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)))"
2883        )
2884        .bind(&seed_org_ids)
2885        .execute(&self.pool)
2886        .await
2887        .map_err(|e| format!("Failed to delete charge_distributions: {}", e))?;
2888
2889        // 5. Invoice line items (reference expenses)
2890        sqlx::query(
2891            "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)))"
2892        )
2893        .bind(&seed_org_ids)
2894        .execute(&self.pool)
2895        .await
2896        .map_err(|e| format!("Failed to delete invoice_line_items: {}", e))?;
2897
2898        // 6. Documents linked to buildings or expenses
2899        sqlx::query!(
2900            "DELETE FROM documents WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2901            &seed_org_ids
2902        )
2903        .execute(&self.pool)
2904        .await
2905        .map_err(|e| format!("Failed to delete documents: {}", e))?;
2906
2907        // 7. Meetings (now safe to delete after board members)
2908        sqlx::query!(
2909            "DELETE FROM meetings WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2910            &seed_org_ids
2911        )
2912        .execute(&self.pool)
2913        .await
2914        .map_err(|e| format!("Failed to delete meetings: {}", e))?;
2915
2916        // 8. Journal entry lines (reference accounts) - MUST be deleted before accounts
2917        sqlx::query!(
2918            "DELETE FROM journal_entry_lines WHERE organization_id = ANY($1)",
2919            &seed_org_ids
2920        )
2921        .execute(&self.pool)
2922        .await
2923        .map_err(|e| format!("Failed to delete journal_entry_lines: {}", e))?;
2924
2925        // 9. Journal entries (now safe after lines are deleted)
2926        sqlx::query!(
2927            "DELETE FROM journal_entries WHERE organization_id = ANY($1)",
2928            &seed_org_ids
2929        )
2930        .execute(&self.pool)
2931        .await
2932        .map_err(|e| format!("Failed to delete journal_entries: {}", e))?;
2933
2934        // 10. Expenses (now safe to delete after distributions, line items, and journal entries)
2935        sqlx::query!(
2936            "DELETE FROM expenses WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2937            &seed_org_ids
2938        )
2939        .execute(&self.pool)
2940        .await
2941        .map_err(|e| format!("Failed to delete expenses: {}", e))?;
2942
2943        // Unit owners (junction table)
2944        sqlx::query(
2945            "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))"
2946        )
2947        .bind(&seed_org_ids)
2948        .execute(&self.pool)
2949        .await
2950        .map_err(|e| format!("Failed to delete unit_owners: {}", e))?;
2951
2952        // Units
2953        sqlx::query!(
2954            "DELETE FROM units WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2955            &seed_org_ids
2956        )
2957        .execute(&self.pool)
2958        .await
2959        .map_err(|e| format!("Failed to delete units: {}", e))?;
2960
2961        // Owners (only those linked to seed organizations through unit_owners)
2962        sqlx::query!(
2963            "DELETE FROM owners WHERE organization_id = ANY($1)",
2964            &seed_org_ids
2965        )
2966        .execute(&self.pool)
2967        .await
2968        .map_err(|e| format!("Failed to delete owners: {}", e))?;
2969
2970        // Buildings
2971        sqlx::query!(
2972            "DELETE FROM buildings WHERE organization_id = ANY($1)",
2973            &seed_org_ids
2974        )
2975        .execute(&self.pool)
2976        .await
2977        .map_err(|e| format!("Failed to delete buildings: {}", e))?;
2978
2979        // PCMN Accounts
2980        sqlx::query!(
2981            "DELETE FROM accounts WHERE organization_id = ANY($1)",
2982            &seed_org_ids
2983        )
2984        .execute(&self.pool)
2985        .await
2986        .map_err(|e| format!("Failed to delete accounts: {}", e))?;
2987
2988        // User roles (before deleting users)
2989        sqlx::query(
2990            "DELETE FROM user_roles WHERE user_id IN (SELECT id FROM users WHERE organization_id = ANY($1) AND role != 'superadmin')"
2991        )
2992        .bind(&seed_org_ids)
2993        .execute(&self.pool)
2994        .await
2995        .map_err(|e| format!("Failed to delete user_roles: {}", e))?;
2996
2997        // Users (except superadmin)
2998        sqlx::query!(
2999            "DELETE FROM users WHERE organization_id = ANY($1) AND role != 'superadmin'",
3000            &seed_org_ids
3001        )
3002        .execute(&self.pool)
3003        .await
3004        .map_err(|e| format!("Failed to delete users: {}", e))?;
3005
3006        // Finally, delete seed organizations
3007        sqlx::query!("DELETE FROM organizations WHERE is_seed_data = true")
3008            .execute(&self.pool)
3009            .await
3010            .map_err(|e| format!("Failed to delete organizations: {}", e))?;
3011
3012        log::info!("✅ Seed data cleared (production data and superadmin preserved)");
3013
3014        Ok(format!(
3015            "✅ Seed data cleared successfully! ({} organizations removed)",
3016            seed_org_ids.len()
3017        ))
3018    }
3019
3020    /// Seed the "Résidence du Parc Royal" scenario world with all 14 personas.
3021    ///
3022    /// Creates one organization, one building, 12 units, 10 co-owners, 3 professionals,
3023    /// 4 community members, one meeting (2nd convocation) and one pending resolution.
3024    pub async fn seed_scenario_world(&self) -> Result<ScenarioWorldResult, String> {
3025        log::info!("🌱 Starting scenario world seeding (Résidence du Parc Royal)...");
3026
3027        // Check if scenario world already exists
3028        let existing = sqlx::query_scalar!(
3029            "SELECT COUNT(*) as count FROM organizations WHERE slug = 'residence-parc-royal-test'"
3030        )
3031        .fetch_one(&self.pool)
3032        .await
3033        .map_err(|e| format!("Failed to check existing scenario world: {}", e))?;
3034
3035        if existing.unwrap_or(0) > 0 {
3036            return Err(
3037                "Scenario world already exists. Please clear it first with DELETE /seed/scenario/world."
3038                    .to_string(),
3039            );
3040        }
3041
3042        let org_id = Uuid::new_v4();
3043        let now = Utc::now();
3044
3045        // 1. Create organization
3046        sqlx::query(
3047            r#"
3048            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)
3049            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
3050            "#,
3051        )
3052        .bind(org_id)
3053        .bind("Résidence du Parc Royal ASBL")
3054        .bind("residence-parc-royal-test")
3055        .bind("contact@residence-parc.be")
3056        .bind("+32 2 600 00 00")
3057        .bind("professional")
3058        .bind(5)
3059        .bind(50)
3060        .bind(true)
3061        .bind(true) // is_seed_data
3062        .bind(now)
3063        .bind(now)
3064        .execute(&self.pool)
3065        .await
3066        .map_err(|e| format!("Failed to create scenario organization: {}", e))?;
3067
3068        log::info!("✅ Scenario organization created: Résidence du Parc Royal ASBL");
3069
3070        // 2. Create building: 42 Avenue Louise, 182 lots, 10000 tantièmes, 1965
3071        let building_id = Uuid::new_v4();
3072        sqlx::query!(
3073            r#"
3074            INSERT INTO buildings (id, organization_id, name, address, city, postal_code, country, total_units, construction_year, slug, created_at, updated_at)
3075            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
3076            "#,
3077            building_id,
3078            org_id,
3079            "Résidence du Parc Royal",
3080            "Avenue Louise 42",
3081            "Bruxelles",
3082            "1050",
3083            "Belgique",
3084            182,
3085            1965,
3086            "residence-du-parc-royal-bruxelles",
3087            now,
3088            now
3089        )
3090        .execute(&self.pool)
3091        .await
3092        .map_err(|e| format!("Failed to create scenario building: {}", e))?;
3093
3094        log::info!("✅ Scenario building created: Résidence du Parc Royal");
3095
3096        // 3. Create users and owners
3097        let mut users_result: Vec<ScenarioUserResult> = Vec::new();
3098        let mut owners_result: Vec<ScenarioOwnerResult> = Vec::new();
3099        let mut units_result: Vec<ScenarioUnitResult> = Vec::new();
3100
3101        // --- 10 copropriétaires (users + owners + units + unit_owners) ---
3102        struct OwnerPersona {
3103            first_name: &'static str,
3104            last_name: &'static str,
3105            email: &'static str,
3106            password: &'static str,
3107            lots: Vec<(&'static str, f64)>, // (unit_number, tantièmes)
3108        }
3109
3110        let owner_personas = vec![
3111            OwnerPersona {
3112                first_name: "Alice",
3113                last_name: "Dubois",
3114                email: "alice@residence-parc.be",
3115                password: "alice123",
3116                lots: vec![("2A", 450.0)],
3117            },
3118            OwnerPersona {
3119                first_name: "Bob",
3120                last_name: "Janssen",
3121                email: "bob@residence-parc.be",
3122                password: "bob123",
3123                lots: vec![("2B", 430.0)],
3124            },
3125            OwnerPersona {
3126                first_name: "Charlie",
3127                last_name: "Martin",
3128                email: "charlie@residence-parc.be",
3129                password: "charlie123",
3130                lots: vec![("3B", 660.0)],
3131            },
3132            OwnerPersona {
3133                first_name: "Diane",
3134                last_name: "Peeters",
3135                email: "diane@residence-parc.be",
3136                password: "diane123",
3137                lots: vec![("3A", 580.0)],
3138            },
3139            OwnerPersona {
3140                first_name: "Emmanuel",
3141                last_name: "Claes",
3142                email: "emmanuel@residence-parc.be",
3143                password: "emmanuel123",
3144                lots: vec![("5A", 1280.0)],
3145            },
3146            OwnerPersona {
3147                first_name: "Nadia",
3148                last_name: "Benali",
3149                email: "nadia@residence-parc.be",
3150                password: "nadia123",
3151                lots: vec![("4A", 320.0)],
3152            },
3153            OwnerPersona {
3154                first_name: "Marguerite",
3155                last_name: "Lemaire",
3156                email: "marguerite@residence-parc.be",
3157                password: "marguerite123",
3158                lots: vec![("1A", 380.0)],
3159            },
3160            OwnerPersona {
3161                first_name: "Jeanne",
3162                last_name: "Devos",
3163                email: "jeanne@residence-parc.be",
3164                password: "jeanne123",
3165                lots: vec![("1B", 290.0)],
3166            },
3167            OwnerPersona {
3168                first_name: "Philippe",
3169                last_name: "Vandermeulen",
3170                email: "philippe@residence-parc.be",
3171                password: "philippe123",
3172                lots: vec![("6A", 600.0), ("6B", 600.0), ("6C", 600.0)],
3173            },
3174            OwnerPersona {
3175                first_name: "Marcel",
3176                last_name: "Dupont",
3177                email: "marcel@residence-parc.be",
3178                password: "marcel123",
3179                lots: vec![("4B", 450.0)],
3180            },
3181        ];
3182
3183        for persona in &owner_personas {
3184            // Create user
3185            let user_id = self
3186                .create_demo_user(
3187                    persona.email,
3188                    persona.password,
3189                    persona.first_name,
3190                    persona.last_name,
3191                    "owner",
3192                    Some(org_id),
3193                )
3194                .await?;
3195
3196            users_result.push(ScenarioUserResult {
3197                user_id,
3198                email: persona.email.to_string(),
3199                password: persona.password.to_string(),
3200                role: "owner".to_string(),
3201                first_name: persona.first_name.to_string(),
3202                last_name: persona.last_name.to_string(),
3203            });
3204
3205            // Create owner record
3206            let owner_id = self
3207                .create_demo_owner(
3208                    org_id,
3209                    persona.first_name,
3210                    persona.last_name,
3211                    persona.email,
3212                    "+32 400 00 00 00",
3213                    "Avenue Louise 42",
3214                    "Bruxelles",
3215                    "1050",
3216                    "Belgique",
3217                )
3218                .await?;
3219
3220            // Link user to owner
3221            sqlx::query("UPDATE owners SET user_id = $1 WHERE id = $2")
3222                .bind(user_id)
3223                .bind(owner_id)
3224                .execute(&self.pool)
3225                .await
3226                .map_err(|e| {
3227                    format!("Failed to link owner {} to user: {}", persona.last_name, e)
3228                })?;
3229
3230            owners_result.push(ScenarioOwnerResult {
3231                owner_id,
3232                user_id,
3233                first_name: persona.first_name.to_string(),
3234                last_name: persona.last_name.to_string(),
3235                email: persona.email.to_string(),
3236            });
3237
3238            // Create units and unit_owner relationships
3239            for (unit_number, tantiemes) in &persona.lots {
3240                let unit_id = self
3241                    .create_demo_unit(
3242                        org_id,
3243                        building_id,
3244                        None,
3245                        unit_number,
3246                        "apartment",
3247                        None,
3248                        70.0, // default area
3249                        *tantiemes,
3250                    )
3251                    .await?;
3252
3253                self.create_demo_unit_owner(
3254                    unit_id,
3255                    owner_id,
3256                    rust_decimal_macros::dec!(1), // 100% ownership per unit
3257                    true,                         // primary contact
3258                    None,                         // active (no end_date)
3259                )
3260                .await?;
3261
3262                units_result.push(ScenarioUnitResult {
3263                    unit_id,
3264                    unit_number: unit_number.to_string(),
3265                    owner_id,
3266                    tantièmes: *tantiemes,
3267                });
3268            }
3269        }
3270
3271        log::info!(
3272            "✅ {} copropriétaires created with {} units",
3273            owner_personas.len(),
3274            units_result.len()
3275        );
3276
3277        // --- 3 professionals (users only, no units) ---
3278        // François Leroy - Syndic
3279        let francois_user_id = self
3280            .create_demo_user(
3281                "francois@syndic-leroy.be",
3282                "francois123",
3283                "François",
3284                "Leroy",
3285                "syndic",
3286                Some(org_id),
3287            )
3288            .await?;
3289        users_result.push(ScenarioUserResult {
3290            user_id: francois_user_id,
3291            email: "francois@syndic-leroy.be".to_string(),
3292            password: "francois123".to_string(),
3293            role: "syndic".to_string(),
3294            first_name: "François".to_string(),
3295            last_name: "Leroy".to_string(),
3296        });
3297
3298        // Gisèle Vandenberghe - Accountant
3299        let gisele_user_id = self
3300            .create_demo_user(
3301                "gisele@cabinet-vdb.be",
3302                "gisele123",
3303                "Gisèle",
3304                "Vandenberghe",
3305                "accountant",
3306                Some(org_id),
3307            )
3308            .await?;
3309        users_result.push(ScenarioUserResult {
3310            user_id: gisele_user_id,
3311            email: "gisele@cabinet-vdb.be".to_string(),
3312            password: "gisele123".to_string(),
3313            role: "accountant".to_string(),
3314            first_name: "Gisèle".to_string(),
3315            last_name: "Vandenberghe".to_string(),
3316        });
3317
3318        // Marc Dubois - Contractor (Plombier)
3319        let marc_user_id = self
3320            .create_demo_user(
3321                "marc@plomberie-dubois.be",
3322                "marc123",
3323                "Marc",
3324                "Dubois",
3325                "contractor",
3326                Some(org_id),
3327            )
3328            .await?;
3329        users_result.push(ScenarioUserResult {
3330            user_id: marc_user_id,
3331            email: "marc@plomberie-dubois.be".to_string(),
3332            password: "marc123".to_string(),
3333            role: "contractor".to_string(),
3334            first_name: "Marc".to_string(),
3335            last_name: "Dubois".to_string(),
3336        });
3337        // Create contractor profile
3338        sqlx::query(
3339            r#"INSERT INTO contractor_profiles (user_id, organization_id, profession, siren_or_vat, specialties)
3340               VALUES ($1, $2, 'Plombier', 'BE0123456789', ARRAY['plumbing', 'heating'])
3341               ON CONFLICT (user_id) DO NOTHING"#,
3342        )
3343        .bind(marc_user_id)
3344        .bind(org_id)
3345        .execute(&self.pool)
3346        .await
3347        .map_err(|e| format!("Failed to create contractor profile for Marc: {}", e))?;
3348
3349        // Sophie Leroux - Contractor (Électricienne)
3350        let sophie_user_id = self
3351            .create_demo_user(
3352                "sophie@elec-leroux.be",
3353                "sophie123",
3354                "Sophie",
3355                "Leroux",
3356                "contractor",
3357                Some(org_id),
3358            )
3359            .await?;
3360        users_result.push(ScenarioUserResult {
3361            user_id: sophie_user_id,
3362            email: "sophie@elec-leroux.be".to_string(),
3363            password: "sophie123".to_string(),
3364            role: "contractor".to_string(),
3365            first_name: "Sophie".to_string(),
3366            last_name: "Leroux".to_string(),
3367        });
3368        sqlx::query(
3369            r#"INSERT INTO contractor_profiles (user_id, organization_id, profession, siren_or_vat, specialties)
3370               VALUES ($1, $2, 'Électricienne', 'BE0987654321', ARRAY['electrical', 'security'])
3371               ON CONFLICT (user_id) DO NOTHING"#,
3372        )
3373        .bind(sophie_user_id)
3374        .bind(org_id)
3375        .execute(&self.pool)
3376        .await
3377        .map_err(|e| format!("Failed to create contractor profile for Sophie: {}", e))?;
3378
3379        // Admin (already exists globally, just reference it)
3380        users_result.push(ScenarioUserResult {
3381            user_id: Uuid::parse_str("00000000-0000-0000-0000-000000000001")
3382                .map_err(|e| format!("Failed to parse admin UUID: {}", e))?,
3383            email: "admin@koprogo.com".to_string(),
3384            password: "admin123".to_string(),
3385            role: "superadmin".to_string(),
3386            first_name: "Super".to_string(),
3387            last_name: "Admin".to_string(),
3388        });
3389
3390        log::info!("✅ 5 professionals created (syndic, accountant, 2 contractors, admin)");
3391
3392        // --- 4 community members (users only, role=owner, no units) ---
3393        struct CommunityPersona {
3394            first_name: &'static str,
3395            last_name: &'static str,
3396            email: &'static str,
3397            password: &'static str,
3398        }
3399
3400        let community_personas = vec![
3401            CommunityPersona {
3402                first_name: "Ahmed",
3403                last_name: "Mansouri",
3404                email: "ahmed@gmail.com",
3405                password: "ahmed123",
3406            },
3407            CommunityPersona {
3408                first_name: "Sophie",
3409                last_name: "Martin",
3410                email: "sophie@gmail.com",
3411                password: "sophie123",
3412            },
3413            CommunityPersona {
3414                first_name: "Lucas",
3415                last_name: "Martin",
3416                email: "lucas.m@school.be",
3417                password: "lucas123",
3418            },
3419            CommunityPersona {
3420                first_name: "Fatima",
3421                last_name: "El Amrani",
3422                email: "fatima@gmail.com",
3423                password: "fatima123",
3424            },
3425        ];
3426
3427        for persona in &community_personas {
3428            let user_id = self
3429                .create_demo_user(
3430                    persona.email,
3431                    persona.password,
3432                    persona.first_name,
3433                    persona.last_name,
3434                    "owner",
3435                    Some(org_id),
3436                )
3437                .await?;
3438
3439            users_result.push(ScenarioUserResult {
3440                user_id,
3441                email: persona.email.to_string(),
3442                password: persona.password.to_string(),
3443                role: "owner".to_string(),
3444                first_name: persona.first_name.to_string(),
3445                last_name: persona.last_name.to_string(),
3446            });
3447        }
3448
3449        log::info!("✅ 4 community members created");
3450
3451        // =====================================================================
3452        // Building 2: Le Clos des Hirondelles (small, NO CdC, < 20 lots)
3453        // =====================================================================
3454        let building2_id = Uuid::new_v4();
3455        sqlx::query!(
3456            r#"
3457            INSERT INTO buildings (id, organization_id, name, address, city, postal_code, country, total_units, construction_year, slug, created_at, updated_at)
3458            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
3459            "#,
3460            building2_id,
3461            org_id,
3462            "Le Clos des Hirondelles",
3463            "8 Rue de la Station",
3464            "Ixelles",
3465            "1050",
3466            "Belgique",
3467            12,
3468            2005,
3469            "le-clos-des-hirondelles-ixelles",
3470            now,
3471            now
3472        )
3473        .execute(&self.pool)
3474        .await
3475        .map_err(|e| format!("Failed to create building 2 (Le Clos des Hirondelles): {}", e))?;
3476
3477        log::info!("✅ Building 2 created: Le Clos des Hirondelles (12 lots, no CdC)");
3478
3479        let mut building2_owners: Vec<ScenarioOwnerResult> = Vec::new();
3480        let mut building2_units: Vec<ScenarioUnitResult> = Vec::new();
3481
3482        // Building 2 copropriétaires
3483        struct Building2Persona {
3484            first_name: &'static str,
3485            last_name: &'static str,
3486            email: &'static str,
3487            password: &'static str,
3488            unit_number: &'static str,
3489            tantiemes: f64,
3490        }
3491
3492        let building2_personas = vec![
3493            Building2Persona {
3494                first_name: "Yves",
3495                last_name: "Lambert",
3496                email: "yves@clos-hirondelles.be",
3497                password: "yves123",
3498                unit_number: "1A",
3499                tantiemes: 350.0,
3500            },
3501            Building2Persona {
3502                first_name: "Claire",
3503                last_name: "Fontaine",
3504                email: "claire@clos-hirondelles.be",
3505                password: "claire123",
3506                unit_number: "1B",
3507                tantiemes: 300.0,
3508            },
3509            Building2Persona {
3510                first_name: "Robert",
3511                last_name: "Mertens",
3512                email: "robert@clos-hirondelles.be",
3513                password: "robert123",
3514                unit_number: "2A",
3515                tantiemes: 350.0,
3516            },
3517        ];
3518
3519        for persona in &building2_personas {
3520            // Create user
3521            let user_id = self
3522                .create_demo_user(
3523                    persona.email,
3524                    persona.password,
3525                    persona.first_name,
3526                    persona.last_name,
3527                    "owner",
3528                    Some(org_id),
3529                )
3530                .await?;
3531
3532            users_result.push(ScenarioUserResult {
3533                user_id,
3534                email: persona.email.to_string(),
3535                password: persona.password.to_string(),
3536                role: "owner".to_string(),
3537                first_name: persona.first_name.to_string(),
3538                last_name: persona.last_name.to_string(),
3539            });
3540
3541            // Create owner record
3542            let owner_id = self
3543                .create_demo_owner(
3544                    org_id,
3545                    persona.first_name,
3546                    persona.last_name,
3547                    persona.email,
3548                    "+32 400 00 00 00",
3549                    "8 Rue de la Station",
3550                    "Ixelles",
3551                    "1050",
3552                    "Belgique",
3553                )
3554                .await?;
3555
3556            // Link user to owner
3557            sqlx::query("UPDATE owners SET user_id = $1 WHERE id = $2")
3558                .bind(user_id)
3559                .bind(owner_id)
3560                .execute(&self.pool)
3561                .await
3562                .map_err(|e| {
3563                    format!(
3564                        "Failed to link building2 owner {} to user: {}",
3565                        persona.last_name, e
3566                    )
3567                })?;
3568
3569            building2_owners.push(ScenarioOwnerResult {
3570                owner_id,
3571                user_id,
3572                first_name: persona.first_name.to_string(),
3573                last_name: persona.last_name.to_string(),
3574                email: persona.email.to_string(),
3575            });
3576
3577            // Create unit
3578            let unit_id = self
3579                .create_demo_unit(
3580                    org_id,
3581                    building2_id,
3582                    None,
3583                    persona.unit_number,
3584                    "apartment",
3585                    None,
3586                    70.0,
3587                    persona.tantiemes,
3588                )
3589                .await?;
3590
3591            // Create unit_owner relationship
3592            self.create_demo_unit_owner(
3593                unit_id,
3594                owner_id,
3595                rust_decimal_macros::dec!(1), // 100% ownership
3596                true,                         // primary contact
3597                None,                         // active (no end_date)
3598            )
3599            .await?;
3600
3601            building2_units.push(ScenarioUnitResult {
3602                unit_id,
3603                unit_number: persona.unit_number.to_string(),
3604                owner_id,
3605                tantièmes: persona.tantiemes,
3606            });
3607        }
3608
3609        log::info!(
3610            "✅ Building 2: {} copropriétaires created with {} units",
3611            building2_personas.len(),
3612            building2_units.len()
3613        );
3614
3615        // =====================================================================
3616        // Building 3: Les Terrasses de Flagey (medium, CdC obligatoire, >= 20 lots)
3617        // =====================================================================
3618        let building3_id = Uuid::new_v4();
3619        sqlx::query!(
3620            r#"
3621            INSERT INTO buildings (id, organization_id, name, address, city, postal_code, country, total_units, construction_year, slug, created_at, updated_at)
3622            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
3623            "#,
3624            building3_id,
3625            org_id,
3626            "Les Terrasses de Flagey",
3627            "25 Place Flagey",
3628            "Ixelles",
3629            "1050",
3630            "Belgique",
3631            48,
3632            1990,
3633            "les-terrasses-de-flagey-ixelles",
3634            now,
3635            now
3636        )
3637        .execute(&self.pool)
3638        .await
3639        .map_err(|e| format!("Failed to create building 3 (Les Terrasses de Flagey): {}", e))?;
3640
3641        log::info!("✅ Building 3 created: Les Terrasses de Flagey (48 lots, CdC obligatoire)");
3642
3643        let mut building3_owners: Vec<ScenarioOwnerResult> = Vec::new();
3644        let mut building3_units: Vec<ScenarioUnitResult> = Vec::new();
3645
3646        // Building 3 copropriétaires
3647        struct Building3Persona {
3648            first_name: &'static str,
3649            last_name: &'static str,
3650            email: &'static str,
3651            password: &'static str,
3652            unit_number: &'static str,
3653            tantiemes: f64,
3654        }
3655
3656        let building3_personas = vec![
3657            Building3Persona {
3658                first_name: "Isabelle",
3659                last_name: "Renard",
3660                email: "isabelle@terrasses-flagey.be",
3661                password: "isabelle123",
3662                unit_number: "1A",
3663                tantiemes: 450.0,
3664            },
3665            Building3Persona {
3666                first_name: "Thomas",
3667                last_name: "Berger",
3668                email: "thomas@terrasses-flagey.be",
3669                password: "thomas123",
3670                unit_number: "2A",
3671                tantiemes: 380.0,
3672            },
3673            Building3Persona {
3674                first_name: "Aminata",
3675                last_name: "Diallo",
3676                email: "aminata@terrasses-flagey.be",
3677                password: "aminata123",
3678                unit_number: "3A",
3679                tantiemes: 520.0,
3680            },
3681            Building3Persona {
3682                first_name: "Victor",
3683                last_name: "Claessens",
3684                email: "victor@terrasses-flagey.be",
3685                password: "victor123",
3686                unit_number: "4A",
3687                tantiemes: 400.0,
3688            },
3689        ];
3690
3691        for persona in &building3_personas {
3692            // Create user
3693            let user_id = self
3694                .create_demo_user(
3695                    persona.email,
3696                    persona.password,
3697                    persona.first_name,
3698                    persona.last_name,
3699                    "owner",
3700                    Some(org_id),
3701                )
3702                .await?;
3703
3704            users_result.push(ScenarioUserResult {
3705                user_id,
3706                email: persona.email.to_string(),
3707                password: persona.password.to_string(),
3708                role: "owner".to_string(),
3709                first_name: persona.first_name.to_string(),
3710                last_name: persona.last_name.to_string(),
3711            });
3712
3713            // Create owner record
3714            let owner_id = self
3715                .create_demo_owner(
3716                    org_id,
3717                    persona.first_name,
3718                    persona.last_name,
3719                    persona.email,
3720                    "+32 400 00 00 00",
3721                    "25 Place Flagey",
3722                    "Ixelles",
3723                    "1050",
3724                    "Belgique",
3725                )
3726                .await?;
3727
3728            // Link user to owner
3729            sqlx::query("UPDATE owners SET user_id = $1 WHERE id = $2")
3730                .bind(user_id)
3731                .bind(owner_id)
3732                .execute(&self.pool)
3733                .await
3734                .map_err(|e| {
3735                    format!(
3736                        "Failed to link building3 owner {} to user: {}",
3737                        persona.last_name, e
3738                    )
3739                })?;
3740
3741            building3_owners.push(ScenarioOwnerResult {
3742                owner_id,
3743                user_id,
3744                first_name: persona.first_name.to_string(),
3745                last_name: persona.last_name.to_string(),
3746                email: persona.email.to_string(),
3747            });
3748
3749            // Create unit
3750            let unit_id = self
3751                .create_demo_unit(
3752                    org_id,
3753                    building3_id,
3754                    None,
3755                    persona.unit_number,
3756                    "apartment",
3757                    None,
3758                    70.0,
3759                    persona.tantiemes,
3760                )
3761                .await?;
3762
3763            // Create unit_owner relationship
3764            self.create_demo_unit_owner(
3765                unit_id,
3766                owner_id,
3767                rust_decimal_macros::dec!(1), // 100% ownership
3768                true,                         // primary contact
3769                None,                         // active (no end_date)
3770            )
3771            .await?;
3772
3773            building3_units.push(ScenarioUnitResult {
3774                unit_id,
3775                unit_number: persona.unit_number.to_string(),
3776                owner_id,
3777                tantièmes: persona.tantiemes,
3778            });
3779        }
3780
3781        log::info!(
3782            "✅ Building 3: {} copropriétaires created with {} units",
3783            building3_personas.len(),
3784            building3_units.len()
3785        );
3786
3787        // --- Meeting: AG Ordinaire 2026 — 2e convocation ---
3788        let meeting_date = (now + chrono::Duration::days(30))
3789            .format("%Y-%m-%d")
3790            .to_string();
3791
3792        let meeting_id = self
3793            .create_demo_meeting(
3794                building_id,
3795                org_id,
3796                "AG Ordinaire 2026 — 2e convocation",
3797                "ordinary",
3798                &meeting_date,
3799                "scheduled",
3800            )
3801            .await?;
3802
3803        // Set is_second_convocation = true directly in SQL
3804        sqlx::query("UPDATE meetings SET is_second_convocation = true WHERE id = $1")
3805            .bind(meeting_id)
3806            .execute(&self.pool)
3807            .await
3808            .map_err(|e| format!("Failed to set is_second_convocation: {}", e))?;
3809
3810        log::info!("✅ Meeting created: AG Ordinaire 2026 — 2e convocation");
3811
3812        // --- Resolution: Approbation des comptes 2025 (Pending, Absolute majority) ---
3813        let resolution_id = Uuid::new_v4();
3814        sqlx::query(
3815            r#"
3816            INSERT INTO resolutions (id, meeting_id, title, description, resolution_type, majority_required, status, created_at)
3817            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
3818            "#,
3819        )
3820        .bind(resolution_id)
3821        .bind(meeting_id)
3822        .bind("Approbation des comptes 2025")
3823        .bind("Approbation des comptes annuels de l'exercice 2025 et décharge au syndic.")
3824        .bind("Ordinary")
3825        .bind("Absolute")
3826        .bind("Pending")
3827        .bind(now)
3828        .execute(&self.pool)
3829        .await
3830        .map_err(|e| format!("Failed to create scenario resolution: {}", e))?;
3831
3832        log::info!("✅ Resolution created: Approbation des comptes 2025");
3833
3834        let result = ScenarioWorldResult {
3835            organization_id: org_id,
3836            building_id,
3837            meeting_id,
3838            resolution_id,
3839            users: users_result,
3840            owners: owners_result,
3841            units: units_result,
3842            building2_id,
3843            building2_name: "Le Clos des Hirondelles".to_string(),
3844            building2_owners,
3845            building2_units,
3846            building3_id,
3847            building3_name: "Les Terrasses de Flagey".to_string(),
3848            building3_owners,
3849            building3_units,
3850        };
3851
3852        log::info!("✅ Scenario world seeded successfully (Résidence du Parc Royal)");
3853
3854        Ok(result)
3855    }
3856
3857    /// Clear all data created by `seed_scenario_world`.
3858    ///
3859    /// Deletes in reverse FK order, scoped by the scenario organization slug.
3860    pub async fn clear_scenario_world(&self) -> Result<String, String> {
3861        log::warn!("⚠️  Clearing scenario world data (Résidence du Parc Royal)...");
3862
3863        // Find the scenario organization
3864        let org_id: Option<Uuid> = sqlx::query_scalar(
3865            "SELECT id FROM organizations WHERE slug = 'residence-parc-royal-test'",
3866        )
3867        .fetch_optional(&self.pool)
3868        .await
3869        .map_err(|e| format!("Failed to find scenario organization: {}", e))?;
3870
3871        let org_id =
3872            match org_id {
3873                Some(id) => id,
3874                None => return Ok(
3875                    "ℹ️  No scenario world found to clear (residence-parc-royal-test not found)."
3876                        .to_string(),
3877                ),
3878            };
3879
3880        log::info!("Found scenario organization: {} — clearing data...", org_id);
3881
3882        // Delete in reverse FK order
3883
3884        // 1. Votes (reference resolutions and owners)
3885        sqlx::query(
3886            "DELETE FROM votes WHERE resolution_id IN (SELECT r.id FROM resolutions r INNER JOIN meetings m ON r.meeting_id = m.id WHERE m.organization_id = $1)",
3887        )
3888        .bind(org_id)
3889        .execute(&self.pool)
3890        .await
3891        .map_err(|e| format!("Failed to delete votes: {}", e))?;
3892
3893        // 2. Resolutions (reference meetings)
3894        sqlx::query(
3895            "DELETE FROM resolutions WHERE meeting_id IN (SELECT id FROM meetings WHERE organization_id = $1)",
3896        )
3897        .bind(org_id)
3898        .execute(&self.pool)
3899        .await
3900        .map_err(|e| format!("Failed to delete resolutions: {}", e))?;
3901
3902        // 3. Convocation recipients (reference convocations)
3903        sqlx::query(
3904            "DELETE FROM convocation_recipients WHERE convocation_id IN (SELECT id FROM convocations WHERE organization_id = $1)",
3905        )
3906        .bind(org_id)
3907        .execute(&self.pool)
3908        .await
3909        .map_err(|e| format!("Failed to delete convocation_recipients: {}", e))?;
3910
3911        // 4. Convocations (reference meetings)
3912        sqlx::query("DELETE FROM convocations WHERE organization_id = $1")
3913            .bind(org_id)
3914            .execute(&self.pool)
3915            .await
3916            .map_err(|e| format!("Failed to delete convocations: {}", e))?;
3917
3918        // 5. Board decisions (reference meetings)
3919        sqlx::query(
3920            "DELETE FROM board_decisions WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = $1)",
3921        )
3922        .bind(org_id)
3923        .execute(&self.pool)
3924        .await
3925        .map_err(|e| format!("Failed to delete board_decisions: {}", e))?;
3926
3927        // 6. Board members (reference meetings)
3928        sqlx::query(
3929            "DELETE FROM board_members WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = $1)",
3930        )
3931        .bind(org_id)
3932        .execute(&self.pool)
3933        .await
3934        .map_err(|e| format!("Failed to delete board_members: {}", e))?;
3935
3936        // 7. Meetings
3937        sqlx::query(
3938            "DELETE FROM meetings WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = $1)",
3939        )
3940        .bind(org_id)
3941        .execute(&self.pool)
3942        .await
3943        .map_err(|e| format!("Failed to delete meetings: {}", e))?;
3944
3945        // 8. Unit owners (junction table)
3946        sqlx::query(
3947            "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 = $1)",
3948        )
3949        .bind(org_id)
3950        .execute(&self.pool)
3951        .await
3952        .map_err(|e| format!("Failed to delete unit_owners: {}", e))?;
3953
3954        // 9. Units
3955        sqlx::query(
3956            "DELETE FROM units WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = $1)",
3957        )
3958        .bind(org_id)
3959        .execute(&self.pool)
3960        .await
3961        .map_err(|e| format!("Failed to delete units: {}", e))?;
3962
3963        // 10. Owners
3964        sqlx::query("DELETE FROM owners WHERE organization_id = $1")
3965            .bind(org_id)
3966            .execute(&self.pool)
3967            .await
3968            .map_err(|e| format!("Failed to delete owners: {}", e))?;
3969
3970        // 11. Documents
3971        sqlx::query(
3972            "DELETE FROM documents WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = $1)",
3973        )
3974        .bind(org_id)
3975        .execute(&self.pool)
3976        .await
3977        .map_err(|e| format!("Failed to delete documents: {}", e))?;
3978
3979        // 12. Buildings
3980        sqlx::query("DELETE FROM buildings WHERE organization_id = $1")
3981            .bind(org_id)
3982            .execute(&self.pool)
3983            .await
3984            .map_err(|e| format!("Failed to delete buildings: {}", e))?;
3985
3986        // 13. User roles (before users, except superadmin)
3987        sqlx::query(
3988            "DELETE FROM user_roles WHERE user_id IN (SELECT id FROM users WHERE organization_id = $1 AND role != 'superadmin')",
3989        )
3990        .bind(org_id)
3991        .execute(&self.pool)
3992        .await
3993        .map_err(|e| format!("Failed to delete user_roles: {}", e))?;
3994
3995        // 14. Users (except admin@koprogo.com)
3996        sqlx::query(
3997            "DELETE FROM users WHERE organization_id = $1 AND email != 'admin@koprogo.com'",
3998        )
3999        .bind(org_id)
4000        .execute(&self.pool)
4001        .await
4002        .map_err(|e| format!("Failed to delete users: {}", e))?;
4003
4004        // 15. PCMN accounts
4005        sqlx::query("DELETE FROM accounts WHERE organization_id = $1")
4006            .bind(org_id)
4007            .execute(&self.pool)
4008            .await
4009            .map_err(|e| format!("Failed to delete accounts: {}", e))?;
4010
4011        // 16. Organization
4012        sqlx::query("DELETE FROM organizations WHERE id = $1")
4013            .bind(org_id)
4014            .execute(&self.pool)
4015            .await
4016            .map_err(|e| format!("Failed to delete organization: {}", e))?;
4017
4018        log::info!("✅ Scenario world cleared (Résidence du Parc Royal)");
4019
4020        Ok("✅ Scenario world cleared successfully (Résidence du Parc Royal).".to_string())
4021    }
4022}
4023
4024#[cfg(test)]
4025mod tests {
4026    use super::*;
4027    use sqlx::PgPool;
4028
4029    /// Test that seed_superadmin() is idempotent - can be called multiple times without errors
4030    ///
4031    /// This test ensures production deployments can safely restart without constraint violations
4032    #[sqlx::test]
4033    #[ignore = "requires DATABASE_URL (integration test)"]
4034    async fn test_seed_superadmin_is_idempotent(pool: PgPool) -> sqlx::Result<()> {
4035        let seeder = DatabaseSeeder::new(pool.clone());
4036
4037        // First call: Create superadmin
4038        let result1 = seeder.seed_superadmin().await;
4039        assert!(result1.is_ok(), "First seed_superadmin call should succeed");
4040        let user1 = result1.unwrap();
4041        assert_eq!(user1.email, "admin@koprogo.com");
4042        assert_eq!(user1.role, UserRole::SuperAdmin);
4043
4044        // Second call: Should succeed (idempotent upsert)
4045        let result2 = seeder.seed_superadmin().await;
4046        assert!(
4047            result2.is_ok(),
4048            "Second seed_superadmin call should succeed (idempotent): {:?}",
4049            result2.err()
4050        );
4051        let user2 = result2.unwrap();
4052        assert_eq!(user2.email, "admin@koprogo.com");
4053        assert_eq!(user2.id, user1.id, "Superadmin UUID should remain the same");
4054
4055        // Third call: Should still succeed
4056        let result3 = seeder.seed_superadmin().await;
4057        assert!(
4058            result3.is_ok(),
4059            "Third seed_superadmin call should succeed (idempotent): {:?}",
4060            result3.err()
4061        );
4062
4063        // Verify only ONE primary role exists for superadmin
4064        let primary_role_count = sqlx::query_scalar::<_, i64>(
4065            r#"
4066            SELECT COUNT(*)
4067            FROM user_roles
4068            WHERE user_id = $1 AND is_primary = true
4069            "#,
4070        )
4071        .bind(user1.id)
4072        .fetch_one(&pool)
4073        .await?;
4074
4075        assert_eq!(
4076            primary_role_count, 1,
4077            "Superadmin should have exactly ONE primary role, found {}",
4078            primary_role_count
4079        );
4080
4081        // Verify the superadmin role exists in user_roles
4082        let role_count = sqlx::query_scalar::<_, i64>(
4083            r#"
4084            SELECT COUNT(*)
4085            FROM user_roles
4086            WHERE user_id = $1 AND role = 'superadmin' AND organization_id IS NULL
4087            "#,
4088        )
4089        .bind(user1.id)
4090        .fetch_one(&pool)
4091        .await?;
4092
4093        assert_eq!(
4094            role_count, 1,
4095            "Superadmin should have exactly ONE superadmin role, found {}",
4096            role_count
4097        );
4098
4099        Ok(())
4100    }
4101
4102    /// Test that seed_superadmin() handles existing user_roles correctly
4103    ///
4104    /// Ensures the UPSERT doesn't violate the idx_user_roles_primary_per_user constraint
4105    #[sqlx::test]
4106    #[ignore = "requires DATABASE_URL (integration test)"]
4107    async fn test_seed_superadmin_preserves_existing_primary_role(
4108        pool: PgPool,
4109    ) -> sqlx::Result<()> {
4110        let seeder = DatabaseSeeder::new(pool.clone());
4111
4112        // First call: Create superadmin
4113        seeder.seed_superadmin().await.unwrap();
4114
4115        // Manually check is_primary state before second call
4116        let is_primary_before = sqlx::query_scalar::<_, bool>(
4117            r#"
4118            SELECT is_primary
4119            FROM user_roles
4120            WHERE user_id = '00000000-0000-0000-0000-000000000001'
4121              AND role = 'superadmin'
4122              AND organization_id IS NULL
4123            "#,
4124        )
4125        .fetch_one(&pool)
4126        .await?;
4127
4128        assert!(
4129            is_primary_before,
4130            "Superadmin role should be primary after first seed"
4131        );
4132
4133        // Second call: Should not violate unique constraint
4134        let result = seeder.seed_superadmin().await;
4135        assert!(
4136            result.is_ok(),
4137            "Second seed should not violate idx_user_roles_primary_per_user constraint: {:?}",
4138            result.err()
4139        );
4140
4141        // Verify is_primary is still true (preserved, not updated)
4142        let is_primary_after = sqlx::query_scalar::<_, bool>(
4143            r#"
4144            SELECT is_primary
4145            FROM user_roles
4146            WHERE user_id = '00000000-0000-0000-0000-000000000001'
4147              AND role = 'superadmin'
4148              AND organization_id IS NULL
4149            "#,
4150        )
4151        .fetch_one(&pool)
4152        .await?;
4153
4154        assert!(
4155            is_primary_after,
4156            "Superadmin role should remain primary after second seed"
4157        );
4158
4159        Ok(())
4160    }
4161}