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