1use std::fmt;
9
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
18#[serde(rename_all = "snake_case")]
19pub enum UserPersona {
20 JuniorAccountant,
22 SeniorAccountant,
24 Controller,
26 Manager,
28 Executive,
30 #[default]
32 AutomatedSystem,
33 ExternalAuditor,
35 FraudActor,
37}
38
39impl fmt::Display for UserPersona {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::JuniorAccountant => write!(f, "junior_accountant"),
43 Self::SeniorAccountant => write!(f, "senior_accountant"),
44 Self::Controller => write!(f, "controller"),
45 Self::Manager => write!(f, "manager"),
46 Self::Executive => write!(f, "executive"),
47 Self::AutomatedSystem => write!(f, "automated_system"),
48 Self::ExternalAuditor => write!(f, "external_auditor"),
49 Self::FraudActor => write!(f, "fraud_actor"),
50 }
51 }
52}
53
54impl UserPersona {
55 pub fn is_human(&self) -> bool {
57 !matches!(self, Self::AutomatedSystem)
58 }
59
60 pub fn has_approval_authority(&self) -> bool {
62 matches!(self, Self::Controller | Self::Manager | Self::Executive)
63 }
64
65 pub fn error_rate(&self) -> f64 {
67 match self {
68 Self::JuniorAccountant => 0.02,
69 Self::SeniorAccountant => 0.005,
70 Self::Controller => 0.002,
71 Self::Manager => 0.003,
72 Self::Executive => 0.001,
73 Self::AutomatedSystem => 0.0001,
74 Self::ExternalAuditor => 0.0,
75 Self::FraudActor => 0.01,
76 }
77 }
78
79 pub fn typical_daily_volume(&self) -> (u32, u32) {
81 match self {
82 Self::JuniorAccountant => (20, 100),
83 Self::SeniorAccountant => (10, 50),
84 Self::Controller => (5, 20),
85 Self::Manager => (1, 10),
86 Self::Executive => (0, 5),
87 Self::AutomatedSystem => (100, 10000),
88 Self::ExternalAuditor => (0, 0),
89 Self::FraudActor => (1, 5),
90 }
91 }
92
93 pub fn approval_threshold(&self) -> Option<f64> {
95 match self {
96 Self::JuniorAccountant => Some(1000.0),
97 Self::SeniorAccountant => Some(10000.0),
98 Self::Controller => Some(100000.0),
99 Self::Manager => Some(500000.0),
100 Self::Executive => None, Self::AutomatedSystem => Some(1000000.0),
102 Self::ExternalAuditor => Some(0.0), Self::FraudActor => Some(10000.0),
104 }
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct WorkingHoursPattern {
111 pub start_hour: u8,
113 pub end_hour: u8,
115 pub peak_hours: Vec<u8>,
117 pub weekend_probability: f64,
119 pub after_hours_probability: f64,
121}
122
123impl Default for WorkingHoursPattern {
124 fn default() -> Self {
125 Self {
126 start_hour: 8,
127 end_hour: 18,
128 peak_hours: vec![10, 11, 14, 15],
129 weekend_probability: 0.05,
130 after_hours_probability: 0.10,
131 }
132 }
133}
134
135impl WorkingHoursPattern {
136 pub fn european() -> Self {
138 Self {
139 start_hour: 9,
140 end_hour: 17,
141 peak_hours: vec![10, 11, 14, 15],
142 weekend_probability: 0.02,
143 after_hours_probability: 0.05,
144 }
145 }
146
147 pub fn us_standard() -> Self {
149 Self {
150 start_hour: 8,
151 end_hour: 17,
152 peak_hours: vec![9, 10, 14, 15],
153 weekend_probability: 0.05,
154 after_hours_probability: 0.10,
155 }
156 }
157
158 pub fn asian() -> Self {
160 Self {
161 start_hour: 9,
162 end_hour: 18,
163 peak_hours: vec![10, 11, 15, 16],
164 weekend_probability: 0.10,
165 after_hours_probability: 0.15,
166 }
167 }
168
169 pub fn batch_processing() -> Self {
171 Self {
172 start_hour: 0,
173 end_hour: 24,
174 peak_hours: vec![2, 3, 4, 22, 23], weekend_probability: 1.0,
176 after_hours_probability: 1.0,
177 }
178 }
179
180 pub fn is_working_hour(&self, hour: u8) -> bool {
182 hour >= self.start_hour && hour < self.end_hour
183 }
184
185 pub fn is_peak_hour(&self, hour: u8) -> bool {
187 self.peak_hours.contains(&hour)
188 }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct User {
194 pub user_id: String,
196
197 pub display_name: String,
199
200 pub email: Option<String>,
202
203 pub persona: UserPersona,
205
206 pub department: Option<String>,
208
209 pub working_hours: WorkingHoursPattern,
211
212 pub company_codes: Vec<String>,
214
215 pub cost_centers: Vec<String>,
217
218 pub is_active: bool,
220
221 pub start_date: Option<chrono::NaiveDate>,
223
224 pub end_date: Option<chrono::NaiveDate>,
226}
227
228impl User {
229 pub fn new(user_id: String, display_name: String, persona: UserPersona) -> Self {
231 let working_hours = if persona.is_human() {
232 WorkingHoursPattern::default()
233 } else {
234 WorkingHoursPattern::batch_processing()
235 };
236
237 Self {
238 user_id,
239 display_name,
240 email: None,
241 persona,
242 department: None,
243 working_hours,
244 company_codes: Vec::new(),
245 cost_centers: Vec::new(),
246 is_active: true,
247 start_date: None,
248 end_date: None,
249 }
250 }
251
252 pub fn system(user_id: &str) -> Self {
254 Self::new(
255 user_id.to_string(),
256 format!("System User {user_id}"),
257 UserPersona::AutomatedSystem,
258 )
259 }
260
261 pub fn can_post_to_company(&self, company_code: &str) -> bool {
263 self.company_codes.is_empty() || self.company_codes.iter().any(|c| c == company_code)
264 }
265
266 pub fn generate_username(persona: UserPersona, index: usize) -> String {
268 match persona {
269 UserPersona::JuniorAccountant => format!("JACC{index:04}"),
270 UserPersona::SeniorAccountant => format!("SACC{index:04}"),
271 UserPersona::Controller => format!("CTRL{index:04}"),
272 UserPersona::Manager => format!("MGR{index:04}"),
273 UserPersona::Executive => format!("EXEC{index:04}"),
274 UserPersona::AutomatedSystem => format!("BATCH{index:04}"),
275 UserPersona::ExternalAuditor => format!("AUDIT{index:04}"),
276 UserPersona::FraudActor => format!("USER{index:04}"), }
278 }
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct UserPool {
284 pub users: Vec<User>,
286 #[serde(skip)]
288 persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
289}
290
291impl UserPool {
292 pub fn new() -> Self {
294 Self {
295 users: Vec::new(),
296 persona_index: std::collections::HashMap::new(),
297 }
298 }
299
300 pub fn from_employees(employees: &[Employee]) -> Self {
309 let mut pool = Self::new();
310 for emp in employees {
311 let display_name = if emp.first_name.is_empty() && emp.last_name.is_empty() {
312 emp.user_id.clone()
313 } else {
314 format!("{} {}", emp.first_name, emp.last_name)
315 };
316 let mut user = User::new(emp.user_id.clone(), display_name, emp.persona);
317 user.email = Some(emp.email.clone());
318 user.department = emp.department_id.clone();
319 user.cost_centers = emp.cost_center.iter().cloned().collect();
320 user.working_hours = emp.working_hours.clone();
321 pool.add_user(user);
322 }
323 pool
324 }
325
326 pub fn add_user(&mut self, user: User) {
328 let idx = self.users.len();
329 let persona = user.persona;
330 self.users.push(user);
331 self.persona_index.entry(persona).or_default().push(idx);
332 }
333
334 pub fn get_users_by_persona(&self, persona: UserPersona) -> Vec<&User> {
336 self.persona_index
337 .get(&persona)
338 .map(|indices| indices.iter().map(|&i| &self.users[i]).collect())
339 .unwrap_or_default()
340 }
341
342 pub fn get_random_user(&self, persona: UserPersona, rng: &mut impl rand::Rng) -> Option<&User> {
344 use rand::seq::IndexedRandom;
345 self.get_users_by_persona(persona).choose(rng).copied()
346 }
347
348 pub fn rebuild_index(&mut self) {
350 self.persona_index.clear();
351 for (idx, user) in self.users.iter().enumerate() {
352 self.persona_index
353 .entry(user.persona)
354 .or_default()
355 .push(idx);
356 }
357 }
358
359 pub fn generate_standard(company_codes: &[String]) -> Self {
361 let mut pool = Self::new();
362
363 for i in 0..10 {
365 let mut user = User::new(
366 User::generate_username(UserPersona::JuniorAccountant, i),
367 format!("Junior Accountant {}", i + 1),
368 UserPersona::JuniorAccountant,
369 );
370 user.company_codes = company_codes.to_vec();
371 pool.add_user(user);
372 }
373
374 for i in 0..5 {
376 let mut user = User::new(
377 User::generate_username(UserPersona::SeniorAccountant, i),
378 format!("Senior Accountant {}", i + 1),
379 UserPersona::SeniorAccountant,
380 );
381 user.company_codes = company_codes.to_vec();
382 pool.add_user(user);
383 }
384
385 for i in 0..2 {
387 let mut user = User::new(
388 User::generate_username(UserPersona::Controller, i),
389 format!("Controller {}", i + 1),
390 UserPersona::Controller,
391 );
392 user.company_codes = company_codes.to_vec();
393 pool.add_user(user);
394 }
395
396 for i in 0..3 {
398 let mut user = User::new(
399 User::generate_username(UserPersona::Manager, i),
400 format!("Finance Manager {}", i + 1),
401 UserPersona::Manager,
402 );
403 user.company_codes = company_codes.to_vec();
404 pool.add_user(user);
405 }
406
407 for i in 0..20 {
409 let mut user = User::new(
410 User::generate_username(UserPersona::AutomatedSystem, i),
411 format!("Batch Job {}", i + 1),
412 UserPersona::AutomatedSystem,
413 );
414 user.company_codes = company_codes.to_vec();
415 pool.add_user(user);
416 }
417
418 pool
419 }
420}
421
422impl Default for UserPool {
423 fn default() -> Self {
424 Self::new()
425 }
426}
427
428#[derive(
430 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
431)]
432#[serde(rename_all = "snake_case")]
433pub enum JobLevel {
434 #[default]
436 Staff,
437 Senior,
439 Lead,
441 Supervisor,
443 Manager,
445 Director,
447 VicePresident,
449 Executive,
451}
452
453impl JobLevel {
454 pub fn management_level(&self) -> u8 {
456 match self {
457 Self::Staff => 0,
458 Self::Senior => 0,
459 Self::Lead => 1,
460 Self::Supervisor => 2,
461 Self::Manager => 3,
462 Self::Director => 4,
463 Self::VicePresident => 5,
464 Self::Executive => 6,
465 }
466 }
467
468 pub fn is_manager(&self) -> bool {
470 self.management_level() >= 2
471 }
472
473 pub fn typical_direct_reports(&self) -> (u16, u16) {
475 match self {
476 Self::Staff | Self::Senior => (0, 0),
477 Self::Lead => (0, 3),
478 Self::Supervisor => (3, 10),
479 Self::Manager => (5, 15),
480 Self::Director => (3, 8),
481 Self::VicePresident => (3, 6),
482 Self::Executive => (5, 12),
483 }
484 }
485}
486
487#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
489#[serde(rename_all = "snake_case")]
490pub enum EmployeeStatus {
491 #[default]
493 Active,
494 OnLeave,
496 Suspended,
498 NoticePeriod,
500 Terminated,
502 Retired,
504 Contractor,
506}
507
508impl EmployeeStatus {
509 pub fn can_transact(&self) -> bool {
511 matches!(self, Self::Active | Self::Contractor)
512 }
513
514 pub fn is_active(&self) -> bool {
516 !matches!(self, Self::Terminated | Self::Retired)
517 }
518}
519
520#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
522#[serde(rename_all = "snake_case")]
523pub enum SystemRole {
524 Viewer,
526 Creator,
528 Approver,
530 PaymentReleaser,
532 BankProcessor,
534 JournalPoster,
536 PeriodClose,
538 Admin,
540 ApAccountant,
542 ArAccountant,
544 Buyer,
546 Executive,
548 FinancialAnalyst,
550 GeneralAccountant,
552 Custom(String),
554}
555
556#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct TransactionCodeAuth {
559 pub tcode: String,
561 pub activity: ActivityType,
563 pub active: bool,
565}
566
567#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
569#[serde(rename_all = "snake_case")]
570pub enum ActivityType {
571 #[default]
573 Display,
574 Create,
576 Change,
578 Delete,
580 Execute,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct Employee {
587 pub employee_id: String,
589
590 pub user_id: String,
592
593 pub display_name: String,
595
596 pub first_name: String,
598
599 pub last_name: String,
601
602 pub email: String,
604
605 pub persona: UserPersona,
607
608 pub job_level: JobLevel,
610
611 pub job_title: String,
613
614 pub department_id: Option<String>,
616
617 pub cost_center: Option<String>,
619
620 pub manager_id: Option<String>,
622
623 pub direct_reports: Vec<String>,
625
626 pub status: EmployeeStatus,
628
629 pub company_code: String,
631
632 pub working_hours: WorkingHoursPattern,
634
635 pub authorized_company_codes: Vec<String>,
637
638 pub authorized_cost_centers: Vec<String>,
640
641 pub approval_limit: Decimal,
643
644 pub can_approve_pr: bool,
646
647 pub can_approve_po: bool,
649
650 pub can_approve_invoice: bool,
652
653 pub can_approve_je: bool,
655
656 pub can_release_payment: bool,
658
659 pub system_roles: Vec<SystemRole>,
661
662 pub transaction_codes: Vec<TransactionCodeAuth>,
664
665 pub hire_date: Option<chrono::NaiveDate>,
667
668 pub termination_date: Option<chrono::NaiveDate>,
670
671 pub location: Option<String>,
673
674 pub is_shared_services: bool,
676
677 pub phone: Option<String>,
679
680 #[serde(with = "crate::serde_decimal")]
685 pub base_salary: rust_decimal::Decimal,
686}
687
688impl Employee {
689 pub fn new(
691 employee_id: impl Into<String>,
692 user_id: impl Into<String>,
693 first_name: impl Into<String>,
694 last_name: impl Into<String>,
695 company_code: impl Into<String>,
696 ) -> Self {
697 let first = first_name.into();
698 let last = last_name.into();
699 let uid = user_id.into();
700 let display_name = format!("{first} {last}");
701 let email = format!(
702 "{}.{}@company.com",
703 first.to_lowercase(),
704 last.to_lowercase()
705 );
706
707 Self {
708 employee_id: employee_id.into(),
709 user_id: uid,
710 display_name,
711 first_name: first,
712 last_name: last,
713 email,
714 persona: UserPersona::JuniorAccountant,
715 job_level: JobLevel::Staff,
716 job_title: "Staff Accountant".to_string(),
717 department_id: None,
718 cost_center: None,
719 manager_id: None,
720 direct_reports: Vec::new(),
721 status: EmployeeStatus::Active,
722 company_code: company_code.into(),
723 working_hours: WorkingHoursPattern::default(),
724 authorized_company_codes: Vec::new(),
725 authorized_cost_centers: Vec::new(),
726 approval_limit: Decimal::ZERO,
727 can_approve_pr: false,
728 can_approve_po: false,
729 can_approve_invoice: false,
730 can_approve_je: false,
731 can_release_payment: false,
732 system_roles: Vec::new(),
733 transaction_codes: Vec::new(),
734 hire_date: None,
735 termination_date: None,
736 location: None,
737 is_shared_services: false,
738 phone: None,
739 base_salary: Decimal::ZERO,
740 }
741 }
742
743 pub fn with_persona(mut self, persona: UserPersona) -> Self {
745 self.persona = persona;
746
747 match persona {
749 UserPersona::JuniorAccountant => {
750 self.job_level = JobLevel::Staff;
751 self.job_title = "Junior Accountant".to_string();
752 self.approval_limit = Decimal::from(1000);
753 }
754 UserPersona::SeniorAccountant => {
755 self.job_level = JobLevel::Senior;
756 self.job_title = "Senior Accountant".to_string();
757 self.approval_limit = Decimal::from(10000);
758 self.can_approve_je = true;
759 }
760 UserPersona::Controller => {
761 self.job_level = JobLevel::Manager;
762 self.job_title = "Controller".to_string();
763 self.approval_limit = Decimal::from(100000);
764 self.can_approve_pr = true;
765 self.can_approve_po = true;
766 self.can_approve_invoice = true;
767 self.can_approve_je = true;
768 }
769 UserPersona::Manager => {
770 self.job_level = JobLevel::Director;
771 self.job_title = "Finance Director".to_string();
772 self.approval_limit = Decimal::from(500000);
773 self.can_approve_pr = true;
774 self.can_approve_po = true;
775 self.can_approve_invoice = true;
776 self.can_approve_je = true;
777 self.can_release_payment = true;
778 }
779 UserPersona::Executive => {
780 self.job_level = JobLevel::Executive;
781 self.job_title = "CFO".to_string();
782 self.approval_limit = Decimal::from(999_999_999); self.can_approve_pr = true;
784 self.can_approve_po = true;
785 self.can_approve_invoice = true;
786 self.can_approve_je = true;
787 self.can_release_payment = true;
788 }
789 UserPersona::AutomatedSystem => {
790 self.job_level = JobLevel::Staff;
791 self.job_title = "Batch Process".to_string();
792 self.working_hours = WorkingHoursPattern::batch_processing();
793 }
794 UserPersona::ExternalAuditor => {
795 self.job_level = JobLevel::Senior;
796 self.job_title = "External Auditor".to_string();
797 self.approval_limit = Decimal::ZERO;
798 }
799 UserPersona::FraudActor => {
800 self.job_level = JobLevel::Staff;
801 self.job_title = "Staff Accountant".to_string();
802 self.approval_limit = Decimal::from(10000);
803 }
804 }
805 self
806 }
807
808 pub fn with_job_level(mut self, level: JobLevel) -> Self {
810 self.job_level = level;
811 self
812 }
813
814 pub fn with_job_title(mut self, title: impl Into<String>) -> Self {
816 self.job_title = title.into();
817 self
818 }
819
820 pub fn with_manager(mut self, manager_id: impl Into<String>) -> Self {
822 self.manager_id = Some(manager_id.into());
823 self
824 }
825
826 pub fn with_department(mut self, department_id: impl Into<String>) -> Self {
828 self.department_id = Some(department_id.into());
829 self
830 }
831
832 pub fn with_cost_center(mut self, cost_center: impl Into<String>) -> Self {
834 self.cost_center = Some(cost_center.into());
835 self
836 }
837
838 pub fn with_approval_limit(mut self, limit: Decimal) -> Self {
840 self.approval_limit = limit;
841 self
842 }
843
844 pub fn with_authorized_company(mut self, company_code: impl Into<String>) -> Self {
846 self.authorized_company_codes.push(company_code.into());
847 self
848 }
849
850 pub fn with_role(mut self, role: SystemRole) -> Self {
852 self.system_roles.push(role);
853 self
854 }
855
856 pub fn with_hire_date(mut self, date: chrono::NaiveDate) -> Self {
858 self.hire_date = Some(date);
859 self
860 }
861
862 pub fn add_direct_report(&mut self, employee_id: String) {
864 if !self.direct_reports.contains(&employee_id) {
865 self.direct_reports.push(employee_id);
866 }
867 }
868
869 pub fn can_approve_amount(&self, amount: Decimal) -> bool {
871 if self.status != EmployeeStatus::Active {
872 return false;
873 }
874 amount <= self.approval_limit
875 }
876
877 pub fn can_approve_in_company(&self, company_code: &str) -> bool {
879 if self.status != EmployeeStatus::Active {
880 return false;
881 }
882 self.authorized_company_codes.is_empty()
883 || self
884 .authorized_company_codes
885 .iter()
886 .any(|c| c == company_code)
887 }
888
889 pub fn has_role(&self, role: &SystemRole) -> bool {
891 self.system_roles.contains(role)
892 }
893
894 pub fn hierarchy_depth(&self) -> u8 {
896 match self.job_level {
899 JobLevel::Executive => 0,
900 JobLevel::VicePresident => 1,
901 JobLevel::Director => 2,
902 JobLevel::Manager => 3,
903 JobLevel::Supervisor => 4,
904 JobLevel::Lead => 5,
905 JobLevel::Senior => 6,
906 JobLevel::Staff => 7,
907 }
908 }
909
910 pub fn to_user(&self) -> User {
912 let mut user = User::new(
913 self.user_id.clone(),
914 self.display_name.clone(),
915 self.persona,
916 );
917 user.email = Some(self.email.clone());
918 user.department = self.department_id.clone();
919 user.working_hours = self.working_hours.clone();
920 user.company_codes = self.authorized_company_codes.clone();
921 user.cost_centers = self.authorized_cost_centers.clone();
922 user.is_active = self.status.can_transact();
923 user.start_date = self.hire_date;
924 user.end_date = self.termination_date;
925 user
926 }
927}
928
929#[derive(Debug, Clone, Default, Serialize, Deserialize)]
931pub struct EmployeePool {
932 pub employees: Vec<Employee>,
934 #[serde(skip)]
936 id_index: std::collections::HashMap<String, usize>,
937 #[serde(skip)]
939 manager_index: std::collections::HashMap<String, Vec<usize>>,
940 #[serde(skip)]
942 persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
943 #[serde(skip)]
945 department_index: std::collections::HashMap<String, Vec<usize>>,
946}
947
948impl EmployeePool {
949 pub fn new() -> Self {
951 Self::default()
952 }
953
954 pub fn add_employee(&mut self, employee: Employee) {
956 let idx = self.employees.len();
957
958 self.id_index.insert(employee.employee_id.clone(), idx);
959
960 if let Some(ref mgr_id) = employee.manager_id {
961 self.manager_index
962 .entry(mgr_id.clone())
963 .or_default()
964 .push(idx);
965 }
966
967 self.persona_index
968 .entry(employee.persona)
969 .or_default()
970 .push(idx);
971
972 if let Some(ref dept_id) = employee.department_id {
973 self.department_index
974 .entry(dept_id.clone())
975 .or_default()
976 .push(idx);
977 }
978
979 self.employees.push(employee);
980 }
981
982 pub fn get_by_id(&self, employee_id: &str) -> Option<&Employee> {
984 self.id_index
985 .get(employee_id)
986 .map(|&idx| &self.employees[idx])
987 }
988
989 pub fn get_by_id_mut(&mut self, employee_id: &str) -> Option<&mut Employee> {
991 self.id_index
992 .get(employee_id)
993 .copied()
994 .map(|idx| &mut self.employees[idx])
995 }
996
997 pub fn get_direct_reports(&self, manager_id: &str) -> Vec<&Employee> {
999 self.manager_index
1000 .get(manager_id)
1001 .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
1002 .unwrap_or_default()
1003 }
1004
1005 pub fn get_by_persona(&self, persona: UserPersona) -> Vec<&Employee> {
1007 self.persona_index
1008 .get(&persona)
1009 .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
1010 .unwrap_or_default()
1011 }
1012
1013 pub fn get_by_department(&self, department_id: &str) -> Vec<&Employee> {
1015 self.department_index
1016 .get(department_id)
1017 .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
1018 .unwrap_or_default()
1019 }
1020
1021 pub fn get_random_approver(&self, rng: &mut impl rand::Rng) -> Option<&Employee> {
1023 use rand::seq::IndexedRandom;
1024
1025 let approvers: Vec<_> = self
1026 .employees
1027 .iter()
1028 .filter(|e| e.persona.has_approval_authority() && e.status == EmployeeStatus::Active)
1029 .collect();
1030
1031 approvers.choose(rng).copied()
1032 }
1033
1034 pub fn get_approver_for_amount(
1036 &self,
1037 amount: Decimal,
1038 rng: &mut impl rand::Rng,
1039 ) -> Option<&Employee> {
1040 use rand::seq::IndexedRandom;
1041
1042 let approvers: Vec<_> = self
1043 .employees
1044 .iter()
1045 .filter(|e| e.can_approve_amount(amount))
1046 .collect();
1047
1048 approvers.choose(rng).copied()
1049 }
1050
1051 pub fn get_managers(&self) -> Vec<&Employee> {
1053 self.employees
1054 .iter()
1055 .filter(|e| !e.direct_reports.is_empty() || e.job_level.is_manager())
1056 .collect()
1057 }
1058
1059 pub fn get_reporting_chain(&self, employee_id: &str) -> Vec<&Employee> {
1061 let mut chain = Vec::new();
1062 let mut current_id = employee_id.to_string();
1063
1064 while let Some(emp) = self.get_by_id(¤t_id) {
1065 chain.push(emp);
1066 if let Some(ref mgr_id) = emp.manager_id {
1067 current_id = mgr_id.clone();
1068 } else {
1069 break;
1070 }
1071 }
1072
1073 chain
1074 }
1075
1076 pub fn rebuild_indices(&mut self) {
1078 self.id_index.clear();
1079 self.manager_index.clear();
1080 self.persona_index.clear();
1081 self.department_index.clear();
1082
1083 for (idx, employee) in self.employees.iter().enumerate() {
1084 self.id_index.insert(employee.employee_id.clone(), idx);
1085
1086 if let Some(ref mgr_id) = employee.manager_id {
1087 self.manager_index
1088 .entry(mgr_id.clone())
1089 .or_default()
1090 .push(idx);
1091 }
1092
1093 self.persona_index
1094 .entry(employee.persona)
1095 .or_default()
1096 .push(idx);
1097
1098 if let Some(ref dept_id) = employee.department_id {
1099 self.department_index
1100 .entry(dept_id.clone())
1101 .or_default()
1102 .push(idx);
1103 }
1104 }
1105 }
1106
1107 pub fn len(&self) -> usize {
1109 self.employees.len()
1110 }
1111
1112 pub fn is_empty(&self) -> bool {
1114 self.employees.is_empty()
1115 }
1116}
1117
1118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1120#[serde(rename_all = "snake_case")]
1121pub enum EmployeeEventType {
1122 #[default]
1124 Hired,
1125 Promoted,
1127 SalaryAdjustment,
1129 Transfer,
1131 Terminated,
1133}
1134
1135impl std::fmt::Display for EmployeeEventType {
1136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1137 match self {
1138 Self::Hired => write!(f, "hired"),
1139 Self::Promoted => write!(f, "promoted"),
1140 Self::SalaryAdjustment => write!(f, "salary_adjustment"),
1141 Self::Transfer => write!(f, "transfer"),
1142 Self::Terminated => write!(f, "terminated"),
1143 }
1144 }
1145}
1146
1147#[derive(Debug, Clone, Serialize, Deserialize)]
1155pub struct EmployeeChangeEvent {
1156 pub employee_id: String,
1158
1159 pub event_date: chrono::NaiveDate,
1161
1162 pub event_type: EmployeeEventType,
1164
1165 pub old_value: Option<String>,
1167
1168 pub new_value: Option<String>,
1170
1171 pub effective_date: chrono::NaiveDate,
1174}
1175
1176impl EmployeeChangeEvent {
1177 pub fn hired(employee_id: impl Into<String>, hire_date: chrono::NaiveDate) -> Self {
1179 Self {
1180 employee_id: employee_id.into(),
1181 event_date: hire_date,
1182 event_type: EmployeeEventType::Hired,
1183 old_value: None,
1184 new_value: Some("active".to_string()),
1185 effective_date: hire_date,
1186 }
1187 }
1188}
1189
1190#[cfg(test)]
1191mod tests {
1192 use super::*;
1193
1194 #[test]
1195 fn test_persona_properties() {
1196 assert!(UserPersona::JuniorAccountant.is_human());
1197 assert!(!UserPersona::AutomatedSystem.is_human());
1198 assert!(UserPersona::Controller.has_approval_authority());
1199 assert!(!UserPersona::JuniorAccountant.has_approval_authority());
1200 }
1201
1202 #[test]
1203 fn test_persona_display_snake_case() {
1204 assert_eq!(
1205 UserPersona::JuniorAccountant.to_string(),
1206 "junior_accountant"
1207 );
1208 assert_eq!(
1209 UserPersona::SeniorAccountant.to_string(),
1210 "senior_accountant"
1211 );
1212 assert_eq!(UserPersona::Controller.to_string(), "controller");
1213 assert_eq!(UserPersona::Manager.to_string(), "manager");
1214 assert_eq!(UserPersona::Executive.to_string(), "executive");
1215 assert_eq!(UserPersona::AutomatedSystem.to_string(), "automated_system");
1216 assert_eq!(UserPersona::ExternalAuditor.to_string(), "external_auditor");
1217 assert_eq!(UserPersona::FraudActor.to_string(), "fraud_actor");
1218
1219 for persona in [
1221 UserPersona::JuniorAccountant,
1222 UserPersona::SeniorAccountant,
1223 UserPersona::Controller,
1224 UserPersona::Manager,
1225 UserPersona::Executive,
1226 UserPersona::AutomatedSystem,
1227 UserPersona::ExternalAuditor,
1228 UserPersona::FraudActor,
1229 ] {
1230 let s = persona.to_string();
1231 assert!(
1232 !s.contains(char::is_uppercase),
1233 "Display output '{}' should be all lowercase snake_case",
1234 s
1235 );
1236 }
1237 }
1238
1239 #[test]
1240 fn test_user_pool() {
1241 let pool = UserPool::generate_standard(&["1000".to_string()]);
1242 assert!(!pool.users.is_empty());
1243 assert!(!pool
1244 .get_users_by_persona(UserPersona::JuniorAccountant)
1245 .is_empty());
1246 }
1247
1248 #[test]
1249 fn test_job_level_hierarchy() {
1250 assert!(JobLevel::Executive.management_level() > JobLevel::Manager.management_level());
1251 assert!(JobLevel::Manager.is_manager());
1252 assert!(!JobLevel::Staff.is_manager());
1253 }
1254
1255 #[test]
1256 fn test_employee_creation() {
1257 let employee = Employee::new("E-001", "jsmith", "John", "Smith", "1000")
1258 .with_persona(UserPersona::Controller);
1259
1260 assert_eq!(employee.employee_id, "E-001");
1261 assert_eq!(employee.display_name, "John Smith");
1262 assert!(employee.can_approve_je);
1263 assert_eq!(employee.job_level, JobLevel::Manager);
1264 }
1265
1266 #[test]
1267 fn test_employee_approval_limits() {
1268 let employee = Employee::new("E-001", "test", "Test", "User", "1000")
1269 .with_approval_limit(Decimal::from(10000));
1270
1271 assert!(employee.can_approve_amount(Decimal::from(5000)));
1272 assert!(!employee.can_approve_amount(Decimal::from(15000)));
1273 }
1274
1275 #[test]
1276 fn test_employee_pool_hierarchy() {
1277 let mut pool = EmployeePool::new();
1278
1279 let cfo = Employee::new("E-001", "cfo", "Jane", "CEO", "1000")
1280 .with_persona(UserPersona::Executive);
1281
1282 let controller = Employee::new("E-002", "ctrl", "Bob", "Controller", "1000")
1283 .with_persona(UserPersona::Controller)
1284 .with_manager("E-001");
1285
1286 let accountant = Employee::new("E-003", "acc", "Alice", "Accountant", "1000")
1287 .with_persona(UserPersona::JuniorAccountant)
1288 .with_manager("E-002");
1289
1290 pool.add_employee(cfo);
1291 pool.add_employee(controller);
1292 pool.add_employee(accountant);
1293
1294 let direct_reports = pool.get_direct_reports("E-001");
1296 assert_eq!(direct_reports.len(), 1);
1297 assert_eq!(direct_reports[0].employee_id, "E-002");
1298
1299 let chain = pool.get_reporting_chain("E-003");
1301 assert_eq!(chain.len(), 3);
1302 assert_eq!(chain[0].employee_id, "E-003");
1303 assert_eq!(chain[1].employee_id, "E-002");
1304 assert_eq!(chain[2].employee_id, "E-001");
1305 }
1306
1307 #[test]
1308 fn test_employee_to_user() {
1309 let employee = Employee::new("E-001", "jdoe", "John", "Doe", "1000")
1310 .with_persona(UserPersona::SeniorAccountant);
1311
1312 let user = employee.to_user();
1313
1314 assert_eq!(user.user_id, "jdoe");
1315 assert_eq!(user.persona, UserPersona::SeniorAccountant);
1316 assert!(user.is_active);
1317 }
1318
1319 #[test]
1320 fn test_employee_status() {
1321 assert!(EmployeeStatus::Active.can_transact());
1322 assert!(EmployeeStatus::Contractor.can_transact());
1323 assert!(!EmployeeStatus::Terminated.can_transact());
1324 assert!(!EmployeeStatus::OnLeave.can_transact());
1325 }
1326}