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{:04}", index),
270 UserPersona::SeniorAccountant => format!("SACC{:04}", index),
271 UserPersona::Controller => format!("CTRL{:04}", index),
272 UserPersona::Manager => format!("MGR{:04}", index),
273 UserPersona::Executive => format!("EXEC{:04}", index),
274 UserPersona::AutomatedSystem => format!("BATCH{:04}", index),
275 UserPersona::ExternalAuditor => format!("AUDIT{:04}", index),
276 UserPersona::FraudActor => format!("USER{:04}", index), }
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
655impl Employee {
656 pub fn new(
658 employee_id: impl Into<String>,
659 user_id: impl Into<String>,
660 first_name: impl Into<String>,
661 last_name: impl Into<String>,
662 company_code: impl Into<String>,
663 ) -> Self {
664 let first = first_name.into();
665 let last = last_name.into();
666 let uid = user_id.into();
667 let display_name = format!("{} {}", first, last);
668 let email = format!(
669 "{}.{}@company.com",
670 first.to_lowercase(),
671 last.to_lowercase()
672 );
673
674 Self {
675 employee_id: employee_id.into(),
676 user_id: uid,
677 display_name,
678 first_name: first,
679 last_name: last,
680 email,
681 persona: UserPersona::JuniorAccountant,
682 job_level: JobLevel::Staff,
683 job_title: "Staff Accountant".to_string(),
684 department_id: None,
685 cost_center: None,
686 manager_id: None,
687 direct_reports: Vec::new(),
688 status: EmployeeStatus::Active,
689 company_code: company_code.into(),
690 working_hours: WorkingHoursPattern::default(),
691 authorized_company_codes: Vec::new(),
692 authorized_cost_centers: Vec::new(),
693 approval_limit: Decimal::ZERO,
694 can_approve_pr: false,
695 can_approve_po: false,
696 can_approve_invoice: false,
697 can_approve_je: false,
698 can_release_payment: false,
699 system_roles: Vec::new(),
700 transaction_codes: Vec::new(),
701 hire_date: None,
702 termination_date: None,
703 location: None,
704 is_shared_services: false,
705 phone: None,
706 }
707 }
708
709 pub fn with_persona(mut self, persona: UserPersona) -> Self {
711 self.persona = persona;
712
713 match persona {
715 UserPersona::JuniorAccountant => {
716 self.job_level = JobLevel::Staff;
717 self.job_title = "Junior Accountant".to_string();
718 self.approval_limit = Decimal::from(1000);
719 }
720 UserPersona::SeniorAccountant => {
721 self.job_level = JobLevel::Senior;
722 self.job_title = "Senior Accountant".to_string();
723 self.approval_limit = Decimal::from(10000);
724 self.can_approve_je = true;
725 }
726 UserPersona::Controller => {
727 self.job_level = JobLevel::Manager;
728 self.job_title = "Controller".to_string();
729 self.approval_limit = Decimal::from(100000);
730 self.can_approve_pr = true;
731 self.can_approve_po = true;
732 self.can_approve_invoice = true;
733 self.can_approve_je = true;
734 }
735 UserPersona::Manager => {
736 self.job_level = JobLevel::Director;
737 self.job_title = "Finance Director".to_string();
738 self.approval_limit = Decimal::from(500000);
739 self.can_approve_pr = true;
740 self.can_approve_po = true;
741 self.can_approve_invoice = true;
742 self.can_approve_je = true;
743 self.can_release_payment = true;
744 }
745 UserPersona::Executive => {
746 self.job_level = JobLevel::Executive;
747 self.job_title = "CFO".to_string();
748 self.approval_limit = Decimal::from(999_999_999); self.can_approve_pr = true;
750 self.can_approve_po = true;
751 self.can_approve_invoice = true;
752 self.can_approve_je = true;
753 self.can_release_payment = true;
754 }
755 UserPersona::AutomatedSystem => {
756 self.job_level = JobLevel::Staff;
757 self.job_title = "Batch Process".to_string();
758 self.working_hours = WorkingHoursPattern::batch_processing();
759 }
760 UserPersona::ExternalAuditor => {
761 self.job_level = JobLevel::Senior;
762 self.job_title = "External Auditor".to_string();
763 self.approval_limit = Decimal::ZERO;
764 }
765 UserPersona::FraudActor => {
766 self.job_level = JobLevel::Staff;
767 self.job_title = "Staff Accountant".to_string();
768 self.approval_limit = Decimal::from(10000);
769 }
770 }
771 self
772 }
773
774 pub fn with_job_level(mut self, level: JobLevel) -> Self {
776 self.job_level = level;
777 self
778 }
779
780 pub fn with_job_title(mut self, title: impl Into<String>) -> Self {
782 self.job_title = title.into();
783 self
784 }
785
786 pub fn with_manager(mut self, manager_id: impl Into<String>) -> Self {
788 self.manager_id = Some(manager_id.into());
789 self
790 }
791
792 pub fn with_department(mut self, department_id: impl Into<String>) -> Self {
794 self.department_id = Some(department_id.into());
795 self
796 }
797
798 pub fn with_cost_center(mut self, cost_center: impl Into<String>) -> Self {
800 self.cost_center = Some(cost_center.into());
801 self
802 }
803
804 pub fn with_approval_limit(mut self, limit: Decimal) -> Self {
806 self.approval_limit = limit;
807 self
808 }
809
810 pub fn with_authorized_company(mut self, company_code: impl Into<String>) -> Self {
812 self.authorized_company_codes.push(company_code.into());
813 self
814 }
815
816 pub fn with_role(mut self, role: SystemRole) -> Self {
818 self.system_roles.push(role);
819 self
820 }
821
822 pub fn with_hire_date(mut self, date: chrono::NaiveDate) -> Self {
824 self.hire_date = Some(date);
825 self
826 }
827
828 pub fn add_direct_report(&mut self, employee_id: String) {
830 if !self.direct_reports.contains(&employee_id) {
831 self.direct_reports.push(employee_id);
832 }
833 }
834
835 pub fn can_approve_amount(&self, amount: Decimal) -> bool {
837 if self.status != EmployeeStatus::Active {
838 return false;
839 }
840 amount <= self.approval_limit
841 }
842
843 pub fn can_approve_in_company(&self, company_code: &str) -> bool {
845 if self.status != EmployeeStatus::Active {
846 return false;
847 }
848 self.authorized_company_codes.is_empty()
849 || self
850 .authorized_company_codes
851 .iter()
852 .any(|c| c == company_code)
853 }
854
855 pub fn has_role(&self, role: &SystemRole) -> bool {
857 self.system_roles.contains(role)
858 }
859
860 pub fn hierarchy_depth(&self) -> u8 {
862 match self.job_level {
865 JobLevel::Executive => 0,
866 JobLevel::VicePresident => 1,
867 JobLevel::Director => 2,
868 JobLevel::Manager => 3,
869 JobLevel::Supervisor => 4,
870 JobLevel::Lead => 5,
871 JobLevel::Senior => 6,
872 JobLevel::Staff => 7,
873 }
874 }
875
876 pub fn to_user(&self) -> User {
878 let mut user = User::new(
879 self.user_id.clone(),
880 self.display_name.clone(),
881 self.persona,
882 );
883 user.email = Some(self.email.clone());
884 user.department = self.department_id.clone();
885 user.working_hours = self.working_hours.clone();
886 user.company_codes = self.authorized_company_codes.clone();
887 user.cost_centers = self.authorized_cost_centers.clone();
888 user.is_active = self.status.can_transact();
889 user.start_date = self.hire_date;
890 user.end_date = self.termination_date;
891 user
892 }
893}
894
895#[derive(Debug, Clone, Default, Serialize, Deserialize)]
897pub struct EmployeePool {
898 pub employees: Vec<Employee>,
900 #[serde(skip)]
902 id_index: std::collections::HashMap<String, usize>,
903 #[serde(skip)]
905 manager_index: std::collections::HashMap<String, Vec<usize>>,
906 #[serde(skip)]
908 persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
909 #[serde(skip)]
911 department_index: std::collections::HashMap<String, Vec<usize>>,
912}
913
914impl EmployeePool {
915 pub fn new() -> Self {
917 Self::default()
918 }
919
920 pub fn add_employee(&mut self, employee: Employee) {
922 let idx = self.employees.len();
923
924 self.id_index.insert(employee.employee_id.clone(), idx);
925
926 if let Some(ref mgr_id) = employee.manager_id {
927 self.manager_index
928 .entry(mgr_id.clone())
929 .or_default()
930 .push(idx);
931 }
932
933 self.persona_index
934 .entry(employee.persona)
935 .or_default()
936 .push(idx);
937
938 if let Some(ref dept_id) = employee.department_id {
939 self.department_index
940 .entry(dept_id.clone())
941 .or_default()
942 .push(idx);
943 }
944
945 self.employees.push(employee);
946 }
947
948 pub fn get_by_id(&self, employee_id: &str) -> Option<&Employee> {
950 self.id_index
951 .get(employee_id)
952 .map(|&idx| &self.employees[idx])
953 }
954
955 pub fn get_by_id_mut(&mut self, employee_id: &str) -> Option<&mut Employee> {
957 self.id_index
958 .get(employee_id)
959 .copied()
960 .map(|idx| &mut self.employees[idx])
961 }
962
963 pub fn get_direct_reports(&self, manager_id: &str) -> Vec<&Employee> {
965 self.manager_index
966 .get(manager_id)
967 .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
968 .unwrap_or_default()
969 }
970
971 pub fn get_by_persona(&self, persona: UserPersona) -> Vec<&Employee> {
973 self.persona_index
974 .get(&persona)
975 .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
976 .unwrap_or_default()
977 }
978
979 pub fn get_by_department(&self, department_id: &str) -> Vec<&Employee> {
981 self.department_index
982 .get(department_id)
983 .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
984 .unwrap_or_default()
985 }
986
987 pub fn get_random_approver(&self, rng: &mut impl rand::Rng) -> Option<&Employee> {
989 use rand::seq::IndexedRandom;
990
991 let approvers: Vec<_> = self
992 .employees
993 .iter()
994 .filter(|e| e.persona.has_approval_authority() && e.status == EmployeeStatus::Active)
995 .collect();
996
997 approvers.choose(rng).copied()
998 }
999
1000 pub fn get_approver_for_amount(
1002 &self,
1003 amount: Decimal,
1004 rng: &mut impl rand::Rng,
1005 ) -> Option<&Employee> {
1006 use rand::seq::IndexedRandom;
1007
1008 let approvers: Vec<_> = self
1009 .employees
1010 .iter()
1011 .filter(|e| e.can_approve_amount(amount))
1012 .collect();
1013
1014 approvers.choose(rng).copied()
1015 }
1016
1017 pub fn get_managers(&self) -> Vec<&Employee> {
1019 self.employees
1020 .iter()
1021 .filter(|e| !e.direct_reports.is_empty() || e.job_level.is_manager())
1022 .collect()
1023 }
1024
1025 pub fn get_reporting_chain(&self, employee_id: &str) -> Vec<&Employee> {
1027 let mut chain = Vec::new();
1028 let mut current_id = employee_id.to_string();
1029
1030 while let Some(emp) = self.get_by_id(¤t_id) {
1031 chain.push(emp);
1032 if let Some(ref mgr_id) = emp.manager_id {
1033 current_id = mgr_id.clone();
1034 } else {
1035 break;
1036 }
1037 }
1038
1039 chain
1040 }
1041
1042 pub fn rebuild_indices(&mut self) {
1044 self.id_index.clear();
1045 self.manager_index.clear();
1046 self.persona_index.clear();
1047 self.department_index.clear();
1048
1049 for (idx, employee) in self.employees.iter().enumerate() {
1050 self.id_index.insert(employee.employee_id.clone(), idx);
1051
1052 if let Some(ref mgr_id) = employee.manager_id {
1053 self.manager_index
1054 .entry(mgr_id.clone())
1055 .or_default()
1056 .push(idx);
1057 }
1058
1059 self.persona_index
1060 .entry(employee.persona)
1061 .or_default()
1062 .push(idx);
1063
1064 if let Some(ref dept_id) = employee.department_id {
1065 self.department_index
1066 .entry(dept_id.clone())
1067 .or_default()
1068 .push(idx);
1069 }
1070 }
1071 }
1072
1073 pub fn len(&self) -> usize {
1075 self.employees.len()
1076 }
1077
1078 pub fn is_empty(&self) -> bool {
1080 self.employees.is_empty()
1081 }
1082}
1083
1084#[cfg(test)]
1085#[allow(clippy::unwrap_used)]
1086mod tests {
1087 use super::*;
1088
1089 #[test]
1090 fn test_persona_properties() {
1091 assert!(UserPersona::JuniorAccountant.is_human());
1092 assert!(!UserPersona::AutomatedSystem.is_human());
1093 assert!(UserPersona::Controller.has_approval_authority());
1094 assert!(!UserPersona::JuniorAccountant.has_approval_authority());
1095 }
1096
1097 #[test]
1098 fn test_persona_display_snake_case() {
1099 assert_eq!(
1100 UserPersona::JuniorAccountant.to_string(),
1101 "junior_accountant"
1102 );
1103 assert_eq!(
1104 UserPersona::SeniorAccountant.to_string(),
1105 "senior_accountant"
1106 );
1107 assert_eq!(UserPersona::Controller.to_string(), "controller");
1108 assert_eq!(UserPersona::Manager.to_string(), "manager");
1109 assert_eq!(UserPersona::Executive.to_string(), "executive");
1110 assert_eq!(UserPersona::AutomatedSystem.to_string(), "automated_system");
1111 assert_eq!(UserPersona::ExternalAuditor.to_string(), "external_auditor");
1112 assert_eq!(UserPersona::FraudActor.to_string(), "fraud_actor");
1113
1114 for persona in [
1116 UserPersona::JuniorAccountant,
1117 UserPersona::SeniorAccountant,
1118 UserPersona::Controller,
1119 UserPersona::Manager,
1120 UserPersona::Executive,
1121 UserPersona::AutomatedSystem,
1122 UserPersona::ExternalAuditor,
1123 UserPersona::FraudActor,
1124 ] {
1125 let s = persona.to_string();
1126 assert!(
1127 !s.contains(char::is_uppercase),
1128 "Display output '{}' should be all lowercase snake_case",
1129 s
1130 );
1131 }
1132 }
1133
1134 #[test]
1135 fn test_user_pool() {
1136 let pool = UserPool::generate_standard(&["1000".to_string()]);
1137 assert!(!pool.users.is_empty());
1138 assert!(!pool
1139 .get_users_by_persona(UserPersona::JuniorAccountant)
1140 .is_empty());
1141 }
1142
1143 #[test]
1144 fn test_job_level_hierarchy() {
1145 assert!(JobLevel::Executive.management_level() > JobLevel::Manager.management_level());
1146 assert!(JobLevel::Manager.is_manager());
1147 assert!(!JobLevel::Staff.is_manager());
1148 }
1149
1150 #[test]
1151 fn test_employee_creation() {
1152 let employee = Employee::new("E-001", "jsmith", "John", "Smith", "1000")
1153 .with_persona(UserPersona::Controller);
1154
1155 assert_eq!(employee.employee_id, "E-001");
1156 assert_eq!(employee.display_name, "John Smith");
1157 assert!(employee.can_approve_je);
1158 assert_eq!(employee.job_level, JobLevel::Manager);
1159 }
1160
1161 #[test]
1162 fn test_employee_approval_limits() {
1163 let employee = Employee::new("E-001", "test", "Test", "User", "1000")
1164 .with_approval_limit(Decimal::from(10000));
1165
1166 assert!(employee.can_approve_amount(Decimal::from(5000)));
1167 assert!(!employee.can_approve_amount(Decimal::from(15000)));
1168 }
1169
1170 #[test]
1171 fn test_employee_pool_hierarchy() {
1172 let mut pool = EmployeePool::new();
1173
1174 let cfo = Employee::new("E-001", "cfo", "Jane", "CEO", "1000")
1175 .with_persona(UserPersona::Executive);
1176
1177 let controller = Employee::new("E-002", "ctrl", "Bob", "Controller", "1000")
1178 .with_persona(UserPersona::Controller)
1179 .with_manager("E-001");
1180
1181 let accountant = Employee::new("E-003", "acc", "Alice", "Accountant", "1000")
1182 .with_persona(UserPersona::JuniorAccountant)
1183 .with_manager("E-002");
1184
1185 pool.add_employee(cfo);
1186 pool.add_employee(controller);
1187 pool.add_employee(accountant);
1188
1189 let direct_reports = pool.get_direct_reports("E-001");
1191 assert_eq!(direct_reports.len(), 1);
1192 assert_eq!(direct_reports[0].employee_id, "E-002");
1193
1194 let chain = pool.get_reporting_chain("E-003");
1196 assert_eq!(chain.len(), 3);
1197 assert_eq!(chain[0].employee_id, "E-003");
1198 assert_eq!(chain[1].employee_id, "E-002");
1199 assert_eq!(chain[2].employee_id, "E-001");
1200 }
1201
1202 #[test]
1203 fn test_employee_to_user() {
1204 let employee = Employee::new("E-001", "jdoe", "John", "Doe", "1000")
1205 .with_persona(UserPersona::SeniorAccountant);
1206
1207 let user = employee.to_user();
1208
1209 assert_eq!(user.user_id, "jdoe");
1210 assert_eq!(user.persona, UserPersona::SeniorAccountant);
1211 assert!(user.is_active);
1212 }
1213
1214 #[test]
1215 fn test_employee_status() {
1216 assert!(EmployeeStatus::Active.can_transact());
1217 assert!(EmployeeStatus::Contractor.can_transact());
1218 assert!(!EmployeeStatus::Terminated.can_transact());
1219 assert!(!EmployeeStatus::OnLeave.can_transact());
1220 }
1221}