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