1use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
16#[serde(rename_all = "snake_case")]
17pub enum UserPersona {
18 JuniorAccountant,
20 SeniorAccountant,
22 Controller,
24 Manager,
26 Executive,
28 #[default]
30 AutomatedSystem,
31 ExternalAuditor,
33 FraudActor,
35}
36
37impl UserPersona {
38 pub fn is_human(&self) -> bool {
40 !matches!(self, Self::AutomatedSystem)
41 }
42
43 pub fn has_approval_authority(&self) -> bool {
45 matches!(self, Self::Controller | Self::Manager | Self::Executive)
46 }
47
48 pub fn error_rate(&self) -> f64 {
50 match self {
51 Self::JuniorAccountant => 0.02,
52 Self::SeniorAccountant => 0.005,
53 Self::Controller => 0.002,
54 Self::Manager => 0.003,
55 Self::Executive => 0.001,
56 Self::AutomatedSystem => 0.0001,
57 Self::ExternalAuditor => 0.0,
58 Self::FraudActor => 0.01,
59 }
60 }
61
62 pub fn typical_daily_volume(&self) -> (u32, u32) {
64 match self {
65 Self::JuniorAccountant => (20, 100),
66 Self::SeniorAccountant => (10, 50),
67 Self::Controller => (5, 20),
68 Self::Manager => (1, 10),
69 Self::Executive => (0, 5),
70 Self::AutomatedSystem => (100, 10000),
71 Self::ExternalAuditor => (0, 0),
72 Self::FraudActor => (1, 5),
73 }
74 }
75
76 pub fn approval_threshold(&self) -> Option<f64> {
78 match self {
79 Self::JuniorAccountant => Some(1000.0),
80 Self::SeniorAccountant => Some(10000.0),
81 Self::Controller => Some(100000.0),
82 Self::Manager => Some(500000.0),
83 Self::Executive => None, Self::AutomatedSystem => Some(1000000.0),
85 Self::ExternalAuditor => Some(0.0), Self::FraudActor => Some(10000.0),
87 }
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct WorkingHoursPattern {
94 pub start_hour: u8,
96 pub end_hour: u8,
98 pub peak_hours: Vec<u8>,
100 pub weekend_probability: f64,
102 pub after_hours_probability: f64,
104}
105
106impl Default for WorkingHoursPattern {
107 fn default() -> Self {
108 Self {
109 start_hour: 8,
110 end_hour: 18,
111 peak_hours: vec![10, 11, 14, 15],
112 weekend_probability: 0.05,
113 after_hours_probability: 0.10,
114 }
115 }
116}
117
118impl WorkingHoursPattern {
119 pub fn european() -> Self {
121 Self {
122 start_hour: 9,
123 end_hour: 17,
124 peak_hours: vec![10, 11, 14, 15],
125 weekend_probability: 0.02,
126 after_hours_probability: 0.05,
127 }
128 }
129
130 pub fn us_standard() -> Self {
132 Self {
133 start_hour: 8,
134 end_hour: 17,
135 peak_hours: vec![9, 10, 14, 15],
136 weekend_probability: 0.05,
137 after_hours_probability: 0.10,
138 }
139 }
140
141 pub fn asian() -> Self {
143 Self {
144 start_hour: 9,
145 end_hour: 18,
146 peak_hours: vec![10, 11, 15, 16],
147 weekend_probability: 0.10,
148 after_hours_probability: 0.15,
149 }
150 }
151
152 pub fn batch_processing() -> Self {
154 Self {
155 start_hour: 0,
156 end_hour: 24,
157 peak_hours: vec![2, 3, 4, 22, 23], weekend_probability: 1.0,
159 after_hours_probability: 1.0,
160 }
161 }
162
163 pub fn is_working_hour(&self, hour: u8) -> bool {
165 hour >= self.start_hour && hour < self.end_hour
166 }
167
168 pub fn is_peak_hour(&self, hour: u8) -> bool {
170 self.peak_hours.contains(&hour)
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct User {
177 pub user_id: String,
179
180 pub display_name: String,
182
183 pub email: Option<String>,
185
186 pub persona: UserPersona,
188
189 pub department: Option<String>,
191
192 pub working_hours: WorkingHoursPattern,
194
195 pub company_codes: Vec<String>,
197
198 pub cost_centers: Vec<String>,
200
201 pub is_active: bool,
203
204 pub start_date: Option<chrono::NaiveDate>,
206
207 pub end_date: Option<chrono::NaiveDate>,
209}
210
211impl User {
212 pub fn new(user_id: String, display_name: String, persona: UserPersona) -> Self {
214 let working_hours = if persona.is_human() {
215 WorkingHoursPattern::default()
216 } else {
217 WorkingHoursPattern::batch_processing()
218 };
219
220 Self {
221 user_id,
222 display_name,
223 email: None,
224 persona,
225 department: None,
226 working_hours,
227 company_codes: Vec::new(),
228 cost_centers: Vec::new(),
229 is_active: true,
230 start_date: None,
231 end_date: None,
232 }
233 }
234
235 pub fn system(user_id: &str) -> Self {
237 Self::new(
238 user_id.to_string(),
239 format!("System User {}", user_id),
240 UserPersona::AutomatedSystem,
241 )
242 }
243
244 pub fn can_post_to_company(&self, company_code: &str) -> bool {
246 self.company_codes.is_empty() || self.company_codes.iter().any(|c| c == company_code)
247 }
248
249 pub fn generate_username(persona: UserPersona, index: usize) -> String {
251 match persona {
252 UserPersona::JuniorAccountant => format!("JACC{:04}", index),
253 UserPersona::SeniorAccountant => format!("SACC{:04}", index),
254 UserPersona::Controller => format!("CTRL{:04}", index),
255 UserPersona::Manager => format!("MGR{:04}", index),
256 UserPersona::Executive => format!("EXEC{:04}", index),
257 UserPersona::AutomatedSystem => format!("BATCH{:04}", index),
258 UserPersona::ExternalAuditor => format!("AUDIT{:04}", index),
259 UserPersona::FraudActor => format!("USER{:04}", index), }
261 }
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct UserPool {
267 pub users: Vec<User>,
269 #[serde(skip)]
271 persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
272}
273
274impl UserPool {
275 pub fn new() -> Self {
277 Self {
278 users: Vec::new(),
279 persona_index: std::collections::HashMap::new(),
280 }
281 }
282
283 pub fn add_user(&mut self, user: User) {
285 let idx = self.users.len();
286 let persona = user.persona;
287 self.users.push(user);
288 self.persona_index.entry(persona).or_default().push(idx);
289 }
290
291 pub fn get_users_by_persona(&self, persona: UserPersona) -> Vec<&User> {
293 self.persona_index
294 .get(&persona)
295 .map(|indices| indices.iter().map(|&i| &self.users[i]).collect())
296 .unwrap_or_default()
297 }
298
299 pub fn get_random_user(&self, persona: UserPersona, rng: &mut impl rand::Rng) -> Option<&User> {
301 use rand::seq::SliceRandom;
302 self.get_users_by_persona(persona).choose(rng).copied()
303 }
304
305 pub fn rebuild_index(&mut self) {
307 self.persona_index.clear();
308 for (idx, user) in self.users.iter().enumerate() {
309 self.persona_index
310 .entry(user.persona)
311 .or_default()
312 .push(idx);
313 }
314 }
315
316 pub fn generate_standard(company_codes: &[String]) -> Self {
318 let mut pool = Self::new();
319
320 for i in 0..10 {
322 let mut user = User::new(
323 User::generate_username(UserPersona::JuniorAccountant, i),
324 format!("Junior Accountant {}", i + 1),
325 UserPersona::JuniorAccountant,
326 );
327 user.company_codes = company_codes.to_vec();
328 pool.add_user(user);
329 }
330
331 for i in 0..5 {
333 let mut user = User::new(
334 User::generate_username(UserPersona::SeniorAccountant, i),
335 format!("Senior Accountant {}", i + 1),
336 UserPersona::SeniorAccountant,
337 );
338 user.company_codes = company_codes.to_vec();
339 pool.add_user(user);
340 }
341
342 for i in 0..2 {
344 let mut user = User::new(
345 User::generate_username(UserPersona::Controller, i),
346 format!("Controller {}", i + 1),
347 UserPersona::Controller,
348 );
349 user.company_codes = company_codes.to_vec();
350 pool.add_user(user);
351 }
352
353 for i in 0..3 {
355 let mut user = User::new(
356 User::generate_username(UserPersona::Manager, i),
357 format!("Finance Manager {}", i + 1),
358 UserPersona::Manager,
359 );
360 user.company_codes = company_codes.to_vec();
361 pool.add_user(user);
362 }
363
364 for i in 0..20 {
366 let mut user = User::new(
367 User::generate_username(UserPersona::AutomatedSystem, i),
368 format!("Batch Job {}", i + 1),
369 UserPersona::AutomatedSystem,
370 );
371 user.company_codes = company_codes.to_vec();
372 pool.add_user(user);
373 }
374
375 pool
376 }
377}
378
379impl Default for UserPool {
380 fn default() -> Self {
381 Self::new()
382 }
383}
384
385#[derive(
387 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
388)]
389#[serde(rename_all = "snake_case")]
390pub enum JobLevel {
391 #[default]
393 Staff,
394 Senior,
396 Lead,
398 Supervisor,
400 Manager,
402 Director,
404 VicePresident,
406 Executive,
408}
409
410impl JobLevel {
411 pub fn management_level(&self) -> u8 {
413 match self {
414 Self::Staff => 0,
415 Self::Senior => 0,
416 Self::Lead => 1,
417 Self::Supervisor => 2,
418 Self::Manager => 3,
419 Self::Director => 4,
420 Self::VicePresident => 5,
421 Self::Executive => 6,
422 }
423 }
424
425 pub fn is_manager(&self) -> bool {
427 self.management_level() >= 2
428 }
429
430 pub fn typical_direct_reports(&self) -> (u16, u16) {
432 match self {
433 Self::Staff | Self::Senior => (0, 0),
434 Self::Lead => (0, 3),
435 Self::Supervisor => (3, 10),
436 Self::Manager => (5, 15),
437 Self::Director => (3, 8),
438 Self::VicePresident => (3, 6),
439 Self::Executive => (5, 12),
440 }
441 }
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
446#[serde(rename_all = "snake_case")]
447pub enum EmployeeStatus {
448 #[default]
450 Active,
451 OnLeave,
453 Suspended,
455 NoticePeriod,
457 Terminated,
459 Retired,
461 Contractor,
463}
464
465impl EmployeeStatus {
466 pub fn can_transact(&self) -> bool {
468 matches!(self, Self::Active | Self::Contractor)
469 }
470
471 pub fn is_active(&self) -> bool {
473 !matches!(self, Self::Terminated | Self::Retired)
474 }
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
479#[serde(rename_all = "snake_case")]
480pub enum SystemRole {
481 Viewer,
483 Creator,
485 Approver,
487 PaymentReleaser,
489 BankProcessor,
491 JournalPoster,
493 PeriodClose,
495 Admin,
497 ApAccountant,
499 ArAccountant,
501 Buyer,
503 Executive,
505 FinancialAnalyst,
507 GeneralAccountant,
509 Custom(String),
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct TransactionCodeAuth {
516 pub tcode: String,
518 pub activity: ActivityType,
520 pub active: bool,
522}
523
524#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
526#[serde(rename_all = "snake_case")]
527pub enum ActivityType {
528 #[default]
530 Display,
531 Create,
533 Change,
535 Delete,
537 Execute,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct Employee {
544 pub employee_id: String,
546
547 pub user_id: String,
549
550 pub display_name: String,
552
553 pub first_name: String,
555
556 pub last_name: String,
558
559 pub email: String,
561
562 pub persona: UserPersona,
564
565 pub job_level: JobLevel,
567
568 pub job_title: String,
570
571 pub department_id: Option<String>,
573
574 pub cost_center: Option<String>,
576
577 pub manager_id: Option<String>,
579
580 pub direct_reports: Vec<String>,
582
583 pub status: EmployeeStatus,
585
586 pub company_code: String,
588
589 pub working_hours: WorkingHoursPattern,
591
592 pub authorized_company_codes: Vec<String>,
594
595 pub authorized_cost_centers: Vec<String>,
597
598 pub approval_limit: Decimal,
600
601 pub can_approve_pr: bool,
603
604 pub can_approve_po: bool,
606
607 pub can_approve_invoice: bool,
609
610 pub can_approve_je: bool,
612
613 pub can_release_payment: bool,
615
616 pub system_roles: Vec<SystemRole>,
618
619 pub transaction_codes: Vec<TransactionCodeAuth>,
621
622 pub hire_date: Option<chrono::NaiveDate>,
624
625 pub termination_date: Option<chrono::NaiveDate>,
627
628 pub location: Option<String>,
630
631 pub is_shared_services: bool,
633
634 pub phone: Option<String>,
636}
637
638impl Employee {
639 pub fn new(
641 employee_id: impl Into<String>,
642 user_id: impl Into<String>,
643 first_name: impl Into<String>,
644 last_name: impl Into<String>,
645 company_code: impl Into<String>,
646 ) -> Self {
647 let first = first_name.into();
648 let last = last_name.into();
649 let uid = user_id.into();
650 let display_name = format!("{} {}", first, last);
651 let email = format!(
652 "{}.{}@company.com",
653 first.to_lowercase(),
654 last.to_lowercase()
655 );
656
657 Self {
658 employee_id: employee_id.into(),
659 user_id: uid,
660 display_name,
661 first_name: first,
662 last_name: last,
663 email,
664 persona: UserPersona::JuniorAccountant,
665 job_level: JobLevel::Staff,
666 job_title: "Staff Accountant".to_string(),
667 department_id: None,
668 cost_center: None,
669 manager_id: None,
670 direct_reports: Vec::new(),
671 status: EmployeeStatus::Active,
672 company_code: company_code.into(),
673 working_hours: WorkingHoursPattern::default(),
674 authorized_company_codes: Vec::new(),
675 authorized_cost_centers: Vec::new(),
676 approval_limit: Decimal::ZERO,
677 can_approve_pr: false,
678 can_approve_po: false,
679 can_approve_invoice: false,
680 can_approve_je: false,
681 can_release_payment: false,
682 system_roles: Vec::new(),
683 transaction_codes: Vec::new(),
684 hire_date: None,
685 termination_date: None,
686 location: None,
687 is_shared_services: false,
688 phone: None,
689 }
690 }
691
692 pub fn with_persona(mut self, persona: UserPersona) -> Self {
694 self.persona = persona;
695
696 match persona {
698 UserPersona::JuniorAccountant => {
699 self.job_level = JobLevel::Staff;
700 self.job_title = "Junior Accountant".to_string();
701 self.approval_limit = Decimal::from(1000);
702 }
703 UserPersona::SeniorAccountant => {
704 self.job_level = JobLevel::Senior;
705 self.job_title = "Senior Accountant".to_string();
706 self.approval_limit = Decimal::from(10000);
707 self.can_approve_je = true;
708 }
709 UserPersona::Controller => {
710 self.job_level = JobLevel::Manager;
711 self.job_title = "Controller".to_string();
712 self.approval_limit = Decimal::from(100000);
713 self.can_approve_pr = true;
714 self.can_approve_po = true;
715 self.can_approve_invoice = true;
716 self.can_approve_je = true;
717 }
718 UserPersona::Manager => {
719 self.job_level = JobLevel::Director;
720 self.job_title = "Finance Director".to_string();
721 self.approval_limit = Decimal::from(500000);
722 self.can_approve_pr = true;
723 self.can_approve_po = true;
724 self.can_approve_invoice = true;
725 self.can_approve_je = true;
726 self.can_release_payment = true;
727 }
728 UserPersona::Executive => {
729 self.job_level = JobLevel::Executive;
730 self.job_title = "CFO".to_string();
731 self.approval_limit = Decimal::from(999_999_999); self.can_approve_pr = true;
733 self.can_approve_po = true;
734 self.can_approve_invoice = true;
735 self.can_approve_je = true;
736 self.can_release_payment = true;
737 }
738 UserPersona::AutomatedSystem => {
739 self.job_level = JobLevel::Staff;
740 self.job_title = "Batch Process".to_string();
741 self.working_hours = WorkingHoursPattern::batch_processing();
742 }
743 UserPersona::ExternalAuditor => {
744 self.job_level = JobLevel::Senior;
745 self.job_title = "External Auditor".to_string();
746 self.approval_limit = Decimal::ZERO;
747 }
748 UserPersona::FraudActor => {
749 self.job_level = JobLevel::Staff;
750 self.job_title = "Staff Accountant".to_string();
751 self.approval_limit = Decimal::from(10000);
752 }
753 }
754 self
755 }
756
757 pub fn with_job_level(mut self, level: JobLevel) -> Self {
759 self.job_level = level;
760 self
761 }
762
763 pub fn with_job_title(mut self, title: impl Into<String>) -> Self {
765 self.job_title = title.into();
766 self
767 }
768
769 pub fn with_manager(mut self, manager_id: impl Into<String>) -> Self {
771 self.manager_id = Some(manager_id.into());
772 self
773 }
774
775 pub fn with_department(mut self, department_id: impl Into<String>) -> Self {
777 self.department_id = Some(department_id.into());
778 self
779 }
780
781 pub fn with_cost_center(mut self, cost_center: impl Into<String>) -> Self {
783 self.cost_center = Some(cost_center.into());
784 self
785 }
786
787 pub fn with_approval_limit(mut self, limit: Decimal) -> Self {
789 self.approval_limit = limit;
790 self
791 }
792
793 pub fn with_authorized_company(mut self, company_code: impl Into<String>) -> Self {
795 self.authorized_company_codes.push(company_code.into());
796 self
797 }
798
799 pub fn with_role(mut self, role: SystemRole) -> Self {
801 self.system_roles.push(role);
802 self
803 }
804
805 pub fn with_hire_date(mut self, date: chrono::NaiveDate) -> Self {
807 self.hire_date = Some(date);
808 self
809 }
810
811 pub fn add_direct_report(&mut self, employee_id: String) {
813 if !self.direct_reports.contains(&employee_id) {
814 self.direct_reports.push(employee_id);
815 }
816 }
817
818 pub fn can_approve_amount(&self, amount: Decimal) -> bool {
820 if self.status != EmployeeStatus::Active {
821 return false;
822 }
823 amount <= self.approval_limit
824 }
825
826 pub fn can_approve_in_company(&self, company_code: &str) -> bool {
828 if self.status != EmployeeStatus::Active {
829 return false;
830 }
831 self.authorized_company_codes.is_empty()
832 || self
833 .authorized_company_codes
834 .iter()
835 .any(|c| c == company_code)
836 }
837
838 pub fn has_role(&self, role: &SystemRole) -> bool {
840 self.system_roles.contains(role)
841 }
842
843 pub fn hierarchy_depth(&self) -> u8 {
845 match self.job_level {
848 JobLevel::Executive => 0,
849 JobLevel::VicePresident => 1,
850 JobLevel::Director => 2,
851 JobLevel::Manager => 3,
852 JobLevel::Supervisor => 4,
853 JobLevel::Lead => 5,
854 JobLevel::Senior => 6,
855 JobLevel::Staff => 7,
856 }
857 }
858
859 pub fn to_user(&self) -> User {
861 let mut user = User::new(
862 self.user_id.clone(),
863 self.display_name.clone(),
864 self.persona,
865 );
866 user.email = Some(self.email.clone());
867 user.department = self.department_id.clone();
868 user.working_hours = self.working_hours.clone();
869 user.company_codes = self.authorized_company_codes.clone();
870 user.cost_centers = self.authorized_cost_centers.clone();
871 user.is_active = self.status.can_transact();
872 user.start_date = self.hire_date;
873 user.end_date = self.termination_date;
874 user
875 }
876}
877
878#[derive(Debug, Clone, Default, Serialize, Deserialize)]
880pub struct EmployeePool {
881 pub employees: Vec<Employee>,
883 #[serde(skip)]
885 id_index: std::collections::HashMap<String, usize>,
886 #[serde(skip)]
888 manager_index: std::collections::HashMap<String, Vec<usize>>,
889 #[serde(skip)]
891 persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
892 #[serde(skip)]
894 department_index: std::collections::HashMap<String, Vec<usize>>,
895}
896
897impl EmployeePool {
898 pub fn new() -> Self {
900 Self::default()
901 }
902
903 pub fn add_employee(&mut self, employee: Employee) {
905 let idx = self.employees.len();
906
907 self.id_index.insert(employee.employee_id.clone(), idx);
908
909 if let Some(ref mgr_id) = employee.manager_id {
910 self.manager_index
911 .entry(mgr_id.clone())
912 .or_default()
913 .push(idx);
914 }
915
916 self.persona_index
917 .entry(employee.persona)
918 .or_default()
919 .push(idx);
920
921 if let Some(ref dept_id) = employee.department_id {
922 self.department_index
923 .entry(dept_id.clone())
924 .or_default()
925 .push(idx);
926 }
927
928 self.employees.push(employee);
929 }
930
931 pub fn get_by_id(&self, employee_id: &str) -> Option<&Employee> {
933 self.id_index
934 .get(employee_id)
935 .map(|&idx| &self.employees[idx])
936 }
937
938 pub fn get_by_id_mut(&mut self, employee_id: &str) -> Option<&mut Employee> {
940 self.id_index
941 .get(employee_id)
942 .copied()
943 .map(|idx| &mut self.employees[idx])
944 }
945
946 pub fn get_direct_reports(&self, manager_id: &str) -> Vec<&Employee> {
948 self.manager_index
949 .get(manager_id)
950 .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
951 .unwrap_or_default()
952 }
953
954 pub fn get_by_persona(&self, persona: UserPersona) -> Vec<&Employee> {
956 self.persona_index
957 .get(&persona)
958 .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
959 .unwrap_or_default()
960 }
961
962 pub fn get_by_department(&self, department_id: &str) -> Vec<&Employee> {
964 self.department_index
965 .get(department_id)
966 .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
967 .unwrap_or_default()
968 }
969
970 pub fn get_random_approver(&self, rng: &mut impl rand::Rng) -> Option<&Employee> {
972 use rand::seq::SliceRandom;
973
974 let approvers: Vec<_> = self
975 .employees
976 .iter()
977 .filter(|e| e.persona.has_approval_authority() && e.status == EmployeeStatus::Active)
978 .collect();
979
980 approvers.choose(rng).copied()
981 }
982
983 pub fn get_approver_for_amount(
985 &self,
986 amount: Decimal,
987 rng: &mut impl rand::Rng,
988 ) -> Option<&Employee> {
989 use rand::seq::SliceRandom;
990
991 let approvers: Vec<_> = self
992 .employees
993 .iter()
994 .filter(|e| e.can_approve_amount(amount))
995 .collect();
996
997 approvers.choose(rng).copied()
998 }
999
1000 pub fn get_managers(&self) -> Vec<&Employee> {
1002 self.employees
1003 .iter()
1004 .filter(|e| !e.direct_reports.is_empty() || e.job_level.is_manager())
1005 .collect()
1006 }
1007
1008 pub fn get_reporting_chain(&self, employee_id: &str) -> Vec<&Employee> {
1010 let mut chain = Vec::new();
1011 let mut current_id = employee_id.to_string();
1012
1013 while let Some(emp) = self.get_by_id(¤t_id) {
1014 chain.push(emp);
1015 if let Some(ref mgr_id) = emp.manager_id {
1016 current_id = mgr_id.clone();
1017 } else {
1018 break;
1019 }
1020 }
1021
1022 chain
1023 }
1024
1025 pub fn rebuild_indices(&mut self) {
1027 self.id_index.clear();
1028 self.manager_index.clear();
1029 self.persona_index.clear();
1030 self.department_index.clear();
1031
1032 for (idx, employee) in self.employees.iter().enumerate() {
1033 self.id_index.insert(employee.employee_id.clone(), idx);
1034
1035 if let Some(ref mgr_id) = employee.manager_id {
1036 self.manager_index
1037 .entry(mgr_id.clone())
1038 .or_default()
1039 .push(idx);
1040 }
1041
1042 self.persona_index
1043 .entry(employee.persona)
1044 .or_default()
1045 .push(idx);
1046
1047 if let Some(ref dept_id) = employee.department_id {
1048 self.department_index
1049 .entry(dept_id.clone())
1050 .or_default()
1051 .push(idx);
1052 }
1053 }
1054 }
1055
1056 pub fn len(&self) -> usize {
1058 self.employees.len()
1059 }
1060
1061 pub fn is_empty(&self) -> bool {
1063 self.employees.is_empty()
1064 }
1065}
1066
1067#[cfg(test)]
1068mod tests {
1069 use super::*;
1070
1071 #[test]
1072 fn test_persona_properties() {
1073 assert!(UserPersona::JuniorAccountant.is_human());
1074 assert!(!UserPersona::AutomatedSystem.is_human());
1075 assert!(UserPersona::Controller.has_approval_authority());
1076 assert!(!UserPersona::JuniorAccountant.has_approval_authority());
1077 }
1078
1079 #[test]
1080 fn test_user_pool() {
1081 let pool = UserPool::generate_standard(&["1000".to_string()]);
1082 assert!(!pool.users.is_empty());
1083 assert!(!pool
1084 .get_users_by_persona(UserPersona::JuniorAccountant)
1085 .is_empty());
1086 }
1087
1088 #[test]
1089 fn test_job_level_hierarchy() {
1090 assert!(JobLevel::Executive.management_level() > JobLevel::Manager.management_level());
1091 assert!(JobLevel::Manager.is_manager());
1092 assert!(!JobLevel::Staff.is_manager());
1093 }
1094
1095 #[test]
1096 fn test_employee_creation() {
1097 let employee = Employee::new("E-001", "jsmith", "John", "Smith", "1000")
1098 .with_persona(UserPersona::Controller);
1099
1100 assert_eq!(employee.employee_id, "E-001");
1101 assert_eq!(employee.display_name, "John Smith");
1102 assert!(employee.can_approve_je);
1103 assert_eq!(employee.job_level, JobLevel::Manager);
1104 }
1105
1106 #[test]
1107 fn test_employee_approval_limits() {
1108 let employee = Employee::new("E-001", "test", "Test", "User", "1000")
1109 .with_approval_limit(Decimal::from(10000));
1110
1111 assert!(employee.can_approve_amount(Decimal::from(5000)));
1112 assert!(!employee.can_approve_amount(Decimal::from(15000)));
1113 }
1114
1115 #[test]
1116 fn test_employee_pool_hierarchy() {
1117 let mut pool = EmployeePool::new();
1118
1119 let cfo = Employee::new("E-001", "cfo", "Jane", "CEO", "1000")
1120 .with_persona(UserPersona::Executive);
1121
1122 let controller = Employee::new("E-002", "ctrl", "Bob", "Controller", "1000")
1123 .with_persona(UserPersona::Controller)
1124 .with_manager("E-001");
1125
1126 let accountant = Employee::new("E-003", "acc", "Alice", "Accountant", "1000")
1127 .with_persona(UserPersona::JuniorAccountant)
1128 .with_manager("E-002");
1129
1130 pool.add_employee(cfo);
1131 pool.add_employee(controller);
1132 pool.add_employee(accountant);
1133
1134 let direct_reports = pool.get_direct_reports("E-001");
1136 assert_eq!(direct_reports.len(), 1);
1137 assert_eq!(direct_reports[0].employee_id, "E-002");
1138
1139 let chain = pool.get_reporting_chain("E-003");
1141 assert_eq!(chain.len(), 3);
1142 assert_eq!(chain[0].employee_id, "E-003");
1143 assert_eq!(chain[1].employee_id, "E-002");
1144 assert_eq!(chain[2].employee_id, "E-001");
1145 }
1146
1147 #[test]
1148 fn test_employee_to_user() {
1149 let employee = Employee::new("E-001", "jdoe", "John", "Doe", "1000")
1150 .with_persona(UserPersona::SeniorAccountant);
1151
1152 let user = employee.to_user();
1153
1154 assert_eq!(user.user_id, "jdoe");
1155 assert_eq!(user.persona, UserPersona::SeniorAccountant);
1156 assert!(user.is_active);
1157 }
1158
1159 #[test]
1160 fn test_employee_status() {
1161 assert!(EmployeeStatus::Active.can_transact());
1162 assert!(EmployeeStatus::Contractor.can_transact());
1163 assert!(!EmployeeStatus::Terminated.can_transact());
1164 assert!(!EmployeeStatus::OnLeave.can_transact());
1165 }
1166}