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 add_user(&mut self, user: User) {
302 let idx = self.users.len();
303 let persona = user.persona;
304 self.users.push(user);
305 self.persona_index.entry(persona).or_default().push(idx);
306 }
307
308 pub fn get_users_by_persona(&self, persona: UserPersona) -> Vec<&User> {
310 self.persona_index
311 .get(&persona)
312 .map(|indices| indices.iter().map(|&i| &self.users[i]).collect())
313 .unwrap_or_default()
314 }
315
316 pub fn get_random_user(&self, persona: UserPersona, rng: &mut impl rand::Rng) -> Option<&User> {
318 use rand::seq::IndexedRandom;
319 self.get_users_by_persona(persona).choose(rng).copied()
320 }
321
322 pub fn rebuild_index(&mut self) {
324 self.persona_index.clear();
325 for (idx, user) in self.users.iter().enumerate() {
326 self.persona_index
327 .entry(user.persona)
328 .or_default()
329 .push(idx);
330 }
331 }
332
333 pub fn generate_standard(company_codes: &[String]) -> Self {
335 let mut pool = Self::new();
336
337 for i in 0..10 {
339 let mut user = User::new(
340 User::generate_username(UserPersona::JuniorAccountant, i),
341 format!("Junior Accountant {}", i + 1),
342 UserPersona::JuniorAccountant,
343 );
344 user.company_codes = company_codes.to_vec();
345 pool.add_user(user);
346 }
347
348 for i in 0..5 {
350 let mut user = User::new(
351 User::generate_username(UserPersona::SeniorAccountant, i),
352 format!("Senior Accountant {}", i + 1),
353 UserPersona::SeniorAccountant,
354 );
355 user.company_codes = company_codes.to_vec();
356 pool.add_user(user);
357 }
358
359 for i in 0..2 {
361 let mut user = User::new(
362 User::generate_username(UserPersona::Controller, i),
363 format!("Controller {}", i + 1),
364 UserPersona::Controller,
365 );
366 user.company_codes = company_codes.to_vec();
367 pool.add_user(user);
368 }
369
370 for i in 0..3 {
372 let mut user = User::new(
373 User::generate_username(UserPersona::Manager, i),
374 format!("Finance Manager {}", i + 1),
375 UserPersona::Manager,
376 );
377 user.company_codes = company_codes.to_vec();
378 pool.add_user(user);
379 }
380
381 for i in 0..20 {
383 let mut user = User::new(
384 User::generate_username(UserPersona::AutomatedSystem, i),
385 format!("Batch Job {}", i + 1),
386 UserPersona::AutomatedSystem,
387 );
388 user.company_codes = company_codes.to_vec();
389 pool.add_user(user);
390 }
391
392 pool
393 }
394}
395
396impl Default for UserPool {
397 fn default() -> Self {
398 Self::new()
399 }
400}
401
402#[derive(
404 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
405)]
406#[serde(rename_all = "snake_case")]
407pub enum JobLevel {
408 #[default]
410 Staff,
411 Senior,
413 Lead,
415 Supervisor,
417 Manager,
419 Director,
421 VicePresident,
423 Executive,
425}
426
427impl JobLevel {
428 pub fn management_level(&self) -> u8 {
430 match self {
431 Self::Staff => 0,
432 Self::Senior => 0,
433 Self::Lead => 1,
434 Self::Supervisor => 2,
435 Self::Manager => 3,
436 Self::Director => 4,
437 Self::VicePresident => 5,
438 Self::Executive => 6,
439 }
440 }
441
442 pub fn is_manager(&self) -> bool {
444 self.management_level() >= 2
445 }
446
447 pub fn typical_direct_reports(&self) -> (u16, u16) {
449 match self {
450 Self::Staff | Self::Senior => (0, 0),
451 Self::Lead => (0, 3),
452 Self::Supervisor => (3, 10),
453 Self::Manager => (5, 15),
454 Self::Director => (3, 8),
455 Self::VicePresident => (3, 6),
456 Self::Executive => (5, 12),
457 }
458 }
459}
460
461#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
463#[serde(rename_all = "snake_case")]
464pub enum EmployeeStatus {
465 #[default]
467 Active,
468 OnLeave,
470 Suspended,
472 NoticePeriod,
474 Terminated,
476 Retired,
478 Contractor,
480}
481
482impl EmployeeStatus {
483 pub fn can_transact(&self) -> bool {
485 matches!(self, Self::Active | Self::Contractor)
486 }
487
488 pub fn is_active(&self) -> bool {
490 !matches!(self, Self::Terminated | Self::Retired)
491 }
492}
493
494#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
496#[serde(rename_all = "snake_case")]
497pub enum SystemRole {
498 Viewer,
500 Creator,
502 Approver,
504 PaymentReleaser,
506 BankProcessor,
508 JournalPoster,
510 PeriodClose,
512 Admin,
514 ApAccountant,
516 ArAccountant,
518 Buyer,
520 Executive,
522 FinancialAnalyst,
524 GeneralAccountant,
526 Custom(String),
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct TransactionCodeAuth {
533 pub tcode: String,
535 pub activity: ActivityType,
537 pub active: bool,
539}
540
541#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
543#[serde(rename_all = "snake_case")]
544pub enum ActivityType {
545 #[default]
547 Display,
548 Create,
550 Change,
552 Delete,
554 Execute,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct Employee {
561 pub employee_id: String,
563
564 pub user_id: String,
566
567 pub display_name: String,
569
570 pub first_name: String,
572
573 pub last_name: String,
575
576 pub email: String,
578
579 pub persona: UserPersona,
581
582 pub job_level: JobLevel,
584
585 pub job_title: String,
587
588 pub department_id: Option<String>,
590
591 pub cost_center: Option<String>,
593
594 pub manager_id: Option<String>,
596
597 pub direct_reports: Vec<String>,
599
600 pub status: EmployeeStatus,
602
603 pub company_code: String,
605
606 pub working_hours: WorkingHoursPattern,
608
609 pub authorized_company_codes: Vec<String>,
611
612 pub authorized_cost_centers: Vec<String>,
614
615 pub approval_limit: Decimal,
617
618 pub can_approve_pr: bool,
620
621 pub can_approve_po: bool,
623
624 pub can_approve_invoice: bool,
626
627 pub can_approve_je: bool,
629
630 pub can_release_payment: bool,
632
633 pub system_roles: Vec<SystemRole>,
635
636 pub transaction_codes: Vec<TransactionCodeAuth>,
638
639 pub hire_date: Option<chrono::NaiveDate>,
641
642 pub termination_date: Option<chrono::NaiveDate>,
644
645 pub location: Option<String>,
647
648 pub is_shared_services: bool,
650
651 pub phone: Option<String>,
653
654 #[serde(with = "rust_decimal::serde::str")]
659 pub base_salary: rust_decimal::Decimal,
660}
661
662impl Employee {
663 pub fn new(
665 employee_id: impl Into<String>,
666 user_id: impl Into<String>,
667 first_name: impl Into<String>,
668 last_name: impl Into<String>,
669 company_code: impl Into<String>,
670 ) -> Self {
671 let first = first_name.into();
672 let last = last_name.into();
673 let uid = user_id.into();
674 let display_name = format!("{first} {last}");
675 let email = format!(
676 "{}.{}@company.com",
677 first.to_lowercase(),
678 last.to_lowercase()
679 );
680
681 Self {
682 employee_id: employee_id.into(),
683 user_id: uid,
684 display_name,
685 first_name: first,
686 last_name: last,
687 email,
688 persona: UserPersona::JuniorAccountant,
689 job_level: JobLevel::Staff,
690 job_title: "Staff Accountant".to_string(),
691 department_id: None,
692 cost_center: None,
693 manager_id: None,
694 direct_reports: Vec::new(),
695 status: EmployeeStatus::Active,
696 company_code: company_code.into(),
697 working_hours: WorkingHoursPattern::default(),
698 authorized_company_codes: Vec::new(),
699 authorized_cost_centers: Vec::new(),
700 approval_limit: Decimal::ZERO,
701 can_approve_pr: false,
702 can_approve_po: false,
703 can_approve_invoice: false,
704 can_approve_je: false,
705 can_release_payment: false,
706 system_roles: Vec::new(),
707 transaction_codes: Vec::new(),
708 hire_date: None,
709 termination_date: None,
710 location: None,
711 is_shared_services: false,
712 phone: None,
713 base_salary: Decimal::ZERO,
714 }
715 }
716
717 pub fn with_persona(mut self, persona: UserPersona) -> Self {
719 self.persona = persona;
720
721 match persona {
723 UserPersona::JuniorAccountant => {
724 self.job_level = JobLevel::Staff;
725 self.job_title = "Junior Accountant".to_string();
726 self.approval_limit = Decimal::from(1000);
727 }
728 UserPersona::SeniorAccountant => {
729 self.job_level = JobLevel::Senior;
730 self.job_title = "Senior Accountant".to_string();
731 self.approval_limit = Decimal::from(10000);
732 self.can_approve_je = true;
733 }
734 UserPersona::Controller => {
735 self.job_level = JobLevel::Manager;
736 self.job_title = "Controller".to_string();
737 self.approval_limit = Decimal::from(100000);
738 self.can_approve_pr = true;
739 self.can_approve_po = true;
740 self.can_approve_invoice = true;
741 self.can_approve_je = true;
742 }
743 UserPersona::Manager => {
744 self.job_level = JobLevel::Director;
745 self.job_title = "Finance Director".to_string();
746 self.approval_limit = Decimal::from(500000);
747 self.can_approve_pr = true;
748 self.can_approve_po = true;
749 self.can_approve_invoice = true;
750 self.can_approve_je = true;
751 self.can_release_payment = true;
752 }
753 UserPersona::Executive => {
754 self.job_level = JobLevel::Executive;
755 self.job_title = "CFO".to_string();
756 self.approval_limit = Decimal::from(999_999_999); self.can_approve_pr = true;
758 self.can_approve_po = true;
759 self.can_approve_invoice = true;
760 self.can_approve_je = true;
761 self.can_release_payment = true;
762 }
763 UserPersona::AutomatedSystem => {
764 self.job_level = JobLevel::Staff;
765 self.job_title = "Batch Process".to_string();
766 self.working_hours = WorkingHoursPattern::batch_processing();
767 }
768 UserPersona::ExternalAuditor => {
769 self.job_level = JobLevel::Senior;
770 self.job_title = "External Auditor".to_string();
771 self.approval_limit = Decimal::ZERO;
772 }
773 UserPersona::FraudActor => {
774 self.job_level = JobLevel::Staff;
775 self.job_title = "Staff Accountant".to_string();
776 self.approval_limit = Decimal::from(10000);
777 }
778 }
779 self
780 }
781
782 pub fn with_job_level(mut self, level: JobLevel) -> Self {
784 self.job_level = level;
785 self
786 }
787
788 pub fn with_job_title(mut self, title: impl Into<String>) -> Self {
790 self.job_title = title.into();
791 self
792 }
793
794 pub fn with_manager(mut self, manager_id: impl Into<String>) -> Self {
796 self.manager_id = Some(manager_id.into());
797 self
798 }
799
800 pub fn with_department(mut self, department_id: impl Into<String>) -> Self {
802 self.department_id = Some(department_id.into());
803 self
804 }
805
806 pub fn with_cost_center(mut self, cost_center: impl Into<String>) -> Self {
808 self.cost_center = Some(cost_center.into());
809 self
810 }
811
812 pub fn with_approval_limit(mut self, limit: Decimal) -> Self {
814 self.approval_limit = limit;
815 self
816 }
817
818 pub fn with_authorized_company(mut self, company_code: impl Into<String>) -> Self {
820 self.authorized_company_codes.push(company_code.into());
821 self
822 }
823
824 pub fn with_role(mut self, role: SystemRole) -> Self {
826 self.system_roles.push(role);
827 self
828 }
829
830 pub fn with_hire_date(mut self, date: chrono::NaiveDate) -> Self {
832 self.hire_date = Some(date);
833 self
834 }
835
836 pub fn add_direct_report(&mut self, employee_id: String) {
838 if !self.direct_reports.contains(&employee_id) {
839 self.direct_reports.push(employee_id);
840 }
841 }
842
843 pub fn can_approve_amount(&self, amount: Decimal) -> bool {
845 if self.status != EmployeeStatus::Active {
846 return false;
847 }
848 amount <= self.approval_limit
849 }
850
851 pub fn can_approve_in_company(&self, company_code: &str) -> bool {
853 if self.status != EmployeeStatus::Active {
854 return false;
855 }
856 self.authorized_company_codes.is_empty()
857 || self
858 .authorized_company_codes
859 .iter()
860 .any(|c| c == company_code)
861 }
862
863 pub fn has_role(&self, role: &SystemRole) -> bool {
865 self.system_roles.contains(role)
866 }
867
868 pub fn hierarchy_depth(&self) -> u8 {
870 match self.job_level {
873 JobLevel::Executive => 0,
874 JobLevel::VicePresident => 1,
875 JobLevel::Director => 2,
876 JobLevel::Manager => 3,
877 JobLevel::Supervisor => 4,
878 JobLevel::Lead => 5,
879 JobLevel::Senior => 6,
880 JobLevel::Staff => 7,
881 }
882 }
883
884 pub fn to_user(&self) -> User {
886 let mut user = User::new(
887 self.user_id.clone(),
888 self.display_name.clone(),
889 self.persona,
890 );
891 user.email = Some(self.email.clone());
892 user.department = self.department_id.clone();
893 user.working_hours = self.working_hours.clone();
894 user.company_codes = self.authorized_company_codes.clone();
895 user.cost_centers = self.authorized_cost_centers.clone();
896 user.is_active = self.status.can_transact();
897 user.start_date = self.hire_date;
898 user.end_date = self.termination_date;
899 user
900 }
901}
902
903#[derive(Debug, Clone, Default, Serialize, Deserialize)]
905pub struct EmployeePool {
906 pub employees: Vec<Employee>,
908 #[serde(skip)]
910 id_index: std::collections::HashMap<String, usize>,
911 #[serde(skip)]
913 manager_index: std::collections::HashMap<String, Vec<usize>>,
914 #[serde(skip)]
916 persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
917 #[serde(skip)]
919 department_index: std::collections::HashMap<String, Vec<usize>>,
920}
921
922impl EmployeePool {
923 pub fn new() -> Self {
925 Self::default()
926 }
927
928 pub fn add_employee(&mut self, employee: Employee) {
930 let idx = self.employees.len();
931
932 self.id_index.insert(employee.employee_id.clone(), idx);
933
934 if let Some(ref mgr_id) = employee.manager_id {
935 self.manager_index
936 .entry(mgr_id.clone())
937 .or_default()
938 .push(idx);
939 }
940
941 self.persona_index
942 .entry(employee.persona)
943 .or_default()
944 .push(idx);
945
946 if let Some(ref dept_id) = employee.department_id {
947 self.department_index
948 .entry(dept_id.clone())
949 .or_default()
950 .push(idx);
951 }
952
953 self.employees.push(employee);
954 }
955
956 pub fn get_by_id(&self, employee_id: &str) -> Option<&Employee> {
958 self.id_index
959 .get(employee_id)
960 .map(|&idx| &self.employees[idx])
961 }
962
963 pub fn get_by_id_mut(&mut self, employee_id: &str) -> Option<&mut Employee> {
965 self.id_index
966 .get(employee_id)
967 .copied()
968 .map(|idx| &mut self.employees[idx])
969 }
970
971 pub fn get_direct_reports(&self, manager_id: &str) -> Vec<&Employee> {
973 self.manager_index
974 .get(manager_id)
975 .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
976 .unwrap_or_default()
977 }
978
979 pub fn get_by_persona(&self, persona: UserPersona) -> Vec<&Employee> {
981 self.persona_index
982 .get(&persona)
983 .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
984 .unwrap_or_default()
985 }
986
987 pub fn get_by_department(&self, department_id: &str) -> Vec<&Employee> {
989 self.department_index
990 .get(department_id)
991 .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
992 .unwrap_or_default()
993 }
994
995 pub fn get_random_approver(&self, rng: &mut impl rand::Rng) -> Option<&Employee> {
997 use rand::seq::IndexedRandom;
998
999 let approvers: Vec<_> = self
1000 .employees
1001 .iter()
1002 .filter(|e| e.persona.has_approval_authority() && e.status == EmployeeStatus::Active)
1003 .collect();
1004
1005 approvers.choose(rng).copied()
1006 }
1007
1008 pub fn get_approver_for_amount(
1010 &self,
1011 amount: Decimal,
1012 rng: &mut impl rand::Rng,
1013 ) -> Option<&Employee> {
1014 use rand::seq::IndexedRandom;
1015
1016 let approvers: Vec<_> = self
1017 .employees
1018 .iter()
1019 .filter(|e| e.can_approve_amount(amount))
1020 .collect();
1021
1022 approvers.choose(rng).copied()
1023 }
1024
1025 pub fn get_managers(&self) -> Vec<&Employee> {
1027 self.employees
1028 .iter()
1029 .filter(|e| !e.direct_reports.is_empty() || e.job_level.is_manager())
1030 .collect()
1031 }
1032
1033 pub fn get_reporting_chain(&self, employee_id: &str) -> Vec<&Employee> {
1035 let mut chain = Vec::new();
1036 let mut current_id = employee_id.to_string();
1037
1038 while let Some(emp) = self.get_by_id(¤t_id) {
1039 chain.push(emp);
1040 if let Some(ref mgr_id) = emp.manager_id {
1041 current_id = mgr_id.clone();
1042 } else {
1043 break;
1044 }
1045 }
1046
1047 chain
1048 }
1049
1050 pub fn rebuild_indices(&mut self) {
1052 self.id_index.clear();
1053 self.manager_index.clear();
1054 self.persona_index.clear();
1055 self.department_index.clear();
1056
1057 for (idx, employee) in self.employees.iter().enumerate() {
1058 self.id_index.insert(employee.employee_id.clone(), idx);
1059
1060 if let Some(ref mgr_id) = employee.manager_id {
1061 self.manager_index
1062 .entry(mgr_id.clone())
1063 .or_default()
1064 .push(idx);
1065 }
1066
1067 self.persona_index
1068 .entry(employee.persona)
1069 .or_default()
1070 .push(idx);
1071
1072 if let Some(ref dept_id) = employee.department_id {
1073 self.department_index
1074 .entry(dept_id.clone())
1075 .or_default()
1076 .push(idx);
1077 }
1078 }
1079 }
1080
1081 pub fn len(&self) -> usize {
1083 self.employees.len()
1084 }
1085
1086 pub fn is_empty(&self) -> bool {
1088 self.employees.is_empty()
1089 }
1090}
1091
1092#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1094#[serde(rename_all = "snake_case")]
1095pub enum EmployeeEventType {
1096 #[default]
1098 Hired,
1099 Promoted,
1101 SalaryAdjustment,
1103 Transfer,
1105 Terminated,
1107}
1108
1109impl std::fmt::Display for EmployeeEventType {
1110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1111 match self {
1112 Self::Hired => write!(f, "hired"),
1113 Self::Promoted => write!(f, "promoted"),
1114 Self::SalaryAdjustment => write!(f, "salary_adjustment"),
1115 Self::Transfer => write!(f, "transfer"),
1116 Self::Terminated => write!(f, "terminated"),
1117 }
1118 }
1119}
1120
1121#[derive(Debug, Clone, Serialize, Deserialize)]
1129pub struct EmployeeChangeEvent {
1130 pub employee_id: String,
1132
1133 pub event_date: chrono::NaiveDate,
1135
1136 pub event_type: EmployeeEventType,
1138
1139 pub old_value: Option<String>,
1141
1142 pub new_value: Option<String>,
1144
1145 pub effective_date: chrono::NaiveDate,
1148}
1149
1150impl EmployeeChangeEvent {
1151 pub fn hired(employee_id: impl Into<String>, hire_date: chrono::NaiveDate) -> Self {
1153 Self {
1154 employee_id: employee_id.into(),
1155 event_date: hire_date,
1156 event_type: EmployeeEventType::Hired,
1157 old_value: None,
1158 new_value: Some("active".to_string()),
1159 effective_date: hire_date,
1160 }
1161 }
1162}
1163
1164#[cfg(test)]
1165#[allow(clippy::unwrap_used)]
1166mod tests {
1167 use super::*;
1168
1169 #[test]
1170 fn test_persona_properties() {
1171 assert!(UserPersona::JuniorAccountant.is_human());
1172 assert!(!UserPersona::AutomatedSystem.is_human());
1173 assert!(UserPersona::Controller.has_approval_authority());
1174 assert!(!UserPersona::JuniorAccountant.has_approval_authority());
1175 }
1176
1177 #[test]
1178 fn test_persona_display_snake_case() {
1179 assert_eq!(
1180 UserPersona::JuniorAccountant.to_string(),
1181 "junior_accountant"
1182 );
1183 assert_eq!(
1184 UserPersona::SeniorAccountant.to_string(),
1185 "senior_accountant"
1186 );
1187 assert_eq!(UserPersona::Controller.to_string(), "controller");
1188 assert_eq!(UserPersona::Manager.to_string(), "manager");
1189 assert_eq!(UserPersona::Executive.to_string(), "executive");
1190 assert_eq!(UserPersona::AutomatedSystem.to_string(), "automated_system");
1191 assert_eq!(UserPersona::ExternalAuditor.to_string(), "external_auditor");
1192 assert_eq!(UserPersona::FraudActor.to_string(), "fraud_actor");
1193
1194 for persona in [
1196 UserPersona::JuniorAccountant,
1197 UserPersona::SeniorAccountant,
1198 UserPersona::Controller,
1199 UserPersona::Manager,
1200 UserPersona::Executive,
1201 UserPersona::AutomatedSystem,
1202 UserPersona::ExternalAuditor,
1203 UserPersona::FraudActor,
1204 ] {
1205 let s = persona.to_string();
1206 assert!(
1207 !s.contains(char::is_uppercase),
1208 "Display output '{}' should be all lowercase snake_case",
1209 s
1210 );
1211 }
1212 }
1213
1214 #[test]
1215 fn test_user_pool() {
1216 let pool = UserPool::generate_standard(&["1000".to_string()]);
1217 assert!(!pool.users.is_empty());
1218 assert!(!pool
1219 .get_users_by_persona(UserPersona::JuniorAccountant)
1220 .is_empty());
1221 }
1222
1223 #[test]
1224 fn test_job_level_hierarchy() {
1225 assert!(JobLevel::Executive.management_level() > JobLevel::Manager.management_level());
1226 assert!(JobLevel::Manager.is_manager());
1227 assert!(!JobLevel::Staff.is_manager());
1228 }
1229
1230 #[test]
1231 fn test_employee_creation() {
1232 let employee = Employee::new("E-001", "jsmith", "John", "Smith", "1000")
1233 .with_persona(UserPersona::Controller);
1234
1235 assert_eq!(employee.employee_id, "E-001");
1236 assert_eq!(employee.display_name, "John Smith");
1237 assert!(employee.can_approve_je);
1238 assert_eq!(employee.job_level, JobLevel::Manager);
1239 }
1240
1241 #[test]
1242 fn test_employee_approval_limits() {
1243 let employee = Employee::new("E-001", "test", "Test", "User", "1000")
1244 .with_approval_limit(Decimal::from(10000));
1245
1246 assert!(employee.can_approve_amount(Decimal::from(5000)));
1247 assert!(!employee.can_approve_amount(Decimal::from(15000)));
1248 }
1249
1250 #[test]
1251 fn test_employee_pool_hierarchy() {
1252 let mut pool = EmployeePool::new();
1253
1254 let cfo = Employee::new("E-001", "cfo", "Jane", "CEO", "1000")
1255 .with_persona(UserPersona::Executive);
1256
1257 let controller = Employee::new("E-002", "ctrl", "Bob", "Controller", "1000")
1258 .with_persona(UserPersona::Controller)
1259 .with_manager("E-001");
1260
1261 let accountant = Employee::new("E-003", "acc", "Alice", "Accountant", "1000")
1262 .with_persona(UserPersona::JuniorAccountant)
1263 .with_manager("E-002");
1264
1265 pool.add_employee(cfo);
1266 pool.add_employee(controller);
1267 pool.add_employee(accountant);
1268
1269 let direct_reports = pool.get_direct_reports("E-001");
1271 assert_eq!(direct_reports.len(), 1);
1272 assert_eq!(direct_reports[0].employee_id, "E-002");
1273
1274 let chain = pool.get_reporting_chain("E-003");
1276 assert_eq!(chain.len(), 3);
1277 assert_eq!(chain[0].employee_id, "E-003");
1278 assert_eq!(chain[1].employee_id, "E-002");
1279 assert_eq!(chain[2].employee_id, "E-001");
1280 }
1281
1282 #[test]
1283 fn test_employee_to_user() {
1284 let employee = Employee::new("E-001", "jdoe", "John", "Doe", "1000")
1285 .with_persona(UserPersona::SeniorAccountant);
1286
1287 let user = employee.to_user();
1288
1289 assert_eq!(user.user_id, "jdoe");
1290 assert_eq!(user.persona, UserPersona::SeniorAccountant);
1291 assert!(user.is_active);
1292 }
1293
1294 #[test]
1295 fn test_employee_status() {
1296 assert!(EmployeeStatus::Active.can_transact());
1297 assert!(EmployeeStatus::Contractor.can_transact());
1298 assert!(!EmployeeStatus::Terminated.can_transact());
1299 assert!(!EmployeeStatus::OnLeave.can_transact());
1300 }
1301}