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::Rng;
8use sqlx::{PgPool, Row};
9use uuid::Uuid;
10
11pub struct DatabaseSeeder {
12 pool: PgPool,
13}
14
15impl DatabaseSeeder {
16 pub fn new(pool: PgPool) -> Self {
17 Self { pool }
18 }
19
20 pub async fn seed_superadmin(&self) -> Result<User, String> {
22 let superadmin_email = "admin@koprogo.com";
23 let superadmin_password = "admin123"; let password_hash = hash(superadmin_password, DEFAULT_COST)
27 .map_err(|e| format!("Failed to hash password: {}", e))?;
28
29 let superadmin_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001")
30 .map_err(|e| format!("Failed to parse UUID: {}", e))?;
31
32 let now = Utc::now();
33
34 sqlx::query!(
36 r#"
37 INSERT INTO users (id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at)
38 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
39 ON CONFLICT (email)
40 DO UPDATE SET
41 password_hash = EXCLUDED.password_hash,
42 updated_at = EXCLUDED.updated_at,
43 is_active = true
44 "#,
45 superadmin_id,
46 superadmin_email,
47 password_hash,
48 "Super",
49 "Admin",
50 "superadmin",
51 None::<Uuid>,
52 true,
53 now,
54 now
55 )
56 .execute(&self.pool)
57 .await
58 .map_err(|e| format!("Failed to upsert superadmin: {}", e))?;
59
60 sqlx::query(
64 r#"
65 INSERT INTO user_roles (id, user_id, role, organization_id, is_primary, created_at, updated_at)
66 VALUES (
67 gen_random_uuid(),
68 $1,
69 'superadmin',
70 NULL,
71 NOT EXISTS (SELECT 1 FROM user_roles WHERE user_id = $1 AND is_primary = true),
72 NOW(),
73 NOW()
74 )
75 ON CONFLICT DO NOTHING
76 "#,
77 )
78 .bind(superadmin_id)
79 .execute(&self.pool)
80 .await
81 .map_err(|e| format!("Failed to upsert superadmin role: {}", e))?;
82
83 log::info!("✅ Superadmin ready: {}", superadmin_email);
84
85 Ok(User {
86 id: superadmin_id,
87 email: superadmin_email.to_string(),
88 password_hash,
89 first_name: "Super".to_string(),
90 last_name: "Admin".to_string(),
91 role: UserRole::SuperAdmin,
92 organization_id: None,
93 is_active: true,
94 processing_restricted: false,
95 processing_restricted_at: None,
96 marketing_opt_out: false,
97 marketing_opt_out_at: None,
98 created_at: now,
99 updated_at: now,
100 })
101 }
102
103 pub async fn seed_belgian_pcmn_for_all_organizations(&self) -> Result<String, String> {
106 log::info!("🌱 Seeding Belgian PCMN for all organizations...");
107
108 let organizations = sqlx::query!("SELECT id FROM organizations")
110 .fetch_all(&self.pool)
111 .await
112 .map_err(|e| format!("Failed to fetch organizations: {}", e))?;
113
114 let mut total_created = 0;
115 let mut orgs_seeded = 0;
116
117 for org in organizations {
118 let org_id = org.id;
119
120 let existing_count = sqlx::query!(
122 "SELECT COUNT(*) as count FROM accounts WHERE organization_id = $1",
123 org_id
124 )
125 .fetch_one(&self.pool)
126 .await
127 .map_err(|e| format!("Failed to count accounts: {}", e))?;
128
129 if existing_count.count.unwrap_or(0) > 0 {
130 log::debug!(
131 "Organization {} already has {} accounts, skipping",
132 org_id,
133 existing_count.count.unwrap_or(0)
134 );
135 continue;
136 }
137
138 let created = self.seed_belgian_pcmn_for_org(org_id).await?;
140 total_created += created;
141 orgs_seeded += 1;
142 }
143
144 let message = format!(
145 "✅ Seeded {} accounts across {} organizations",
146 total_created, orgs_seeded
147 );
148 log::info!("{}", message);
149 Ok(message)
150 }
151
152 async fn seed_belgian_pcmn_for_org(&self, organization_id: Uuid) -> Result<i64, String> {
154 let base_accounts = vec![
156 (
158 "6100",
159 "Charges courantes",
160 None,
161 AccountType::Expense,
162 true,
163 ),
164 (
165 "6110",
166 "Entretien et réparations",
167 None,
168 AccountType::Expense,
169 true,
170 ),
171 ("6120", "Personnel", None, AccountType::Expense, true),
172 (
173 "6130",
174 "Services extérieurs",
175 None,
176 AccountType::Expense,
177 true,
178 ),
179 (
180 "6140",
181 "Honoraires et commissions",
182 None,
183 AccountType::Expense,
184 true,
185 ),
186 ("6150", "Assurances", None, AccountType::Expense, true),
187 (
188 "6200",
189 "Travaux extraordinaires",
190 None,
191 AccountType::Expense,
192 true,
193 ),
194 (
196 "7000",
197 "Produits de gestion",
198 None,
199 AccountType::Revenue,
200 true,
201 ),
202 (
203 "7100",
204 "Appels de fonds",
205 Some("7000"),
206 AccountType::Revenue,
207 true,
208 ),
209 (
210 "7200",
211 "Autres produits",
212 Some("7000"),
213 AccountType::Revenue,
214 true,
215 ),
216 (
218 "4000",
219 "Comptes de tiers",
220 None,
221 AccountType::Liability,
222 false,
223 ),
224 (
225 "4100",
226 "TVA à récupérer",
227 Some("4000"),
228 AccountType::Asset,
229 true,
230 ),
231 (
232 "4110",
233 "TVA récupérable",
234 Some("4100"),
235 AccountType::Asset,
236 true,
237 ),
238 (
239 "4400",
240 "Fournisseurs",
241 Some("4000"),
242 AccountType::Liability,
243 true,
244 ),
245 (
246 "4500",
247 "Copropriétaires",
248 Some("4000"),
249 AccountType::Asset,
250 true,
251 ),
252 ("5500", "Banque", None, AccountType::Asset, true),
254 ("5700", "Caisse", None, AccountType::Asset, true),
255 ];
256
257 let mut created_count = 0;
258
259 for (code, label, parent_code, account_type, direct_use) in base_accounts {
260 let account = Account::new(
262 code.to_string(),
263 label.to_string(),
264 parent_code.map(|s| s.to_string()),
265 account_type,
266 direct_use,
267 organization_id,
268 )?;
269
270 let result = sqlx::query!(
272 r#"
273 INSERT INTO accounts (id, code, label, parent_code, account_type, direct_use, organization_id, created_at, updated_at)
274 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
275 ON CONFLICT (code, organization_id) DO NOTHING
276 "#,
277 account.id,
278 account.code,
279 account.label,
280 account.parent_code,
281 account.account_type as AccountType,
282 account.direct_use,
283 account.organization_id,
284 account.created_at,
285 account.updated_at
286 )
287 .execute(&self.pool)
288 .await
289 .map_err(|e| format!("Failed to insert account {}: {}", code, e))?;
290
291 if result.rows_affected() > 0 {
292 created_count += 1;
293 }
294 }
295
296 log::info!(
297 "Created {} PCMN accounts for organization {}",
298 created_count,
299 organization_id
300 );
301 Ok(created_count)
302 }
303
304 pub async fn seed_demo_data(&self) -> Result<String, String> {
306 log::info!("🌱 Starting demo data seeding...");
307
308 let existing_seed_orgs =
310 sqlx::query!("SELECT COUNT(*) as count FROM organizations WHERE is_seed_data = true")
311 .fetch_one(&self.pool)
312 .await
313 .map_err(|e| format!("Failed to count seed organizations: {}", e))?;
314
315 if existing_seed_orgs.count.unwrap_or(0) > 0 {
316 return Err(
317 "Seed data already exists. Please use 'Clear Seed Data' first.".to_string(),
318 );
319 }
320
321 let org1_id = Uuid::new_v4();
323 let now = Utc::now();
324
325 sqlx::query(
326 r#"
327 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)
328 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
329 "#
330 )
331 .bind(org1_id)
332 .bind("Résidence Grand Place SPRL")
333 .bind("residence-grand-place")
334 .bind("contact@grandplace.be")
335 .bind("+32 2 501 23 45")
336 .bind("professional")
337 .bind(20)
338 .bind(50)
339 .bind(true) .bind(true) .bind(now)
342 .bind(now)
343 .execute(&self.pool)
344 .await
345 .map_err(|e| format!("Failed to create demo organization 1: {}", e))?;
346
347 log::info!("✅ Organization 1 created: Résidence Grand Place SPRL");
348
349 let syndic1_id = self
351 .create_demo_user(
352 "syndic@grandplace.be",
353 "syndic123",
354 "Jean",
355 "Dupont",
356 "syndic",
357 Some(org1_id),
358 )
359 .await?;
360
361 let _accountant_id = self
362 .create_demo_user(
363 "comptable@grandplace.be",
364 "comptable123",
365 "Marie",
366 "Martin",
367 "accountant",
368 Some(org1_id),
369 )
370 .await?;
371
372 let owner1_user_id = self
373 .create_demo_user(
374 "proprietaire1@grandplace.be",
375 "owner123",
376 "Pierre",
377 "Durand",
378 "owner",
379 Some(org1_id),
380 )
381 .await?;
382
383 let owner2_user_id = self
384 .create_demo_user(
385 "proprietaire2@grandplace.be",
386 "owner123",
387 "Sophie",
388 "Bernard",
389 "owner",
390 Some(org1_id),
391 )
392 .await?;
393
394 log::info!("✅ Demo users created");
395
396 let building1_id = self
398 .create_demo_building(
399 org1_id,
400 "Résidence Grand Place",
401 "Grand Place 15",
402 "Bruxelles",
403 "1000",
404 "Belgique",
405 15,
406 1995,
407 )
408 .await?;
409
410 let building2_id = self
411 .create_demo_building(
412 org1_id,
413 "Les Jardins d'Ixelles",
414 "Rue du Trône 85",
415 "Bruxelles",
416 "1050",
417 "Belgique",
418 8,
419 2010,
420 )
421 .await?;
422
423 log::info!("✅ Demo buildings created");
424
425 let owner1_db_id = self
427 .create_demo_owner(
428 org1_id,
429 "Pierre",
430 "Durand",
431 "pierre.durand@email.be",
432 "+32 476 12 34 56",
433 "Avenue Louise 15",
434 "Bruxelles",
435 "1050",
436 "Belgique",
437 )
438 .await?;
439
440 let owner2_db_id = self
441 .create_demo_owner(
442 org1_id,
443 "Sophie",
444 "Bernard",
445 "sophie.bernard@email.be",
446 "+32 495 98 76 54",
447 "Rue Royale 28",
448 "Bruxelles",
449 "1000",
450 "Belgique",
451 )
452 .await?;
453
454 let owner3_db_id = self
455 .create_demo_owner(
456 org1_id,
457 "Michel",
458 "Lefebvre",
459 "michel.lefebvre@email.be",
460 "+32 477 11 22 33",
461 "Boulevard d'Avroy 42",
462 "Liège",
463 "4000",
464 "Belgique",
465 )
466 .await?;
467
468 log::info!("✅ Demo owners created");
469
470 sqlx::query("UPDATE owners SET user_id = $1 WHERE id = $2")
472 .bind(owner1_user_id)
473 .bind(owner1_db_id)
474 .execute(&self.pool)
475 .await
476 .map_err(|e| format!("Failed to link owner1 to user: {}", e))?;
477
478 sqlx::query("UPDATE owners SET user_id = $1 WHERE id = $2")
479 .bind(owner2_user_id)
480 .bind(owner2_db_id)
481 .execute(&self.pool)
482 .await
483 .map_err(|e| format!("Failed to link owner2 to user: {}", e))?;
484
485 log::info!("✅ Users linked to owners");
486
487 let unit1_id = self
489 .create_demo_unit(
490 org1_id,
491 building1_id,
492 None, "101",
494 "apartment",
495 Some(1),
496 75.5,
497 250.0,
498 )
499 .await?;
500
501 let unit2_id = self
502 .create_demo_unit(
503 org1_id,
504 building1_id,
505 None, "102",
507 "apartment",
508 Some(1),
509 62.0,
510 200.0,
511 )
512 .await?;
513
514 let unit3_id = self
515 .create_demo_unit(
516 org1_id,
517 building1_id,
518 None, "103",
520 "apartment",
521 Some(1),
522 85.0,
523 300.0,
524 )
525 .await?;
526
527 let unit4_id = self
528 .create_demo_unit(
529 org1_id,
530 building2_id,
531 None, "201",
533 "apartment",
534 Some(2),
535 95.0,
536 350.0,
537 )
538 .await?;
539
540 log::info!("✅ Demo units created");
541
542 self.create_demo_unit_owner(
545 unit1_id,
546 owner1_db_id,
547 1.0, true, None, )
551 .await?;
552
553 self.create_demo_unit_owner(
555 unit2_id,
556 owner2_db_id,
557 0.6, true, None,
560 )
561 .await?;
562
563 self.create_demo_unit_owner(
564 unit2_id,
565 owner3_db_id,
566 0.4, false, None,
569 )
570 .await?;
571
572 self.create_demo_unit_owner(
574 unit3_id,
575 owner1_db_id,
576 0.5, true, None,
579 )
580 .await?;
581
582 self.create_demo_unit_owner(
583 unit3_id,
584 owner2_db_id,
585 0.3, false,
587 None,
588 )
589 .await?;
590
591 self.create_demo_unit_owner(
592 unit3_id,
593 owner3_db_id,
594 0.2, false,
596 None,
597 )
598 .await?;
599
600 self.create_demo_unit_owner(
602 unit4_id,
603 owner3_db_id,
604 1.0, true, None,
607 )
608 .await?;
609
610 log::info!("✅ Demo unit_owners relationships created");
611
612 self.seed_pcmn_accounts(org1_id).await?;
614 log::info!("✅ Belgian PCMN accounts seeded");
615
616 let expense1_id = self
619 .create_demo_expense_with_vat(
620 building1_id,
621 org1_id,
622 "Charges copropriété T1 2025",
623 4132.23, 21.0, "2025-01-15",
626 "2025-02-15", "administration",
628 "paid",
629 Some("Syndic Services SPRL"),
630 Some("SYN-2025-001"),
631 Some("6100"), )
633 .await?;
634
635 let expense2_id = self
637 .create_demo_expense_with_vat(
638 building1_id,
639 org1_id,
640 "Réparation ascenseur - Remplacement moteur",
641 2066.12, 21.0, "2025-02-10",
644 "2025-03-10",
645 "maintenance",
646 "paid",
647 Some("Ascenseurs Plus SA"),
648 Some("ASC-2025-023"),
649 Some("6110"), )
651 .await?;
652
653 let expense3_id = self
655 .create_demo_expense_with_vat(
656 building2_id,
657 org1_id,
658 "Charges copropriété T1 2025",
659 2479.34, 21.0, "2025-01-15",
662 "2025-02-15", "administration",
664 "overdue",
665 Some("Syndic Services SPRL"),
666 Some("SYN-2025-002"),
667 Some("6100"), )
669 .await?;
670
671 let expense4_id = self
673 .create_demo_expense_with_vat(
674 building2_id,
675 org1_id,
676 "Nettoyage parties communes - Forfait annuel",
677 1132.08, 6.0, "2025-01-01",
680 "2025-01-31",
681 "cleaning",
682 "paid",
683 Some("CleanPro Belgium SPRL"),
684 Some("CLN-2025-156"),
685 Some("6130"), )
687 .await?;
688
689 let expense5_id = self
691 .create_demo_expense_with_vat(
692 building1_id,
693 org1_id,
694 "Assurance incendie immeuble 2025",
695 1850.00, 0.0, "2025-01-05",
698 "2025-02-05",
699 "insurance",
700 "pending",
701 Some("AXA Belgium"),
702 Some("AXA-2025-8472"),
703 Some("6150"), )
705 .await?;
706
707 let expense6_id = self
709 .create_demo_expense_with_vat(
710 building1_id,
711 org1_id,
712 "Rénovation façade - Devis Entreprise Martin",
713 12396.69, 21.0, "2025-03-01",
716 "2025-04-30",
717 "works",
718 "pending",
719 Some("Entreprise Martin & Fils SPRL"),
720 Some("MART-2025-042"),
721 Some("6200"), )
723 .await?;
724
725 let now = Utc::now();
728 let current_month = now.format("%B %Y").to_string();
729 let month_start = format!("{}", now.format("%Y-%m-01"));
730 let day_3 = format!("{}", now.format("%Y-%m-03"));
731 let day_5 = format!("{}", now.format("%Y-%m-05"));
732 let day_8 = format!("{}", now.format("%Y-%m-08"));
733 let day_10 = format!("{}", now.format("%Y-%m-10"));
734 let month_end = format!("{}", (now + chrono::Duration::days(30)).format("%Y-%m-%d"));
735
736 let expense7_id = self
738 .create_demo_expense_with_vat(
739 building1_id,
740 org1_id,
741 &format!("Maintenance ascenseur {}", current_month),
742 826.45, 21.0, &day_5,
745 &month_end,
746 "maintenance",
747 "paid",
748 Some("Ascenseurs Plus SA"),
749 Some(&format!("ASC-{}-001", now.format("%Y-%m"))),
750 Some("6110"), )
752 .await?;
753
754 let expense8_id = self
756 .create_demo_expense_with_vat(
757 building1_id,
758 org1_id,
759 &format!("Électricité communs {}", current_month),
760 387.60, 21.0, &day_3,
763 &format!("{}", (now + chrono::Duration::days(25)).format("%Y-%m-%d")),
764 "utilities",
765 "paid",
766 Some("Engie Electrabel"),
767 Some(&format!("ENGIE-{}-3847", now.format("%Y-%m"))),
768 Some("6100"), )
770 .await?;
771
772 let expense9_id = self
774 .create_demo_expense_with_vat(
775 building1_id,
776 org1_id,
777 &format!("Nettoyage communs {}", current_month),
778 471.70, 6.0, &month_start,
781 &format!("{}", (now + chrono::Duration::days(20)).format("%Y-%m-%d")),
782 "cleaning",
783 "paid",
784 Some("NetClean Services SPRL"),
785 Some(&format!("CLEAN-{}-074", now.format("%Y-%m"))),
786 Some("6130"), )
788 .await?;
789
790 let expense10_id = self
792 .create_demo_expense_with_vat(
793 building1_id,
794 org1_id,
795 &format!("Eau communs {}", current_month),
796 156.60, 6.0, &day_8,
799 &format!("{}", (now + chrono::Duration::days(30)).format("%Y-%m-%d")),
800 "utilities",
801 "pending",
802 Some("Vivaqua"),
803 Some(&format!("VIVA-{}-9284", now.format("%Y-%m"))),
804 Some("6100"), )
806 .await?;
807
808 let expense11_id = self
810 .create_demo_expense_with_vat(
811 building1_id,
812 org1_id,
813 &format!("Chauffage gaz {}", current_month),
814 1240.00, 21.0, &day_10,
817 &format!("{}", (now + chrono::Duration::days(30)).format("%Y-%m-%d")),
818 "utilities",
819 "paid",
820 Some("Sibelga"),
821 Some(&format!("SIBEL-{}-7453", now.format("%Y-%m"))),
822 Some("6100"), )
824 .await?;
825
826 log::info!("✅ Demo expenses with VAT created (including current month)");
827
828 self.create_demo_distributions(expense1_id, org1_id).await?;
830 self.create_demo_distributions(expense2_id, org1_id).await?;
831 self.create_demo_distributions(expense3_id, org1_id).await?;
832 self.create_demo_distributions(expense4_id, org1_id).await?;
833 self.create_demo_distributions(expense5_id, org1_id).await?;
834 self.create_demo_distributions(expense6_id, org1_id).await?;
835 self.create_demo_distributions(expense7_id, org1_id).await?;
836 self.create_demo_distributions(expense8_id, org1_id).await?;
837 self.create_demo_distributions(expense9_id, org1_id).await?;
838 self.create_demo_distributions(expense10_id, org1_id)
839 .await?;
840 self.create_demo_distributions(expense11_id, org1_id)
841 .await?;
842 log::info!("✅ Charge distributions calculated");
843
844 self.create_demo_payment_reminder(
846 expense3_id,
847 owner2_db_id, org1_id,
849 "FirstReminder",
850 20, )
852 .await?;
853
854 self.create_demo_payment_reminder(
855 expense3_id,
856 owner3_db_id, org1_id,
858 "SecondReminder",
859 35, )
861 .await?;
862
863 log::info!("✅ Payment reminders created");
864
865 log::info!("Creating owner contributions...");
867
868 use chrono::Datelike;
870 let quarter = ((now.month() - 1) / 3) + 1;
871 let year = now.year();
872
873 self.create_demo_owner_contribution(
876 org1_id,
877 owner1_db_id, Some(unit1_id),
879 &format!("Appel de fonds T{} {} - Charges courantes", quarter, year),
880 650.0,
881 "regular",
882 &month_start,
883 "paid",
884 Some(&day_5),
885 Some("7000"), )
887 .await?;
888
889 self.create_demo_owner_contribution(
890 org1_id,
891 owner2_db_id, Some(unit2_id),
893 &format!("Appel de fonds T{} {} - Charges courantes", quarter, year),
894 750.0,
895 "regular",
896 &month_start,
897 "paid",
898 Some(&day_8),
899 Some("7000"),
900 )
901 .await?;
902
903 self.create_demo_owner_contribution(
904 org1_id,
905 owner3_db_id, Some(unit3_id),
907 &format!("Appel de fonds T{} {} - Charges courantes", quarter, year),
908 600.0,
909 "regular",
910 &month_start,
911 "pending",
912 None, Some("7000"),
914 )
915 .await?;
916
917 let prev_month = (now - chrono::Duration::days(20))
921 .format("%Y-%m-05")
922 .to_string();
923 let prev_month_payment = (now - chrono::Duration::days(15))
924 .format("%Y-%m-10")
925 .to_string();
926
927 self.create_demo_owner_contribution(
928 org1_id,
929 owner1_db_id, Some(unit1_id),
931 "Appel de fonds extraordinaire - Réfection toiture",
932 1200.0,
933 "extraordinary",
934 &prev_month,
935 "paid",
936 Some(&prev_month_payment),
937 Some("7100"), )
939 .await?;
940
941 self.create_demo_owner_contribution(
942 org1_id,
943 owner2_db_id, Some(unit2_id),
945 "Appel de fonds extraordinaire - Réfection toiture",
946 1400.0,
947 "extraordinary",
948 &prev_month,
949 "pending",
950 None, Some("7100"),
952 )
953 .await?;
954
955 log::info!("✅ Owner contributions created");
956
957 let meeting1_date = (now - chrono::Duration::days(90))
959 .format("%Y-%m-%d")
960 .to_string();
961 let meeting2_date = (now - chrono::Duration::days(60))
962 .format("%Y-%m-%d")
963 .to_string();
964
965 let meeting1_id = self
966 .create_demo_meeting(
967 building1_id,
968 org1_id,
969 &format!("Assemblée Générale Ordinaire {}", year),
970 "ordinary",
971 &meeting1_date,
972 "completed",
973 )
974 .await?;
975
976 let meeting2_id = self
977 .create_demo_meeting(
978 building2_id,
979 org1_id,
980 "Assemblée Générale Extraordinaire - Travaux",
981 "extraordinary",
982 &meeting2_date,
983 "completed",
984 )
985 .await?;
986
987 log::info!("✅ Demo meetings created");
988
989 let mandate_start = meeting1_date.clone();
992 let mandate_end = (now + chrono::Duration::days(275))
993 .format("%Y-%m-%d")
994 .to_string(); self.create_demo_board_member(
998 owner1_db_id,
999 building1_id,
1000 org1_id,
1001 meeting1_id,
1002 "president",
1003 &mandate_start,
1004 &mandate_end,
1005 )
1006 .await?;
1007
1008 self.create_demo_board_member(
1010 owner2_db_id,
1011 building2_id,
1012 org1_id,
1013 meeting2_id,
1014 "treasurer",
1015 &meeting2_date,
1016 &format!("{}", (now + chrono::Duration::days(305)).format("%Y-%m-%d")),
1017 )
1018 .await?;
1019
1020 log::info!("✅ Demo board members elected");
1021
1022 self.create_demo_board_decision(
1025 building1_id,
1026 org1_id,
1027 meeting1_id,
1028 "Rénovation de la façade",
1029 "Approuver les devis pour la rénovation de la façade principale",
1030 Some("2025-11-26"), "pending",
1032 )
1033 .await?;
1034
1035 self.create_demo_board_decision(
1037 building1_id,
1038 org1_id,
1039 meeting1_id,
1040 "Contrat d'assurance",
1041 "Signer le nouveau contrat d'assurance avec AXA",
1042 Some("2025-11-05"), "in_progress",
1044 )
1045 .await?;
1046
1047 self.create_demo_board_decision(
1049 building1_id,
1050 org1_id,
1051 meeting1_id,
1052 "Nettoyage des gouttières",
1053 "Engager une entreprise pour le nettoyage annuel des gouttières",
1054 Some("2025-10-15"), "pending",
1056 )
1057 .await?;
1058
1059 self.create_demo_board_decision(
1061 building1_id,
1062 org1_id,
1063 meeting1_id,
1064 "Installation caméras",
1065 "Installation du système de vidéosurveillance dans le hall",
1066 Some("2025-10-01"),
1067 "completed",
1068 )
1069 .await?;
1070
1071 self.create_demo_board_decision(
1073 building2_id,
1074 org1_id,
1075 meeting2_id,
1076 "Remplacement chaudière",
1077 "Valider le choix du fournisseur pour la nouvelle chaudière",
1078 Some("2025-11-11"), "pending",
1080 )
1081 .await?;
1082
1083 self.create_demo_board_decision(
1085 building2_id,
1086 org1_id,
1087 meeting2_id,
1088 "Aménagement parking vélos",
1089 "Organiser l'aménagement du parking à vélos au rez-de-chaussée",
1090 Some("2025-11-21"), "in_progress",
1092 )
1093 .await?;
1094
1095 log::info!("✅ Demo board decisions created");
1096
1097 self.create_demo_document(
1099 building1_id,
1100 org1_id,
1101 "Procès-Verbal AG 2024",
1102 "meeting_minutes",
1103 "/uploads/demo/pv-ag-2024.pdf",
1104 syndic1_id,
1105 )
1106 .await?;
1107
1108 self.create_demo_document(
1109 building1_id,
1110 org1_id,
1111 "Règlement de copropriété",
1112 "regulation",
1113 "/uploads/demo/reglement.pdf",
1114 syndic1_id,
1115 )
1116 .await?;
1117
1118 log::info!("✅ Demo documents created");
1119
1120 let org2_id = Uuid::new_v4();
1122 sqlx::query(
1123 r#"
1124 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)
1125 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
1126 "#
1127 )
1128 .bind(org2_id)
1129 .bind("Copropriété Bruxelles SPRL")
1130 .bind("copro-bruxelles")
1131 .bind("info@copro-bruxelles.be")
1132 .bind("+32 2 123 45 67")
1133 .bind("starter")
1134 .bind(5)
1135 .bind(10)
1136 .bind(true) .bind(true) .bind(now)
1139 .bind(now)
1140 .execute(&self.pool)
1141 .await
1142 .map_err(|e| format!("Failed to create demo organization 2: {}", e))?;
1143
1144 let _syndic2_id = self
1145 .create_demo_user(
1146 "syndic@copro-bruxelles.be",
1147 "syndic123",
1148 "Marc",
1149 "Dubois",
1150 "syndic",
1151 Some(org2_id),
1152 )
1153 .await?;
1154
1155 let building3_id = self
1156 .create_demo_building(
1157 org2_id,
1158 "Résidence Européenne",
1159 "Avenue Louise 123",
1160 "Bruxelles",
1161 "1050",
1162 "Belgique",
1163 12,
1164 2005,
1165 )
1166 .await?;
1167
1168 self.create_demo_meeting(
1169 building3_id,
1170 org2_id,
1171 "AG Annuelle 2025",
1172 "ordinary",
1173 "2025-05-10",
1174 "scheduled",
1175 )
1176 .await?;
1177
1178 log::info!("✅ Organization 2 created");
1179
1180 let org3_id = Uuid::new_v4();
1182 sqlx::query(
1183 r#"
1184 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)
1185 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
1186 "#
1187 )
1188 .bind(org3_id)
1189 .bind("Syndic Liège SA")
1190 .bind("syndic-liege")
1191 .bind("contact@syndic-liege.be")
1192 .bind("+32 4 222 33 44")
1193 .bind("enterprise")
1194 .bind(50)
1195 .bind(100)
1196 .bind(true) .bind(true) .bind(now)
1199 .bind(now)
1200 .execute(&self.pool)
1201 .await
1202 .map_err(|e| format!("Failed to create demo organization 3: {}", e))?;
1203
1204 let _syndic3_id = self
1205 .create_demo_user(
1206 "syndic@syndic-liege.be",
1207 "syndic123",
1208 "Sophie",
1209 "Lambert",
1210 "syndic",
1211 Some(org3_id),
1212 )
1213 .await?;
1214
1215 let _building4_id = self
1216 .create_demo_building(
1217 org3_id,
1218 "Les Terrasses de Liège",
1219 "Boulevard de la Sauvenière 45",
1220 "Liège",
1221 "4000",
1222 "Belgique",
1223 8,
1224 2018,
1225 )
1226 .await?;
1227
1228 log::info!("✅ Organization 3 created");
1229
1230 Ok("✅ Demo data seeded successfully!\n\n\
1231 📊 Summary:\n\
1232 - 3 Organizations: Grand Place (Bruxelles), Bruxelles Louise, Liège\n\
1233 - 6+ Users: 3 Syndics, 1 Accountant, 2+ Owners\n\
1234 - 4 Buildings across Belgium\n\
1235 - 3 Owners (database records)\n\
1236 - 4 Units\n\
1237 - 4 Expenses\n\
1238 - 3 Meetings\n\
1239 - 2 Documents\n\n\
1240 🇧🇪 Belgian Demo - Credentials:\n\
1241 - Org 1 (Grand Place): syndic@grandplace.be / syndic123\n\
1242 - Org 2 (Bruxelles): syndic@copro-bruxelles.be / syndic123\n\
1243 - Org 3 (Liège): syndic@syndic-liege.be / syndic123\n\
1244 - SuperAdmin: admin@koprogo.com / admin123"
1245 .to_string())
1246 }
1247
1248 async fn create_demo_user(
1249 &self,
1250 email: &str,
1251 password: &str,
1252 first_name: &str,
1253 last_name: &str,
1254 role: &str,
1255 organization_id: Option<Uuid>,
1256 ) -> Result<Uuid, String> {
1257 let password_hash =
1258 hash(password, DEFAULT_COST).map_err(|e| format!("Failed to hash password: {}", e))?;
1259
1260 let user_id = Uuid::new_v4();
1261 let now = Utc::now();
1262
1263 sqlx::query!(
1264 r#"
1265 INSERT INTO users (id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at)
1266 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
1267 "#,
1268 user_id,
1269 email,
1270 password_hash,
1271 first_name,
1272 last_name,
1273 role,
1274 organization_id,
1275 true,
1276 now,
1277 now
1278 )
1279 .execute(&self.pool)
1280 .await
1281 .map_err(|e| format!("Failed to create user {}: {}", email, e))?;
1282
1283 sqlx::query(
1284 r#"
1285 INSERT INTO user_roles (id, user_id, role, organization_id, is_primary, created_at, updated_at)
1286 VALUES (gen_random_uuid(), $1, $2, $3, true, $4, $4)
1287 ON CONFLICT (user_id, role, organization_id)
1288 DO UPDATE SET is_primary = true, updated_at = EXCLUDED.updated_at
1289 "#,
1290 )
1291 .bind(user_id)
1292 .bind(role)
1293 .bind(organization_id)
1294 .bind(now)
1295 .execute(&self.pool)
1296 .await
1297 .map_err(|e| format!("Failed to assign role {} to user {}: {}", role, email, e))?;
1298
1299 Ok(user_id)
1300 }
1301
1302 #[allow(clippy::too_many_arguments)]
1303 async fn create_demo_building(
1304 &self,
1305 org_id: Uuid,
1306 name: &str,
1307 address: &str,
1308 city: &str,
1309 postal_code: &str,
1310 country: &str,
1311 total_units: i32,
1312 construction_year: i32,
1313 ) -> Result<Uuid, String> {
1314 let building_id = Uuid::new_v4();
1315 let now = Utc::now();
1316
1317 sqlx::query!(
1318 r#"
1319 INSERT INTO buildings (id, organization_id, name, address, city, postal_code, country, total_units, construction_year, created_at, updated_at)
1320 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
1321 "#,
1322 building_id,
1323 org_id,
1324 name,
1325 address,
1326 city,
1327 postal_code,
1328 country,
1329 total_units,
1330 construction_year,
1331 now,
1332 now
1333 )
1334 .execute(&self.pool)
1335 .await
1336 .map_err(|e| format!("Failed to create building {}: {}", name, e))?;
1337
1338 Ok(building_id)
1339 }
1340
1341 #[allow(clippy::too_many_arguments)]
1342 async fn create_demo_owner(
1343 &self,
1344 organization_id: Uuid,
1345 first_name: &str,
1346 last_name: &str,
1347 email: &str,
1348 phone: &str,
1349 address: &str,
1350 city: &str,
1351 postal_code: &str,
1352 country: &str,
1353 ) -> Result<Uuid, String> {
1354 let owner_id = Uuid::new_v4();
1355 let now = Utc::now();
1356
1357 sqlx::query!(
1358 r#"
1359 INSERT INTO owners (id, organization_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at)
1360 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
1361 "#,
1362 owner_id,
1363 organization_id,
1364 first_name,
1365 last_name,
1366 email,
1367 phone,
1368 address,
1369 city,
1370 postal_code,
1371 country,
1372 now,
1373 now
1374 )
1375 .execute(&self.pool)
1376 .await
1377 .map_err(|e| format!("Failed to create owner {} {}: {}", first_name, last_name, e))?;
1378
1379 Ok(owner_id)
1380 }
1381
1382 #[allow(clippy::too_many_arguments)]
1383 async fn create_demo_unit(
1384 &self,
1385 organization_id: Uuid,
1386 building_id: Uuid,
1387 owner_id: Option<Uuid>,
1388 unit_number: &str,
1389 unit_type: &str,
1390 floor: Option<i32>,
1391 surface_area: f64,
1392 quota: f64,
1393 ) -> Result<Uuid, String> {
1394 let unit_id = Uuid::new_v4();
1395 let now = Utc::now();
1396
1397 sqlx::query(
1398 r#"
1399 INSERT INTO units (id, organization_id, building_id, owner_id, unit_number, unit_type, floor, surface_area, quota, created_at, updated_at)
1400 VALUES ($1, $2, $3, $4, $5, $6::unit_type, $7, $8, $9, $10, $11)
1401 "#
1402 )
1403 .bind(unit_id)
1404 .bind(organization_id)
1405 .bind(building_id)
1406 .bind(owner_id)
1407 .bind(unit_number)
1408 .bind(unit_type)
1409 .bind(floor)
1410 .bind(surface_area)
1411 .bind(quota)
1412 .bind(now)
1413 .bind(now)
1414 .execute(&self.pool)
1415 .await
1416 .map_err(|e| format!("Failed to create unit {}: {}", unit_number, e))?;
1417
1418 Ok(unit_id)
1419 }
1420
1421 async fn create_demo_unit_owner(
1422 &self,
1423 unit_id: Uuid,
1424 owner_id: Uuid,
1425 ownership_percentage: f64,
1426 is_primary_contact: bool,
1427 end_date: Option<chrono::DateTime<Utc>>,
1428 ) -> Result<Uuid, String> {
1429 let unit_owner_id = Uuid::new_v4();
1430 let now = Utc::now();
1431
1432 sqlx::query!(
1433 r#"
1434 INSERT INTO unit_owners (id, unit_id, owner_id, ownership_percentage, start_date, end_date, is_primary_contact, created_at, updated_at)
1435 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
1436 "#,
1437 unit_owner_id,
1438 unit_id,
1439 owner_id,
1440 ownership_percentage,
1441 now, end_date,
1443 is_primary_contact,
1444 now, now )
1447 .execute(&self.pool)
1448 .await
1449 .map_err(|e| format!("Failed to create unit_owner relationship: {}", e))?;
1450
1451 Ok(unit_owner_id)
1452 }
1453
1454 #[allow(dead_code)]
1455 #[allow(clippy::too_many_arguments)]
1456 async fn create_demo_expense(
1457 &self,
1458 building_id: Uuid,
1459 organization_id: Uuid,
1460 description: &str,
1461 amount: f64,
1462 expense_date: &str,
1463 category: &str,
1464 payment_status: &str,
1465 supplier: Option<&str>,
1466 invoice_number: Option<&str>,
1467 ) -> Result<Uuid, String> {
1468 let expense_id = Uuid::new_v4();
1469 let now = Utc::now();
1470 let expense_date_parsed =
1471 chrono::DateTime::parse_from_rfc3339(&format!("{}T00:00:00Z", expense_date))
1472 .map_err(|e| format!("Failed to parse date: {}", e))?
1473 .with_timezone(&Utc);
1474
1475 let approval_status = if payment_status == "paid" {
1477 "approved" } else {
1479 "draft" };
1481
1482 let paid_date = if payment_status == "paid" {
1484 Some(expense_date_parsed)
1485 } else {
1486 None
1487 };
1488
1489 let existing: Option<(Uuid,)> =
1491 sqlx::query_as("SELECT id FROM expenses WHERE description = $1 AND building_id = $2")
1492 .bind(description)
1493 .bind(building_id)
1494 .fetch_optional(&self.pool)
1495 .await
1496 .map_err(|e| format!("Failed to check existing expense: {}", e))?;
1497
1498 let final_expense_id = if let Some((existing_id,)) = existing {
1499 sqlx::query(
1501 r#"
1502 UPDATE expenses SET
1503 category = $1::expense_category,
1504 amount = $2,
1505 expense_date = $3,
1506 payment_status = $4::payment_status,
1507 approval_status = $5::approval_status,
1508 paid_date = $6,
1509 supplier = $7,
1510 invoice_number = $8,
1511 updated_at = $9
1512 WHERE id = $10
1513 "#,
1514 )
1515 .bind(category)
1516 .bind(amount)
1517 .bind(expense_date_parsed)
1518 .bind(payment_status)
1519 .bind(approval_status)
1520 .bind(paid_date)
1521 .bind(supplier)
1522 .bind(invoice_number)
1523 .bind(now)
1524 .bind(existing_id)
1525 .execute(&self.pool)
1526 .await
1527 .map_err(|e| format!("Failed to update expense: {}", e))?;
1528 existing_id
1529 } else {
1530 sqlx::query(
1532 r#"
1533 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)
1534 VALUES ($1, $2, $3, $4::expense_category, $5, $6, $7, $8::payment_status, $9::approval_status, $10, $11, $12, $13, $14)
1535 "#
1536 )
1537 .bind(expense_id)
1538 .bind(organization_id)
1539 .bind(building_id)
1540 .bind(category)
1541 .bind(description)
1542 .bind(amount)
1543 .bind(expense_date_parsed)
1544 .bind(payment_status)
1545 .bind(approval_status)
1546 .bind(paid_date)
1547 .bind(supplier)
1548 .bind(invoice_number)
1549 .bind(now)
1550 .bind(now)
1551 .execute(&self.pool)
1552 .await
1553 .map_err(|e| format!("Failed to create expense: {}", e))?;
1554 expense_id
1555 };
1556
1557 Ok(final_expense_id)
1558 }
1559
1560 #[allow(clippy::too_many_arguments)]
1561 async fn create_demo_meeting(
1562 &self,
1563 building_id: Uuid,
1564 org_id: Uuid,
1565 title: &str,
1566 meeting_type: &str,
1567 scheduled_date: &str,
1568 status: &str,
1569 ) -> Result<Uuid, String> {
1570 let meeting_id = Uuid::new_v4();
1571 let now = Utc::now();
1572 let scheduled_date_parsed =
1573 chrono::DateTime::parse_from_rfc3339(&format!("{}T10:00:00Z", scheduled_date))
1574 .map_err(|e| format!("Failed to parse date: {}", e))?
1575 .with_timezone(&Utc);
1576
1577 let agenda_json = serde_json::json!([
1578 "Approbation des comptes",
1579 "Travaux à prévoir",
1580 "Questions diverses"
1581 ]);
1582
1583 sqlx::query(
1584 r#"
1585 INSERT INTO meetings (id, building_id, organization_id, meeting_type, title, description, scheduled_date, location, status, agenda, created_at, updated_at)
1586 VALUES ($1, $2, $3, $4::meeting_type, $5, $6, $7, $8, $9::meeting_status, $10, $11, $12)
1587 "#
1588 )
1589 .bind(meeting_id)
1590 .bind(building_id)
1591 .bind(org_id)
1592 .bind(meeting_type)
1593 .bind(title)
1594 .bind(Some("Assemblée générale annuelle"))
1595 .bind(scheduled_date_parsed)
1596 .bind("Salle polyvalente")
1597 .bind(status)
1598 .bind(agenda_json)
1599 .bind(now)
1600 .bind(now)
1601 .execute(&self.pool)
1602 .await
1603 .map_err(|e| format!("Failed to create meeting: {}", e))?;
1604
1605 Ok(meeting_id)
1606 }
1607
1608 #[allow(clippy::too_many_arguments)]
1609 async fn create_demo_document(
1610 &self,
1611 building_id: Uuid,
1612 org_id: Uuid,
1613 title: &str,
1614 document_type: &str,
1615 file_path: &str,
1616 uploaded_by: Uuid,
1617 ) -> Result<Uuid, String> {
1618 let document_id = Uuid::new_v4();
1619 let now = Utc::now();
1620
1621 sqlx::query(
1622 r#"
1623 INSERT INTO documents (id, building_id, organization_id, document_type, title, description, file_path, file_size, mime_type, uploaded_by, created_at, updated_at)
1624 VALUES ($1, $2, $3, $4::document_type, $5, $6, $7, $8, $9, $10, $11, $12)
1625 "#
1626 )
1627 .bind(document_id)
1628 .bind(building_id)
1629 .bind(org_id)
1630 .bind(document_type)
1631 .bind(title)
1632 .bind(Some("Document de démonstration"))
1633 .bind(file_path)
1634 .bind(1024_i64)
1635 .bind("application/pdf")
1636 .bind(uploaded_by)
1637 .bind(now)
1638 .bind(now)
1639 .execute(&self.pool)
1640 .await
1641 .map_err(|e| format!("Failed to create document: {}", e))?;
1642
1643 Ok(document_id)
1644 }
1645
1646 #[allow(clippy::too_many_arguments)]
1647 async fn create_demo_board_member(
1648 &self,
1649 owner_id: Uuid,
1650 building_id: Uuid,
1651 org_id: Uuid,
1652 meeting_id: Uuid,
1653 position: &str,
1654 mandate_start: &str,
1655 mandate_end: &str,
1656 ) -> Result<Uuid, String> {
1657 let board_member_id = Uuid::new_v4();
1658 let now = Utc::now();
1659
1660 let mandate_start_parsed = NaiveDate::parse_from_str(mandate_start, "%Y-%m-%d")
1661 .map_err(|e| format!("Failed to parse mandate_start date: {}", e))?
1662 .and_hms_opt(0, 0, 0)
1663 .ok_or("Failed to create datetime")?;
1664
1665 let mandate_end_parsed = NaiveDate::parse_from_str(mandate_end, "%Y-%m-%d")
1666 .map_err(|e| format!("Failed to parse mandate_end date: {}", e))?
1667 .and_hms_opt(0, 0, 0)
1668 .ok_or("Failed to create datetime")?;
1669
1670 sqlx::query(
1671 r#"
1672 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)
1673 VALUES ($1, $2, $3, $4, $5::board_position, $6, $7, $8, $9, $10, $11)
1674 "#
1675 )
1676 .bind(board_member_id)
1677 .bind(owner_id)
1678 .bind(building_id)
1679 .bind(org_id)
1680 .bind(position)
1681 .bind(mandate_start_parsed)
1682 .bind(mandate_end_parsed)
1683 .bind(meeting_id)
1684 .bind(true)
1685 .bind(now)
1686 .bind(now)
1687 .execute(&self.pool)
1688 .await
1689 .map_err(|e| format!("Failed to create board member: {}", e))?;
1690
1691 Ok(board_member_id)
1692 }
1693
1694 #[allow(clippy::too_many_arguments)]
1695 async fn create_demo_board_decision(
1696 &self,
1697 building_id: Uuid,
1698 org_id: Uuid,
1699 meeting_id: Uuid,
1700 subject: &str,
1701 decision_text: &str,
1702 deadline: Option<&str>,
1703 status: &str,
1704 ) -> Result<Uuid, String> {
1705 let decision_id = Uuid::new_v4();
1706 let now = Utc::now();
1707
1708 let deadline_parsed = if let Some(deadline_str) = deadline {
1709 Some(
1710 NaiveDate::parse_from_str(deadline_str, "%Y-%m-%d")
1711 .map_err(|e| format!("Failed to parse deadline date: {}", e))?
1712 .and_hms_opt(0, 0, 0)
1713 .ok_or("Failed to create datetime")?,
1714 )
1715 } else {
1716 None
1717 };
1718
1719 sqlx::query(
1720 r#"
1721 INSERT INTO board_decisions (id, building_id, organization_id, meeting_id, subject, decision_text, deadline, status, created_at, updated_at)
1722 VALUES ($1, $2, $3, $4, $5, $6, $7, $8::decision_status, $9, $10)
1723 "#
1724 )
1725 .bind(decision_id)
1726 .bind(building_id)
1727 .bind(org_id)
1728 .bind(meeting_id)
1729 .bind(subject)
1730 .bind(decision_text)
1731 .bind(deadline_parsed)
1732 .bind(status)
1733 .bind(now)
1734 .bind(now)
1735 .execute(&self.pool)
1736 .await
1737 .map_err(|e| format!("Failed to create board decision: {}", e))?;
1738
1739 Ok(decision_id)
1740 }
1741
1742 pub async fn seed_realistic_data(&self) -> Result<String, String> {
1745 log::info!("🌱 Starting realistic data seeding...");
1746
1747 let existing_orgs = sqlx::query("SELECT COUNT(*) as count FROM organizations")
1749 .fetch_one(&self.pool)
1750 .await
1751 .map_err(|e| format!("Failed to count organizations: {}", e))?;
1752
1753 let count: i64 = existing_orgs
1754 .try_get("count")
1755 .map_err(|e| format!("Failed to get count: {}", e))?;
1756 if count > 0 {
1757 return Err("Data already exists. Please clear the database first.".to_string());
1758 }
1759
1760 let mut rng = rand::rng();
1761
1762 let cities = [
1764 "Bruxelles",
1765 "Anvers",
1766 "Gand",
1767 "Charleroi",
1768 "Liège",
1769 "Bruges",
1770 "Namur",
1771 "Louvain",
1772 ];
1773 let street_types = ["Rue", "Avenue", "Boulevard", "Place", "Chaussée"];
1774 let street_names = [
1775 "des Fleurs",
1776 "du Parc",
1777 "de la Gare",
1778 "Royale",
1779 "de l'Église",
1780 "du Commerce",
1781 "de la Liberté",
1782 "des Arts",
1783 "Victor Hugo",
1784 "Louise",
1785 ];
1786
1787 let org_configs = [
1789 ("Petite Copropriété SPRL", "small", 5, 30), ("Copropriété Moyenne SA", "medium", 8, 60), ("Grande Résidence NV", "large", 10, 100), ];
1793
1794 let mut total_buildings = 0;
1795 let mut total_units = 0;
1796 let mut total_owners = 0;
1797 let mut total_expenses = 0;
1798
1799 for (idx, (org_name, size, num_buildings, target_units)) in org_configs.iter().enumerate() {
1800 let org_id = Uuid::new_v4();
1801 let now = Utc::now();
1802
1803 log::info!(
1804 "📍 Organization {}: {} ({} buildings, ~{} units)",
1805 idx + 1,
1806 org_name,
1807 num_buildings,
1808 target_units
1809 );
1810
1811 sqlx::query(
1813 "INSERT INTO organizations (id, name, slug, contact_email, contact_phone, subscription_plan, max_buildings, max_users, is_active, created_at, updated_at)
1814 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)"
1815 )
1816 .bind(org_id)
1817 .bind(*org_name)
1818 .bind(format!("{}-{}", size, idx))
1819 .bind(format!("contact@{}.be", size))
1820 .bind(format!("+32 2 {} {} {}", rng.random_range(100..999), rng.random_range(10..99), rng.random_range(10..99)))
1821 .bind(if *size == "large" { "enterprise" } else if *size == "medium" { "professional" } else { "starter" })
1822 .bind(*num_buildings)
1823 .bind(if *size == "large" { 50 } else if *size == "medium" { 20 } else { 10 })
1824 .bind(true)
1825 .bind(now)
1826 .bind(now)
1827 .execute(&self.pool)
1828 .await
1829 .map_err(|e| format!("Failed to create organization: {}", e))?;
1830
1831 let user_id = Uuid::new_v4();
1833 let password_hash = hash("admin123", DEFAULT_COST)
1834 .map_err(|e| format!("Failed to hash password: {}", e))?;
1835
1836 sqlx::query(
1837 "INSERT INTO users (id, email, password_hash, first_name, last_name, role, organization_id, is_active, created_at, updated_at)
1838 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"
1839 )
1840 .bind(user_id)
1841 .bind(format!("admin@{}.be", size))
1842 .bind(&password_hash)
1843 .bind("Admin")
1844 .bind(org_name.split_whitespace().next().unwrap_or("User"))
1845 .bind("syndic")
1846 .bind(Some(org_id))
1847 .bind(true)
1848 .bind(now)
1849 .bind(now)
1850 .execute(&self.pool)
1851 .await
1852 .map_err(|e| format!("Failed to create user: {}", e))?;
1853
1854 let num_owners = (target_units * 2 / 3) as usize; let mut owner_ids = Vec::new();
1857
1858 for o in 0..num_owners {
1859 let owner_id = Uuid::new_v4();
1860
1861 let first_name: String = FirstName().fake();
1863 let last_name: String = LastName().fake();
1864 let street: String = StreetName().fake();
1865 let city_idx = rng.random_range(0..cities.len());
1866 let owner_city = cities[city_idx];
1867
1868 sqlx::query(
1869 "INSERT INTO owners (id, organization_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at)
1870 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)"
1871 )
1872 .bind(owner_id)
1873 .bind(org_id)
1874 .bind(&first_name)
1875 .bind(&last_name)
1876 .bind(format!("{}. {}{}@{}.be", first_name.chars().next().unwrap_or('x'), last_name.to_lowercase(), o + 1, size))
1877 .bind(format!("+32 {} {} {} {}",
1878 if rng.random_bool(0.5) { "2" } else { "4" },
1879 rng.random_range(100..999),
1880 rng.random_range(10..99),
1881 rng.random_range(10..99)
1882 ))
1883 .bind(format!("{} {}", street, rng.random_range(1..200)))
1884 .bind(owner_city)
1885 .bind(format!("{}", rng.random_range(1000..9999)))
1886 .bind("Belgium")
1887 .bind(now)
1888 .bind(now)
1889 .execute(&self.pool)
1890 .await
1891 .map_err(|e| format!("Failed to create owner: {}", e))?;
1892
1893 owner_ids.push(owner_id);
1894 }
1895
1896 total_owners += num_owners;
1897
1898 let units_per_building = target_units / num_buildings;
1900 let mut org_units = 0;
1901
1902 for b in 0..*num_buildings {
1903 let building_id = Uuid::new_v4();
1904 let city = cities[rng.random_range(0..cities.len())];
1905 let street_type = street_types[rng.random_range(0..street_types.len())];
1906 let street_name = street_names[rng.random_range(0..street_names.len())];
1907 let building_name = format!("Résidence {}", street_name);
1908
1909 sqlx::query(
1910 "INSERT INTO buildings (id, organization_id, name, address, city, postal_code, country, total_units, construction_year, created_at, updated_at)
1911 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)"
1912 )
1913 .bind(building_id)
1914 .bind(org_id)
1915 .bind(&building_name)
1916 .bind(format!("{} {} {}", street_type, street_name, rng.random_range(1..200)))
1917 .bind(city)
1918 .bind(format!("{}", rng.random_range(1000..9999)))
1919 .bind("Belgium")
1920 .bind(units_per_building)
1921 .bind(rng.random_range(1960..2024))
1922 .bind(now)
1923 .bind(now)
1924 .execute(&self.pool)
1925 .await
1926 .map_err(|e| format!("Failed to create building: {}", e))?;
1927
1928 let units_this_building = if b == num_buildings - 1 {
1930 target_units - org_units
1932 } else {
1933 units_per_building
1934 };
1935
1936 for u in 0..units_this_building {
1937 let floor = u / 4; let unit_number = format!("{}.{}", floor, (u % 4) + 1);
1939
1940 let owner_id = if rng.random_bool(0.66) && !owner_ids.is_empty() {
1942 Some(owner_ids[rng.random_range(0..owner_ids.len())])
1943 } else {
1944 None
1945 };
1946
1947 let unit_types = ["apartment", "apartment", "apartment", "parking", "cellar"];
1949 let unit_type = unit_types[rng.random_range(0..unit_types.len())];
1950
1951 sqlx::query(
1952 "INSERT INTO units (id, organization_id, building_id, unit_number, unit_type, floor, surface_area, quota, owner_id, created_at, updated_at)
1953 VALUES ($1, $2, $3, $4, $5::unit_type, $6, $7, $8, $9, $10, $11)"
1954 )
1955 .bind(Uuid::new_v4())
1956 .bind(org_id)
1957 .bind(building_id)
1958 .bind(&unit_number)
1959 .bind(unit_type)
1960 .bind(floor)
1961 .bind(rng.random_range(45.0..150.0))
1962 .bind(rng.random_range(50..200) as i32)
1963 .bind(owner_id)
1964 .bind(now)
1965 .bind(now)
1966 .execute(&self.pool)
1967 .await
1968 .map_err(|e| format!("Failed to create unit: {}", e))?;
1969 }
1970
1971 org_units += units_this_building;
1972
1973 let existing_expenses: (i64,) =
1975 sqlx::query_as("SELECT COUNT(*) FROM expenses WHERE building_id = $1")
1976 .bind(building_id)
1977 .fetch_one(&self.pool)
1978 .await
1979 .map_err(|e| format!("Failed to check existing expenses: {}", e))?;
1980
1981 if existing_expenses.0 == 0 {
1983 let num_expenses = rng.random_range(2..=3);
1985 let expense_types = [
1986 ("Entretien ascenseur", 450.0, 800.0),
1987 ("Nettoyage parties communes", 300.0, 600.0),
1988 ("Chauffage collectif", 1500.0, 3000.0),
1989 ("Assurance immeuble", 800.0, 1500.0),
1990 ("Travaux façade", 5000.0, 15000.0),
1991 ];
1992
1993 for _ in 0..num_expenses {
1994 let (desc, min_amount, max_amount) =
1995 expense_types[rng.random_range(0..expense_types.len())];
1996 let amount = rng.random_range(min_amount..max_amount);
1997 let days_ago = rng.random_range(0..90);
1998 let expense_date = Utc::now() - chrono::Duration::days(days_ago);
1999
2000 let categories = [
2002 "maintenance",
2003 "repairs",
2004 "insurance",
2005 "utilities",
2006 "cleaning",
2007 "administration",
2008 "works",
2009 ];
2010 let category = categories[rng.random_range(0..categories.len())];
2011
2012 let payment_status = if rng.random_bool(0.7) {
2014 "paid"
2015 } else {
2016 "pending"
2017 };
2018
2019 let approval_status = if payment_status == "paid" {
2021 "approved" } else {
2023 "draft" };
2025
2026 let paid_date = if payment_status == "paid" {
2028 Some(expense_date)
2029 } else {
2030 None
2031 };
2032
2033 sqlx::query(
2034 "INSERT INTO expenses (id, organization_id, building_id, category, description, amount, expense_date, payment_status, approval_status, paid_date, created_at, updated_at)
2035 VALUES ($1, $2, $3, $4::expense_category, $5, $6, $7, $8::payment_status, $9::approval_status, $10, $11, $12)"
2036 )
2037 .bind(Uuid::new_v4())
2038 .bind(org_id)
2039 .bind(building_id)
2040 .bind(category)
2041 .bind(desc)
2042 .bind(amount)
2043 .bind(expense_date)
2044 .bind(payment_status)
2045 .bind(approval_status)
2046 .bind(paid_date)
2047 .bind(now)
2048 .bind(now)
2049 .execute(&self.pool)
2050 .await
2051 .map_err(|e| format!("Failed to create expense: {}", e))?;
2052
2053 total_expenses += 1;
2054 }
2055 } }
2057
2058 total_buildings += num_buildings;
2059 total_units += org_units as usize;
2060
2061 log::info!(
2062 " ✅ Created {} buildings, {} units, {} owners",
2063 num_buildings,
2064 org_units,
2065 num_owners
2066 );
2067 }
2068
2069 Ok(format!(
2070 "✅ Realistic seed data created successfully!\n\
2071 Total: {} orgs, {} buildings, {} units, {} owners, {} expenses\n\
2072 \nTest credentials:\n\
2073 - Small org: admin@small.be / admin123\n\
2074 - Medium org: admin@medium.be / admin123\n\
2075 - Large org: admin@large.be / admin123",
2076 org_configs.len(),
2077 total_buildings,
2078 total_units,
2079 total_owners,
2080 total_expenses
2081 ))
2082 }
2083
2084 async fn seed_pcmn_accounts(&self, organization_id: Uuid) -> Result<(), String> {
2086 let accounts = vec![
2089 ("6100", "Charges courantes", "EXPENSE"),
2091 ("6110", "Entretien et réparations", "EXPENSE"),
2092 ("6120", "Personnel", "EXPENSE"),
2093 ("6130", "Services extérieurs", "EXPENSE"),
2094 ("6140", "Honoraires et commissions", "EXPENSE"),
2095 ("6150", "Assurances", "EXPENSE"),
2096 ("6200", "Travaux extraordinaires", "EXPENSE"),
2097 ("7000", "Appels de fonds ordinaires", "REVENUE"),
2099 ("7100", "Appels de fonds extraordinaires", "REVENUE"),
2100 ("7200", "Autres produits", "REVENUE"),
2101 ("4000", "Copropriétaires débiteurs", "ASSET"),
2103 ("4110", "TVA à récupérer", "ASSET"),
2104 ("4400", "Fournisseurs", "LIABILITY"),
2105 ("4500", "TVA à payer", "LIABILITY"),
2106 ("5500", "Banque compte courant", "ASSET"),
2108 ("5700", "Caisse", "ASSET"),
2109 ];
2110
2111 let now = Utc::now();
2112
2113 for (code, label, account_type_str) in accounts {
2114 sqlx::query(
2115 r#"
2116 INSERT INTO accounts (id, code, label, parent_code, account_type, direct_use, organization_id, created_at, updated_at)
2117 VALUES ($1, $2, $3, $4, $5::account_type, $6, $7, $8, $9)
2118 ON CONFLICT (code, organization_id) DO NOTHING
2119 "#
2120 )
2121 .bind(Uuid::new_v4())
2122 .bind(code)
2123 .bind(label)
2124 .bind(None::<String>) .bind(account_type_str)
2126 .bind(true) .bind(organization_id)
2128 .bind(now)
2129 .bind(now)
2130 .execute(&self.pool)
2131 .await
2132 .map_err(|e| format!("Failed to seed account {}: {}", code, e))?;
2133 }
2134
2135 Ok(())
2136 }
2137
2138 #[allow(clippy::too_many_arguments)]
2140 async fn create_demo_expense_with_vat(
2141 &self,
2142 building_id: Uuid,
2143 organization_id: Uuid,
2144 description: &str,
2145 amount_excl_vat: f64,
2146 vat_rate: f64,
2147 expense_date: &str,
2148 due_date: &str,
2149 category: &str,
2150 payment_status: &str,
2151 supplier: Option<&str>,
2152 invoice_number: Option<&str>,
2153 account_code: Option<&str>,
2154 ) -> Result<Uuid, String> {
2155 let expense_id = Uuid::new_v4();
2156 let now = Utc::now();
2157
2158 let vat_amount = (amount_excl_vat * vat_rate / 100.0 * 100.0).round() / 100.0;
2160 let amount = amount_excl_vat + vat_amount;
2161
2162 let expense_date_parsed =
2163 chrono::DateTime::parse_from_rfc3339(&format!("{}T00:00:00Z", expense_date))
2164 .map_err(|e| format!("Failed to parse expense_date: {}", e))?
2165 .with_timezone(&Utc);
2166
2167 let due_date_parsed =
2168 chrono::DateTime::parse_from_rfc3339(&format!("{}T00:00:00Z", due_date))
2169 .map_err(|e| format!("Failed to parse due_date: {}", e))?
2170 .with_timezone(&Utc);
2171
2172 let paid_date = if payment_status == "paid" {
2174 Some(expense_date_parsed) } else {
2176 None
2177 };
2178
2179 let existing: Option<(Uuid,)> =
2181 sqlx::query_as("SELECT id FROM expenses WHERE description = $1 AND building_id = $2")
2182 .bind(description)
2183 .bind(building_id)
2184 .fetch_optional(&self.pool)
2185 .await
2186 .map_err(|e| format!("Failed to check existing expense: {}", e))?;
2187
2188 let expense_id = if let Some((existing_id,)) = existing {
2189 sqlx::query(
2191 r#"
2192 UPDATE expenses SET
2193 category = $1::expense_category,
2194 amount = $2,
2195 amount_excl_vat = $3,
2196 vat_rate = $4,
2197 expense_date = $5,
2198 due_date = $6,
2199 payment_status = $7::payment_status,
2200 paid_date = $8,
2201 approval_status = $9::approval_status,
2202 supplier = $10,
2203 invoice_number = $11,
2204 account_code = $12,
2205 updated_at = $13
2206 WHERE id = $14
2207 "#,
2208 )
2209 .bind(category)
2210 .bind(amount)
2211 .bind(amount_excl_vat)
2212 .bind(vat_rate)
2213 .bind(expense_date_parsed)
2214 .bind(due_date_parsed)
2215 .bind(payment_status)
2216 .bind(paid_date)
2217 .bind("approved")
2218 .bind(supplier)
2219 .bind(invoice_number)
2220 .bind(account_code)
2221 .bind(now)
2222 .bind(existing_id)
2223 .execute(&self.pool)
2224 .await
2225 .map_err(|e| format!("Failed to update expense: {}", e))?;
2226 existing_id
2227 } else {
2228 sqlx::query(
2230 r#"
2231 INSERT INTO expenses (
2232 id, organization_id, building_id, category, description,
2233 amount, amount_excl_vat, vat_rate, expense_date, due_date,
2234 payment_status, paid_date, approval_status, supplier, invoice_number,
2235 account_code, created_at, updated_at
2236 )
2237 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)
2238 "#
2239 )
2240 .bind(expense_id)
2241 .bind(organization_id)
2242 .bind(building_id)
2243 .bind(category)
2244 .bind(description)
2245 .bind(amount)
2246 .bind(amount_excl_vat)
2247 .bind(vat_rate)
2248 .bind(expense_date_parsed)
2249 .bind(due_date_parsed)
2250 .bind(payment_status)
2251 .bind(paid_date)
2252 .bind("approved")
2253 .bind(supplier)
2254 .bind(invoice_number)
2255 .bind(account_code)
2256 .bind(now)
2257 .bind(now)
2258 .execute(&self.pool)
2259 .await
2260 .map_err(|e| format!("Failed to create expense with VAT: {}", e))?;
2261 expense_id
2262 };
2263
2264 if let Some(acc_code) = account_code {
2266 self.generate_journal_entry_for_expense(
2267 expense_id,
2268 organization_id,
2269 building_id,
2270 description,
2271 amount_excl_vat,
2272 vat_rate,
2273 amount,
2274 expense_date_parsed,
2275 acc_code,
2276 supplier,
2277 invoice_number,
2278 )
2279 .await?;
2280 }
2281
2282 Ok(expense_id)
2283 }
2284
2285 #[allow(clippy::too_many_arguments)]
2292 async fn generate_journal_entry_for_expense(
2293 &self,
2294 expense_id: Uuid,
2295 organization_id: Uuid,
2296 _building_id: Uuid,
2297 description: &str,
2298 amount_excl_vat: f64,
2299 vat_rate: f64,
2300 total_amount: f64,
2301 expense_date: chrono::DateTime<Utc>,
2302 account_code: &str,
2303 supplier: Option<&str>,
2304 invoice_number: Option<&str>,
2305 ) -> Result<(), String> {
2306 let journal_entry_id = Uuid::new_v4();
2307 let now = Utc::now();
2308
2309 let vat_amount = total_amount - amount_excl_vat;
2311
2312 let mut tx = self
2314 .pool
2315 .begin()
2316 .await
2317 .map_err(|e| format!("Failed to begin transaction: {}", e))?;
2318
2319 sqlx::query!(
2321 r#"
2322 INSERT INTO journal_entries (
2323 id, organization_id, entry_date, description,
2324 document_ref, expense_id, created_at, updated_at
2325 )
2326 VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
2327 "#,
2328 journal_entry_id,
2329 organization_id,
2330 expense_date,
2331 format!("{} - {}", description, supplier.unwrap_or("Fournisseur")),
2332 invoice_number,
2333 expense_id,
2334 now,
2335 now
2336 )
2337 .execute(&mut *tx)
2338 .await
2339 .map_err(|e| format!("Failed to create journal entry: {}", e))?;
2340
2341 sqlx::query!(
2343 r#"
2344 INSERT INTO journal_entry_lines (
2345 journal_entry_id, organization_id, account_code,
2346 debit, credit, description
2347 )
2348 VALUES ($1, $2, $3, $4, $5, $6)
2349 "#,
2350 journal_entry_id,
2351 organization_id,
2352 account_code,
2353 rust_decimal::Decimal::from_f64_retain(amount_excl_vat).unwrap_or_default(),
2354 rust_decimal::Decimal::from_f64_retain(0.0).unwrap_or_default(),
2355 format!("Dépense: {}", description)
2356 )
2357 .execute(&mut *tx)
2358 .await
2359 .map_err(|e| format!("Failed to create expense debit line: {}", e))?;
2360
2361 if vat_amount > 0.01 {
2363 sqlx::query!(
2364 r#"
2365 INSERT INTO journal_entry_lines (
2366 journal_entry_id, organization_id, account_code,
2367 debit, credit, description
2368 )
2369 VALUES ($1, $2, $3, $4, $5, $6)
2370 "#,
2371 journal_entry_id,
2372 organization_id,
2373 "4110", rust_decimal::Decimal::from_f64_retain(vat_amount).unwrap_or_default(),
2375 rust_decimal::Decimal::from_f64_retain(0.0).unwrap_or_default(),
2376 format!("TVA récupérable {}%", vat_rate)
2377 )
2378 .execute(&mut *tx)
2379 .await
2380 .map_err(|e| format!("Failed to create VAT debit line: {}", e))?;
2381 }
2382
2383 sqlx::query!(
2385 r#"
2386 INSERT INTO journal_entry_lines (
2387 journal_entry_id, organization_id, account_code,
2388 debit, credit, description
2389 )
2390 VALUES ($1, $2, $3, $4, $5, $6)
2391 "#,
2392 journal_entry_id,
2393 organization_id,
2394 "4400", rust_decimal::Decimal::from_f64_retain(0.0).unwrap_or_default(),
2396 rust_decimal::Decimal::from_f64_retain(total_amount).unwrap_or_default(),
2397 supplier.map(|s| format!("Fournisseur: {}", s))
2398 )
2399 .execute(&mut *tx)
2400 .await
2401 .map_err(|e| format!("Failed to create supplier credit line: {}", e))?;
2402
2403 tx.commit()
2405 .await
2406 .map_err(|e| format!("Failed to commit journal entry transaction: {}", e))?;
2407
2408 Ok(())
2409 }
2410
2411 async fn create_demo_distributions(
2413 &self,
2414 expense_id: Uuid,
2415 _organization_id: Uuid,
2416 ) -> Result<(), String> {
2417 let expense_row = sqlx::query!(
2419 "SELECT building_id, amount FROM expenses WHERE id = $1",
2420 expense_id
2421 )
2422 .fetch_one(&self.pool)
2423 .await
2424 .map_err(|e| format!("Failed to fetch expense: {}", e))?;
2425
2426 let building_id = expense_row.building_id;
2427 let total_amount = expense_row.amount;
2428
2429 let units = sqlx::query!(
2431 r#"
2432 SELECT u.id as unit_id, u.quota
2433 FROM units u
2434 WHERE u.building_id = $1
2435 "#,
2436 building_id
2437 )
2438 .fetch_all(&self.pool)
2439 .await
2440 .map_err(|e| format!("Failed to fetch units: {}", e))?;
2441
2442 if units.is_empty() {
2443 return Ok(()); }
2445
2446 let total_quota: f64 = units.iter().map(|u| u.quota).sum();
2448
2449 let now = Utc::now();
2450
2451 for unit in units {
2454 let primary_owner = sqlx::query!(
2456 r#"
2457 SELECT owner_id
2458 FROM unit_owners
2459 WHERE unit_id = $1 AND end_date IS NULL AND is_primary_contact = true
2460 ORDER BY created_at ASC
2461 LIMIT 1
2462 "#,
2463 unit.unit_id
2464 )
2465 .fetch_optional(&self.pool)
2466 .await
2467 .map_err(|e| format!("Failed to fetch primary owner: {}", e))?;
2468
2469 let owner_id = match primary_owner {
2471 Some(owner) => owner.owner_id,
2472 None => continue, };
2474
2475 let quota_percentage = if total_quota > 0.0 {
2476 unit.quota / total_quota
2477 } else {
2478 0.0
2479 };
2480
2481 let amount_due = if total_quota > 0.0 {
2482 (quota_percentage * total_amount * 100.0).round() / 100.0
2483 } else {
2484 0.0
2485 };
2486
2487 sqlx::query(
2488 r#"
2489 INSERT INTO charge_distributions (
2490 id, expense_id, unit_id, owner_id,
2491 quota_percentage, amount_due, created_at
2492 )
2493 VALUES ($1, $2, $3, $4, $5, $6, $7)
2494 "#,
2495 )
2496 .bind(Uuid::new_v4())
2497 .bind(expense_id)
2498 .bind(unit.unit_id)
2499 .bind(owner_id)
2500 .bind(quota_percentage)
2501 .bind(amount_due)
2502 .bind(now)
2503 .execute(&self.pool)
2504 .await
2505 .map_err(|e| format!("Failed to create charge distribution: {}", e))?;
2506 }
2507
2508 Ok(())
2509 }
2510
2511 #[allow(clippy::too_many_arguments)]
2513 async fn create_demo_owner_contribution(
2514 &self,
2515 organization_id: Uuid,
2516 owner_id: Uuid,
2517 unit_id: Option<Uuid>,
2518 description: &str,
2519 amount: f64,
2520 contribution_type: &str,
2521 contribution_date: &str,
2522 payment_status: &str,
2523 payment_date: Option<&str>,
2524 account_code: Option<&str>,
2525 ) -> Result<Uuid, String> {
2526 let contribution_id = Uuid::new_v4();
2527 let contribution_date = NaiveDate::parse_from_str(contribution_date, "%Y-%m-%d")
2528 .map_err(|e| format!("Invalid contribution date: {}", e))?
2529 .and_hms_opt(10, 0, 0)
2530 .ok_or("Invalid contribution time")?
2531 .and_local_timezone(Utc)
2532 .unwrap();
2533
2534 let payment_date_tz = payment_date
2535 .map(|date_str| {
2536 NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
2537 .map_err(|e| format!("Invalid payment date: {}", e))
2538 .and_then(|date| {
2539 date.and_hms_opt(10, 0, 0)
2540 .ok_or("Invalid payment time".to_string())
2541 })
2542 .map(|dt| dt.and_local_timezone(Utc).unwrap())
2543 })
2544 .transpose()?;
2545
2546 let payment_method = if payment_status == "paid" {
2547 Some("bank_transfer")
2548 } else {
2549 None
2550 };
2551
2552 let now = Utc::now();
2553
2554 sqlx::query(
2555 r#"
2556 INSERT INTO owner_contributions (
2557 id, organization_id, owner_id, unit_id,
2558 description, amount, account_code,
2559 contribution_type, contribution_date, payment_date,
2560 payment_method, payment_status,
2561 created_at, updated_at
2562 )
2563 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
2564 "#,
2565 )
2566 .bind(contribution_id)
2567 .bind(organization_id)
2568 .bind(owner_id)
2569 .bind(unit_id)
2570 .bind(description)
2571 .bind(amount)
2572 .bind(account_code)
2573 .bind(contribution_type)
2574 .bind(contribution_date)
2575 .bind(payment_date_tz)
2576 .bind(payment_method)
2577 .bind(payment_status)
2578 .bind(now)
2579 .bind(now)
2580 .execute(&self.pool)
2581 .await
2582 .map_err(|e| format!("Failed to create owner contribution: {}", e))?;
2583
2584 if let Some(acc_code) = account_code {
2586 self.generate_journal_entry_for_contribution(
2587 contribution_id,
2588 organization_id,
2589 description,
2590 amount,
2591 contribution_date,
2592 acc_code,
2593 )
2594 .await?;
2595 }
2596
2597 Ok(contribution_id)
2598 }
2599
2600 async fn generate_journal_entry_for_contribution(
2606 &self,
2607 contribution_id: Uuid,
2608 organization_id: Uuid,
2609 description: &str,
2610 amount: f64,
2611 contribution_date: chrono::DateTime<Utc>,
2612 account_code: &str,
2613 ) -> Result<(), String> {
2614 let journal_entry_id = Uuid::new_v4();
2615 let now = Utc::now();
2616
2617 let mut tx = self
2619 .pool
2620 .begin()
2621 .await
2622 .map_err(|e| format!("Failed to begin transaction: {}", e))?;
2623
2624 sqlx::query("SET CONSTRAINTS ALL DEFERRED")
2626 .execute(&mut *tx)
2627 .await
2628 .map_err(|e| format!("Failed to defer constraints: {}", e))?;
2629
2630 sqlx::query(
2632 r#"
2633 INSERT INTO journal_entries (
2634 id, organization_id, entry_date, description,
2635 contribution_id, created_at, updated_at
2636 )
2637 VALUES ($1, $2, $3, $4, $5, $6, $7)
2638 "#,
2639 )
2640 .bind(journal_entry_id)
2641 .bind(organization_id)
2642 .bind(contribution_date)
2643 .bind(description)
2644 .bind(contribution_id)
2645 .bind(now)
2646 .bind(now)
2647 .execute(&mut *tx)
2648 .await
2649 .map_err(|e| format!("Failed to create journal entry: {}", e))?;
2650
2651 sqlx::query(
2653 r#"
2654 INSERT INTO journal_entry_lines (
2655 id, journal_entry_id, organization_id, account_code,
2656 description, debit, credit, created_at
2657 )
2658 VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
2659 "#,
2660 )
2661 .bind(Uuid::new_v4())
2662 .bind(journal_entry_id)
2663 .bind(organization_id)
2664 .bind("4000") .bind(format!("Créance - {}", description))
2666 .bind(amount) .bind(0.0) .bind(now)
2669 .execute(&mut *tx)
2670 .await
2671 .map_err(|e| format!("Failed to create debit line (4000): {}", e))?;
2672
2673 sqlx::query(
2675 r#"
2676 INSERT INTO journal_entry_lines (
2677 id, journal_entry_id, organization_id, account_code,
2678 description, debit, credit, created_at
2679 )
2680 VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
2681 "#,
2682 )
2683 .bind(Uuid::new_v4())
2684 .bind(journal_entry_id)
2685 .bind(organization_id)
2686 .bind(account_code) .bind(format!("Produit - {}", description))
2688 .bind(0.0) .bind(amount) .bind(now)
2691 .execute(&mut *tx)
2692 .await
2693 .map_err(|e| format!("Failed to create credit line ({}): {}", account_code, e))?;
2694
2695 tx.commit()
2697 .await
2698 .map_err(|e| format!("Failed to commit transaction: {}", e))?;
2699
2700 Ok(())
2701 }
2702
2703 #[allow(clippy::too_many_arguments)]
2705 async fn create_demo_payment_reminder(
2706 &self,
2707 expense_id: Uuid,
2708 owner_id: Uuid,
2709 organization_id: Uuid,
2710 reminder_level: &str,
2711 days_overdue: i64,
2712 ) -> Result<Uuid, String> {
2713 let reminder_id = Uuid::new_v4();
2714 let now = Utc::now();
2715
2716 let expense = sqlx::query!(
2718 "SELECT amount, due_date FROM expenses WHERE id = $1",
2719 expense_id
2720 )
2721 .fetch_one(&self.pool)
2722 .await
2723 .map_err(|e| format!("Failed to fetch expense: {}", e))?;
2724
2725 let amount_owed = expense.amount;
2726 let due_date = expense
2727 .due_date
2728 .expect("Due date required for payment reminder");
2729
2730 let penalty_amount = if days_overdue > 0 {
2732 let yearly_penalty = amount_owed * 0.08;
2733 let daily_penalty = yearly_penalty / 365.0;
2734 ((daily_penalty * days_overdue as f64) * 100.0).round() / 100.0
2735 } else {
2736 0.0
2737 };
2738
2739 let total_amount = amount_owed + penalty_amount;
2740 let sent_date = now - chrono::Duration::days(5); sqlx::query(
2743 r#"
2744 INSERT INTO payment_reminders (
2745 id, organization_id, expense_id, owner_id,
2746 level, status, amount_owed, penalty_amount, total_amount,
2747 due_date, days_overdue, delivery_method, sent_date,
2748 created_at, updated_at
2749 )
2750 VALUES ($1, $2, $3, $4, $5::reminder_level, $6::reminder_status, $7, $8, $9, $10, $11, $12::delivery_method, $13, $14, $15)
2751 "#
2752 )
2753 .bind(reminder_id)
2754 .bind(organization_id)
2755 .bind(expense_id)
2756 .bind(owner_id)
2757 .bind(reminder_level) .bind("Sent") .bind(amount_owed)
2760 .bind(penalty_amount)
2761 .bind(total_amount)
2762 .bind(due_date)
2763 .bind(days_overdue as i32)
2764 .bind("Email") .bind(sent_date)
2766 .bind(now)
2767 .bind(now)
2768 .execute(&self.pool)
2769 .await
2770 .map_err(|e| format!("Failed to create payment reminder: {}", e))?;
2771
2772 Ok(reminder_id)
2773 }
2774
2775 pub async fn clear_demo_data(&self) -> Result<String, String> {
2777 log::warn!("⚠️ Clearing seed data only (preserving production data)...");
2778
2779 let seed_org_ids: Vec<Uuid> =
2781 sqlx::query_scalar!("SELECT id FROM organizations WHERE is_seed_data = true")
2782 .fetch_all(&self.pool)
2783 .await
2784 .map_err(|e| format!("Failed to fetch seed organizations: {}", e))?;
2785
2786 if seed_org_ids.is_empty() {
2787 return Ok("ℹ️ No seed data found to clear.".to_string());
2788 }
2789
2790 log::info!("Found {} seed organizations to clean", seed_org_ids.len());
2791
2792 sqlx::query!(
2795 "DELETE FROM board_decisions WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2796 &seed_org_ids
2797 )
2798 .execute(&self.pool)
2799 .await
2800 .map_err(|e| format!("Failed to delete board_decisions: {}", e))?;
2801
2802 sqlx::query!(
2804 "DELETE FROM board_members WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2805 &seed_org_ids
2806 )
2807 .execute(&self.pool)
2808 .await
2809 .map_err(|e| format!("Failed to delete board_members: {}", e))?;
2810
2811 sqlx::query!(
2813 "DELETE FROM payment_reminders WHERE organization_id = ANY($1)",
2814 &seed_org_ids
2815 )
2816 .execute(&self.pool)
2817 .await
2818 .map_err(|e| format!("Failed to delete payment_reminders: {}", e))?;
2819
2820 sqlx::query!(
2822 "DELETE FROM owner_contributions WHERE organization_id = ANY($1)",
2823 &seed_org_ids
2824 )
2825 .execute(&self.pool)
2826 .await
2827 .map_err(|e| format!("Failed to delete owner_contributions: {}", e))?;
2828
2829 sqlx::query(
2831 "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)))"
2832 )
2833 .bind(&seed_org_ids)
2834 .execute(&self.pool)
2835 .await
2836 .map_err(|e| format!("Failed to delete charge_distributions: {}", e))?;
2837
2838 sqlx::query(
2840 "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)))"
2841 )
2842 .bind(&seed_org_ids)
2843 .execute(&self.pool)
2844 .await
2845 .map_err(|e| format!("Failed to delete invoice_line_items: {}", e))?;
2846
2847 sqlx::query!(
2849 "DELETE FROM documents WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2850 &seed_org_ids
2851 )
2852 .execute(&self.pool)
2853 .await
2854 .map_err(|e| format!("Failed to delete documents: {}", e))?;
2855
2856 sqlx::query!(
2858 "DELETE FROM meetings WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2859 &seed_org_ids
2860 )
2861 .execute(&self.pool)
2862 .await
2863 .map_err(|e| format!("Failed to delete meetings: {}", e))?;
2864
2865 sqlx::query!(
2867 "DELETE FROM journal_entry_lines WHERE organization_id = ANY($1)",
2868 &seed_org_ids
2869 )
2870 .execute(&self.pool)
2871 .await
2872 .map_err(|e| format!("Failed to delete journal_entry_lines: {}", e))?;
2873
2874 sqlx::query!(
2876 "DELETE FROM journal_entries WHERE organization_id = ANY($1)",
2877 &seed_org_ids
2878 )
2879 .execute(&self.pool)
2880 .await
2881 .map_err(|e| format!("Failed to delete journal_entries: {}", e))?;
2882
2883 sqlx::query!(
2885 "DELETE FROM expenses WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2886 &seed_org_ids
2887 )
2888 .execute(&self.pool)
2889 .await
2890 .map_err(|e| format!("Failed to delete expenses: {}", e))?;
2891
2892 sqlx::query(
2894 "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))"
2895 )
2896 .bind(&seed_org_ids)
2897 .execute(&self.pool)
2898 .await
2899 .map_err(|e| format!("Failed to delete unit_owners: {}", e))?;
2900
2901 sqlx::query!(
2903 "DELETE FROM units WHERE building_id IN (SELECT id FROM buildings WHERE organization_id = ANY($1))",
2904 &seed_org_ids
2905 )
2906 .execute(&self.pool)
2907 .await
2908 .map_err(|e| format!("Failed to delete units: {}", e))?;
2909
2910 sqlx::query!(
2912 "DELETE FROM owners WHERE organization_id = ANY($1)",
2913 &seed_org_ids
2914 )
2915 .execute(&self.pool)
2916 .await
2917 .map_err(|e| format!("Failed to delete owners: {}", e))?;
2918
2919 sqlx::query!(
2921 "DELETE FROM buildings WHERE organization_id = ANY($1)",
2922 &seed_org_ids
2923 )
2924 .execute(&self.pool)
2925 .await
2926 .map_err(|e| format!("Failed to delete buildings: {}", e))?;
2927
2928 sqlx::query!(
2930 "DELETE FROM accounts WHERE organization_id = ANY($1)",
2931 &seed_org_ids
2932 )
2933 .execute(&self.pool)
2934 .await
2935 .map_err(|e| format!("Failed to delete accounts: {}", e))?;
2936
2937 sqlx::query(
2939 "DELETE FROM user_roles WHERE user_id IN (SELECT id FROM users WHERE organization_id = ANY($1) AND role != 'superadmin')"
2940 )
2941 .bind(&seed_org_ids)
2942 .execute(&self.pool)
2943 .await
2944 .map_err(|e| format!("Failed to delete user_roles: {}", e))?;
2945
2946 sqlx::query!(
2948 "DELETE FROM users WHERE organization_id = ANY($1) AND role != 'superadmin'",
2949 &seed_org_ids
2950 )
2951 .execute(&self.pool)
2952 .await
2953 .map_err(|e| format!("Failed to delete users: {}", e))?;
2954
2955 sqlx::query!("DELETE FROM organizations WHERE is_seed_data = true")
2957 .execute(&self.pool)
2958 .await
2959 .map_err(|e| format!("Failed to delete organizations: {}", e))?;
2960
2961 log::info!("✅ Seed data cleared (production data and superadmin preserved)");
2962
2963 Ok(format!(
2964 "✅ Seed data cleared successfully! ({} organizations removed)",
2965 seed_org_ids.len()
2966 ))
2967 }
2968}
2969
2970#[cfg(test)]
2971mod tests {
2972 use super::*;
2973 use sqlx::PgPool;
2974
2975 #[sqlx::test]
2979 async fn test_seed_superadmin_is_idempotent(pool: PgPool) -> sqlx::Result<()> {
2980 let seeder = DatabaseSeeder::new(pool.clone());
2981
2982 let result1 = seeder.seed_superadmin().await;
2984 assert!(result1.is_ok(), "First seed_superadmin call should succeed");
2985 let user1 = result1.unwrap();
2986 assert_eq!(user1.email, "admin@koprogo.com");
2987 assert_eq!(user1.role, UserRole::SuperAdmin);
2988
2989 let result2 = seeder.seed_superadmin().await;
2991 assert!(
2992 result2.is_ok(),
2993 "Second seed_superadmin call should succeed (idempotent): {:?}",
2994 result2.err()
2995 );
2996 let user2 = result2.unwrap();
2997 assert_eq!(user2.email, "admin@koprogo.com");
2998 assert_eq!(user2.id, user1.id, "Superadmin UUID should remain the same");
2999
3000 let result3 = seeder.seed_superadmin().await;
3002 assert!(
3003 result3.is_ok(),
3004 "Third seed_superadmin call should succeed (idempotent): {:?}",
3005 result3.err()
3006 );
3007
3008 let primary_role_count = sqlx::query_scalar::<_, i64>(
3010 r#"
3011 SELECT COUNT(*)
3012 FROM user_roles
3013 WHERE user_id = $1 AND is_primary = true
3014 "#,
3015 )
3016 .bind(user1.id)
3017 .fetch_one(&pool)
3018 .await?;
3019
3020 assert_eq!(
3021 primary_role_count, 1,
3022 "Superadmin should have exactly ONE primary role, found {}",
3023 primary_role_count
3024 );
3025
3026 let role_count = sqlx::query_scalar::<_, i64>(
3028 r#"
3029 SELECT COUNT(*)
3030 FROM user_roles
3031 WHERE user_id = $1 AND role = 'superadmin' AND organization_id IS NULL
3032 "#,
3033 )
3034 .bind(user1.id)
3035 .fetch_one(&pool)
3036 .await?;
3037
3038 assert_eq!(
3039 role_count, 1,
3040 "Superadmin should have exactly ONE superadmin role, found {}",
3041 role_count
3042 );
3043
3044 Ok(())
3045 }
3046
3047 #[sqlx::test]
3051 async fn test_seed_superadmin_preserves_existing_primary_role(
3052 pool: PgPool,
3053 ) -> sqlx::Result<()> {
3054 let seeder = DatabaseSeeder::new(pool.clone());
3055
3056 seeder.seed_superadmin().await.unwrap();
3058
3059 let is_primary_before = sqlx::query_scalar::<_, bool>(
3061 r#"
3062 SELECT is_primary
3063 FROM user_roles
3064 WHERE user_id = '00000000-0000-0000-0000-000000000001'
3065 AND role = 'superadmin'
3066 AND organization_id IS NULL
3067 "#,
3068 )
3069 .fetch_one(&pool)
3070 .await?;
3071
3072 assert!(
3073 is_primary_before,
3074 "Superadmin role should be primary after first seed"
3075 );
3076
3077 let result = seeder.seed_superadmin().await;
3079 assert!(
3080 result.is_ok(),
3081 "Second seed should not violate idx_user_roles_primary_per_user constraint: {:?}",
3082 result.err()
3083 );
3084
3085 let is_primary_after = sqlx::query_scalar::<_, bool>(
3087 r#"
3088 SELECT is_primary
3089 FROM user_roles
3090 WHERE user_id = '00000000-0000-0000-0000-000000000001'
3091 AND role = 'superadmin'
3092 AND organization_id IS NULL
3093 "#,
3094 )
3095 .fetch_one(&pool)
3096 .await?;
3097
3098 assert!(
3099 is_primary_after,
3100 "Superadmin role should remain primary after second seed"
3101 );
3102
3103 Ok(())
3104 }
3105}