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#[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: 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 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 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 if let Ok((mot, Ok(empreinte))) = issue {
136 self.cache_empreintes().insert(mot, empreinte);
137 }
138 }
139 }
140
141 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 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 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 pub async fn seed_superadmin(&self) -> Result<User, String> {
243 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 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 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 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 pub async fn seed_belgian_pcmn_for_all_organizations(&self) -> Result<String, String> {
345 log::info!("🌱 Seeding Belgian PCMN for all organizations...");
346
347 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 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 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 async fn seed_belgian_pcmn_for_org(&self, organization_id: Uuid) -> Result<i64, String> {
393 let base_accounts = vec![
395 (
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 (
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 (
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 ("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 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 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 pub async fn seed_demo_data(&self) -> Result<String, String> {
545 log::info!("🌱 Starting demo data seeding...");
546
547 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 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) .bind(true) .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 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 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 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 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 let unit1_id = self
728 .create_demo_unit(
729 org1_id,
730 building1_id,
731 None, "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, "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, "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, "201",
772 "apartment",
773 Some(2),
774 95.0,
775 350.0,
776 )
777 .await?;
778
779 log::info!("✅ Demo units created");
780
781 self.create_demo_unit_owner(
784 unit1_id,
785 owner1_db_id,
786 rust_decimal_macros::dec!(1), true, None, )
790 .await?;
791
792 self.create_demo_unit_owner(
794 unit2_id,
795 owner2_db_id,
796 rust_decimal_macros::dec!(0.6), true, 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), false, None,
808 )
809 .await?;
810
811 self.create_demo_unit_owner(
813 unit3_id,
814 owner1_db_id,
815 rust_decimal_macros::dec!(0.5), true, 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), 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), false,
835 None,
836 )
837 .await?;
838
839 self.create_demo_unit_owner(
841 unit4_id,
842 owner3_db_id,
843 rust_decimal_macros::dec!(1), true, None,
846 )
847 .await?;
848
849 log::info!("✅ Demo unit_owners relationships created");
850
851 self.seed_pcmn_accounts(org1_id).await?;
853 log::info!("✅ Belgian PCMN accounts seeded");
854
855 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, 21.0, "2025-01-15",
865 "2025-02-15", "administration",
867 "paid",
868 Some("Syndic Services SPRL"),
869 Some("SYN-2025-001"),
870 Some("6100"), )
872 .await?;
873
874 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, 21.0, "2025-02-10",
883 "2025-03-10",
884 "maintenance",
885 "paid",
886 Some("Ascenseurs Plus SA"),
887 Some("ASC-2025-023"),
888 Some("6110"), )
890 .await?;
891
892 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, 21.0, "2025-01-15",
901 "2025-02-15", "administration",
903 "overdue",
904 Some("Syndic Services SPRL"),
905 Some("SYN-2025-002"),
906 Some("6100"), )
908 .await?;
909
910 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, 6.0, "2025-01-01",
919 "2025-01-31",
920 "cleaning",
921 "paid",
922 Some("CleanPro Belgium SPRL"),
923 Some("CLN-2025-156"),
924 Some("6130"), )
926 .await?;
927
928 let expense5_id = self
930 .create_demo_expense_with_vat(
931 building1_id,
932 org1_id,
933 "Assurance incendie immeuble 2025",
934 1850.00, 0.0, "2025-01-05",
937 "2025-02-05",
938 "insurance",
939 "pending",
940 Some("AXA Belgium"),
941 Some("AXA-2025-8472"),
942 Some("6150"), )
944 .await?;
945
946 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, 21.0, "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"), )
962 .await?;
963
964 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 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, 21.0, &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"), )
991 .await?;
992
993 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, 21.0, &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"), )
1009 .await?;
1010
1011 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, 6.0, &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"), )
1027 .await?;
1028
1029 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, 6.0, &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"), )
1045 .await?;
1046
1047 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, 21.0, &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"), )
1063 .await?;
1064
1065 log::info!("✅ Demo expenses with VAT created (including current month)");
1066
1067 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 self.create_demo_payment_reminder(
1085 expense3_id,
1086 owner2_db_id, org1_id,
1088 "FirstReminder",
1089 20, )
1091 .await?;
1092
1093 self.create_demo_payment_reminder(
1094 expense3_id,
1095 owner3_db_id, org1_id,
1097 "SecondReminder",
1098 35, )
1100 .await?;
1101
1102 log::info!("✅ Payment reminders created");
1103
1104 log::info!("Creating owner contributions...");
1106
1107 use chrono::Datelike;
1109 let quarter = ((now.month() - 1) / 3) + 1;
1110 let year = now.year();
1111
1112 self.create_demo_owner_contribution(
1115 org1_id,
1116 owner1_db_id, 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"), )
1126 .await?;
1127
1128 self.create_demo_owner_contribution(
1129 org1_id,
1130 owner2_db_id, 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, 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, Some("7000"),
1153 )
1154 .await?;
1155
1156 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, 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"), )
1178 .await?;
1179
1180 self.create_demo_owner_contribution(
1181 org1_id,
1182 owner2_db_id, Some(unit2_id),
1184 "Appel de fonds extraordinaire - Réfection toiture",
1185 1400.0,
1186 "extraordinary",
1187 &prev_month,
1188 "pending",
1189 None, Some("7100"),
1191 )
1192 .await?;
1193
1194 log::info!("✅ Owner contributions created");
1195
1196 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 let mandate_start = meeting1_date.clone();
1231 let mandate_end = (now + chrono::Duration::days(275))
1232 .format("%Y-%m-%d")
1233 .to_string(); 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 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 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"), "pending",
1271 )
1272 .await?;
1273
1274 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"), "in_progress",
1283 )
1284 .await?;
1285
1286 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"), "pending",
1295 )
1296 .await?;
1297
1298 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 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"), "pending",
1319 )
1320 .await?;
1321
1322 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"), "in_progress",
1331 )
1332 .await?;
1333
1334 log::info!("✅ Demo board decisions created");
1335
1336 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 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) .bind(true) .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 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) .bind(true) .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 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 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 _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, end_date,
1704 is_primary_contact,
1705 now, now )
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 let approval_status = if payment_status == "paid" {
1738 "approved" } else {
1740 "draft" };
1742
1743 let paid_date = if payment_status == "paid" {
1745 Some(expense_date_parsed)
1746 } else {
1747 None
1748 };
1749
1750 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 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 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 pub async fn seed_realistic_data(&self) -> Result<String, String> {
2009 log::info!("🌱 Starting realistic data seeding...");
2010
2011 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 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 let org_configs = [
2053 ("Petite Copropriété SPRL", "small", 5, 30), ("Copropriété Moyenne SA", "medium", 8, 60), ("Grande Résidence NV", "large", 10, 100), ];
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 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 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 let num_owners = (target_units * 2 / 3) as usize; let mut owner_ids = Vec::new();
2121
2122 for o in 0..num_owners {
2123 let owner_id = Uuid::new_v4();
2124
2125 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 let units_per_building = target_units / num_buildings;
2164 let mut org_units = 0;
2165 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 let units_this_building = if b == num_buildings - 1 {
2196 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; let unit_number = format!("{}.{}", floor, (u % 4) + 1);
2205
2206 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 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 "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 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 if existing_expenses.0 == 0 {
2249 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 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 let payment_status = if rng.random_bool(0.7) {
2280 "paid"
2281 } else {
2282 "pending"
2283 };
2284
2285 let approval_status = if payment_status == "paid" {
2287 "approved" } else {
2289 "draft" };
2291
2292 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 } }
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 async fn seed_pcmn_accounts(&self, organization_id: Uuid) -> Result<(), String> {
2352 let accounts = vec![
2355 ("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 ("7000", "Appels de fonds ordinaires", "REVENUE"),
2365 ("7100", "Appels de fonds extraordinaires", "REVENUE"),
2366 ("7200", "Autres produits", "REVENUE"),
2367 ("4000", "Copropriétaires débiteurs", "ASSET"),
2369 ("4110", "TVA à récupérer", "ASSET"),
2370 ("4400", "Fournisseurs", "LIABILITY"),
2371 ("4500", "TVA à payer", "LIABILITY"),
2372 ("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>) .bind(account_type_str)
2392 .bind(true) .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 #[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 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 let paid_date = if payment_status == "paid" {
2440 Some(expense_date_parsed) } else {
2442 None
2443 };
2444
2445 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 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 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 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 #[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 let vat_amount = total_amount - amount_excl_vat;
2577
2578 let mut tx = self
2580 .pool
2581 .begin()
2582 .await
2583 .map_err(|e| format!("Failed to begin transaction: {}", e))?;
2584
2585 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 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 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", 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 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", 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 tx.commit()
2671 .await
2672 .map_err(|e| format!("Failed to commit journal entry transaction: {}", e))?;
2673
2674 Ok(())
2675 }
2676
2677 async fn create_demo_distributions(
2679 &self,
2680 expense_id: Uuid,
2681 _organization_id: Uuid,
2682 ) -> Result<(), String> {
2683 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 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(()); }
2711
2712 let total_quota: Decimal = units.iter().map(|u| u.quota).sum();
2716
2717 let now = Utc::now();
2718
2719 for unit in units {
2722 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 let owner_id = match primary_owner {
2739 Some(owner) => owner.owner_id,
2740 None => continue, };
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 #[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 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 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 let mut tx = self
2887 .pool
2888 .begin()
2889 .await
2890 .map_err(|e| format!("Failed to begin transaction: {}", e))?;
2891
2892 sqlx::query("SET CONSTRAINTS ALL DEFERRED")
2894 .execute(&mut *tx)
2895 .await
2896 .map_err(|e| format!("Failed to defer constraints: {}", e))?;
2897
2898 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 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") .bind(format!("Créance - {}", description))
2934 .bind(amount) .bind(0.0) .bind(now)
2937 .execute(&mut *tx)
2938 .await
2939 .map_err(|e| format!("Failed to create debit line (4000): {}", e))?;
2940
2941 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) .bind(format!("Produit - {}", description))
2956 .bind(0.0) .bind(amount) .bind(now)
2959 .execute(&mut *tx)
2960 .await
2961 .map_err(|e| format!("Failed to create credit line ({}): {}", account_code, e))?;
2962
2963 tx.commit()
2965 .await
2966 .map_err(|e| format!("Failed to commit transaction: {}", e))?;
2967
2968 Ok(())
2969 }
2970
2971 #[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 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 let penalty_amount: Decimal = if days_overdue > 0 {
3000 let yearly_penalty = amount_owed * Decimal::new(8, 2); 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); 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) .bind("Sent") .bind(amount_owed)
3028 .bind(penalty_amount)
3029 .bind(total_amount)
3030 .bind(due_date)
3031 .bind(days_overdue as i32)
3032 .bind("Email") .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 pub async fn clear_demo_data(&self) -> Result<String, String> {
3045 log::warn!("⚠️ Clearing seed data only (preserving production data)...");
3046
3047 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn seed_scenario_world(&self) -> Result<ScenarioWorldResult, String> {
3242 log::info!("🌱 Starting scenario world seeding (Résidence du Parc Royal)...");
3243
3244 self.precalculer_empreintes(&[
3252 "syndic123",
3253 "sophie123",
3254 "francois123",
3255 "gisele123",
3256 "marc123",
3257 "comptable123",
3258 "owner123",
3259 ])
3260 .await;
3261
3262 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 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) .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 let building_id = Uuid::new_v4();
3307 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 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 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)>, }
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 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 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 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 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 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, *tantiemes,
3492 )
3493 .await?;
3494
3495 self.create_demo_unit_owner(
3496 unit_id,
3497 owner_id,
3498 rust_decimal_macros::dec!(1), true, None, )
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 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 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 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 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 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 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 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 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 let building2_id = Uuid::new_v4();
3702 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 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 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 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 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 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 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 self.create_demo_unit_owner(
3846 unit_id,
3847 owner_id,
3848 rust_decimal_macros::dec!(1), true, None, )
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 let building3_id = Uuid::new_v4();
3872 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 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 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 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 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 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 self.create_demo_unit_owner(
4019 unit_id,
4020 owner_id,
4021 rust_decimal_macros::dec!(1), true, None, )
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 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 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 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 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 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 pub async fn clear_scenario_world(&self) -> Result<String, String> {
4187 log::warn!("⚠️ Clearing scenario world data (Résidence du Parc Royal)...");
4188
4189 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 #[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 seeder.seed_superadmin().await.unwrap();
4440
4441 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 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 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}