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