Skip to main content

koprogo_api/infrastructure/database/
seed.rs

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