Skip to main content

datasynth_core/models/
user.rs

1//! User persona and behavior models.
2//!
3//! Defines user personas and behavioral patterns for realistic
4//! transaction generation, including working hours, error rates,
5//! and transaction volumes. Also includes Employee model with
6//! manager hierarchy for organizational structure simulation.
7
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10
11/// User persona classification for behavioral modeling.
12///
13/// Different personas exhibit different transaction patterns, timing,
14/// error rates, and access to accounts/functions.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
16#[serde(rename_all = "snake_case")]
17pub enum UserPersona {
18    /// Entry-level accountant with limited access
19    JuniorAccountant,
20    /// Experienced accountant with broader access
21    SeniorAccountant,
22    /// Financial controller with approval authority
23    Controller,
24    /// Management with override capabilities
25    Manager,
26    /// CFO/Finance Director with full access
27    Executive,
28    /// Automated batch job or interface
29    #[default]
30    AutomatedSystem,
31    /// External auditor with read access
32    ExternalAuditor,
33    /// Fraud actor for simulation scenarios
34    FraudActor,
35}
36
37impl UserPersona {
38    /// Check if this persona represents a human user.
39    pub fn is_human(&self) -> bool {
40        !matches!(self, Self::AutomatedSystem)
41    }
42
43    /// Check if this persona has approval authority.
44    pub fn has_approval_authority(&self) -> bool {
45        matches!(self, Self::Controller | Self::Manager | Self::Executive)
46    }
47
48    /// Get typical error rate for this persona (0.0-1.0).
49    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    /// Get typical transaction volume per day.
63    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    /// Get approval threshold amount.
77    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, // Unlimited
84            Self::AutomatedSystem => Some(1000000.0),
85            Self::ExternalAuditor => Some(0.0), // Read-only
86            Self::FraudActor => Some(10000.0),
87        }
88    }
89}
90
91/// Working hours pattern for human users.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct WorkingHoursPattern {
94    /// Start hour (0-23)
95    pub start_hour: u8,
96    /// End hour (0-23)
97    pub end_hour: u8,
98    /// Peak hours (typically mid-morning and mid-afternoon)
99    pub peak_hours: Vec<u8>,
100    /// Probability of weekend work
101    pub weekend_probability: f64,
102    /// Probability of after-hours work
103    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    /// Pattern for European office hours.
120    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    /// Pattern for US office hours.
131    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    /// Pattern for Asian office hours.
142    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    /// Pattern for 24/7 batch processing.
153    pub fn batch_processing() -> Self {
154        Self {
155            start_hour: 0,
156            end_hour: 24,
157            peak_hours: vec![2, 3, 4, 22, 23], // Off-peak hours for systems
158            weekend_probability: 1.0,
159            after_hours_probability: 1.0,
160        }
161    }
162
163    /// Check if an hour is within working hours.
164    pub fn is_working_hour(&self, hour: u8) -> bool {
165        hour >= self.start_hour && hour < self.end_hour
166    }
167
168    /// Check if an hour is a peak hour.
169    pub fn is_peak_hour(&self, hour: u8) -> bool {
170        self.peak_hours.contains(&hour)
171    }
172}
173
174/// Individual user account for transaction attribution.
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct User {
177    /// User ID (login name)
178    pub user_id: String,
179
180    /// Display name
181    pub display_name: String,
182
183    /// Email address
184    pub email: Option<String>,
185
186    /// Persona classification
187    pub persona: UserPersona,
188
189    /// Department
190    pub department: Option<String>,
191
192    /// Working hours pattern
193    pub working_hours: WorkingHoursPattern,
194
195    /// Assigned company codes
196    pub company_codes: Vec<String>,
197
198    /// Assigned cost centers (can post to)
199    pub cost_centers: Vec<String>,
200
201    /// Is this user currently active
202    pub is_active: bool,
203
204    /// Start date of employment
205    pub start_date: Option<chrono::NaiveDate>,
206
207    /// End date of employment (if terminated)
208    pub end_date: Option<chrono::NaiveDate>,
209}
210
211impl User {
212    /// Create a new user with minimal required fields.
213    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    /// Create a system/batch user.
236    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    /// Check if user can post to a company code.
245    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    /// Generate a typical username for a persona.
250    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), // Appears normal
260        }
261    }
262}
263
264/// Pool of users for transaction attribution.
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct UserPool {
267    /// All users in the pool
268    pub users: Vec<User>,
269    /// Index by persona for quick lookup
270    #[serde(skip)]
271    persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
272}
273
274impl UserPool {
275    /// Create a new empty user pool.
276    pub fn new() -> Self {
277        Self {
278            users: Vec::new(),
279            persona_index: std::collections::HashMap::new(),
280        }
281    }
282
283    /// Add a user to the pool.
284    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    /// Get all users of a specific persona.
292    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    /// Get a random user of a specific persona.
300    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    /// Rebuild the persona index (call after deserialization).
306    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    /// Generate a standard user pool with typical distribution.
317    pub fn generate_standard(company_codes: &[String]) -> Self {
318        let mut pool = Self::new();
319
320        // Junior accountants (many)
321        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        // Senior accountants
332        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        // Controllers
343        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        // Managers
354        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        // Automated systems (many)
365        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/// Employee job level in the organization hierarchy.
386#[derive(
387    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
388)]
389#[serde(rename_all = "snake_case")]
390pub enum JobLevel {
391    /// Individual contributor
392    #[default]
393    Staff,
394    /// Senior individual contributor
395    Senior,
396    /// Lead/principal
397    Lead,
398    /// First-line manager
399    Supervisor,
400    /// Middle management
401    Manager,
402    /// Senior manager / director
403    Director,
404    /// VP level
405    VicePresident,
406    /// C-level executive
407    Executive,
408}
409
410impl JobLevel {
411    /// Get the management level (0 = IC, higher = more senior).
412    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    /// Check if this is a management position.
426    pub fn is_manager(&self) -> bool {
427        self.management_level() >= 2
428    }
429
430    /// Get typical direct reports range.
431    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/// Employee status in HR system.
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
446#[serde(rename_all = "snake_case")]
447pub enum EmployeeStatus {
448    /// Active employee
449    #[default]
450    Active,
451    /// On leave (sabbatical, parental, etc.)
452    OnLeave,
453    /// Suspended
454    Suspended,
455    /// Notice period
456    NoticePeriod,
457    /// Terminated
458    Terminated,
459    /// Retired
460    Retired,
461    /// Contractor (not full employee)
462    Contractor,
463}
464
465impl EmployeeStatus {
466    /// Check if employee can perform transactions.
467    pub fn can_transact(&self) -> bool {
468        matches!(self, Self::Active | Self::Contractor)
469    }
470
471    /// Check if employee is active in some capacity.
472    pub fn is_active(&self) -> bool {
473        !matches!(self, Self::Terminated | Self::Retired)
474    }
475}
476
477/// System role for access control.
478#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
479#[serde(rename_all = "snake_case")]
480pub enum SystemRole {
481    /// View-only access
482    Viewer,
483    /// Can create documents
484    Creator,
485    /// Can approve documents
486    Approver,
487    /// Can release payments
488    PaymentReleaser,
489    /// Can perform bank transactions
490    BankProcessor,
491    /// Can post journal entries
492    JournalPoster,
493    /// Can perform period close activities
494    PeriodClose,
495    /// System administrator
496    Admin,
497    /// AP Accountant
498    ApAccountant,
499    /// AR Accountant
500    ArAccountant,
501    /// Buyer / Procurement
502    Buyer,
503    /// Executive / Management
504    Executive,
505    /// Financial Analyst
506    FinancialAnalyst,
507    /// General Accountant
508    GeneralAccountant,
509    /// Custom role with name
510    Custom(String),
511}
512
513/// Transaction code authorization.
514#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct TransactionCodeAuth {
516    /// Transaction code (e.g., "FB01", "ME21N")
517    pub tcode: String,
518    /// Activity type (create, change, display, delete)
519    pub activity: ActivityType,
520    /// Is authorization active?
521    pub active: bool,
522}
523
524/// Activity types for authorization.
525#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
526#[serde(rename_all = "snake_case")]
527pub enum ActivityType {
528    /// Display only
529    #[default]
530    Display,
531    /// Create new
532    Create,
533    /// Change existing
534    Change,
535    /// Delete
536    Delete,
537    /// Execute (for reports)
538    Execute,
539}
540
541/// Employee master data with organizational hierarchy.
542#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct Employee {
544    /// Employee ID (e.g., "E-001234")
545    pub employee_id: String,
546
547    /// User ID (login name, links to User)
548    pub user_id: String,
549
550    /// Display name
551    pub display_name: String,
552
553    /// First name
554    pub first_name: String,
555
556    /// Last name
557    pub last_name: String,
558
559    /// Email address
560    pub email: String,
561
562    /// Persona classification
563    pub persona: UserPersona,
564
565    /// Job level
566    pub job_level: JobLevel,
567
568    /// Job title
569    pub job_title: String,
570
571    /// Department ID
572    pub department_id: Option<String>,
573
574    /// Cost center
575    pub cost_center: Option<String>,
576
577    /// Manager's employee ID (for hierarchy)
578    pub manager_id: Option<String>,
579
580    /// Direct reports (employee IDs)
581    pub direct_reports: Vec<String>,
582
583    /// Employment status
584    pub status: EmployeeStatus,
585
586    /// Company code
587    pub company_code: String,
588
589    /// Working hours pattern
590    pub working_hours: WorkingHoursPattern,
591
592    /// Authorized company codes
593    pub authorized_company_codes: Vec<String>,
594
595    /// Authorized cost centers
596    pub authorized_cost_centers: Vec<String>,
597
598    /// Approval limit (monetary threshold)
599    pub approval_limit: Decimal,
600
601    /// Can approve purchase requisitions
602    pub can_approve_pr: bool,
603
604    /// Can approve purchase orders
605    pub can_approve_po: bool,
606
607    /// Can approve invoices
608    pub can_approve_invoice: bool,
609
610    /// Can approve journal entries
611    pub can_approve_je: bool,
612
613    /// Can release payments
614    pub can_release_payment: bool,
615
616    /// System roles
617    pub system_roles: Vec<SystemRole>,
618
619    /// Authorized transaction codes
620    pub transaction_codes: Vec<TransactionCodeAuth>,
621
622    /// Hire date
623    pub hire_date: Option<chrono::NaiveDate>,
624
625    /// Termination date (if applicable)
626    pub termination_date: Option<chrono::NaiveDate>,
627
628    /// Location / plant
629    pub location: Option<String>,
630
631    /// Is this an intercompany employee (works for multiple entities)?
632    pub is_shared_services: bool,
633
634    /// Phone number
635    pub phone: Option<String>,
636}
637
638impl Employee {
639    /// Create a new employee.
640    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    /// Set persona and adjust defaults accordingly.
693    pub fn with_persona(mut self, persona: UserPersona) -> Self {
694        self.persona = persona;
695
696        // Adjust job level and approval limit based on persona
697        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); // Unlimited
732                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    /// Set job level.
758    pub fn with_job_level(mut self, level: JobLevel) -> Self {
759        self.job_level = level;
760        self
761    }
762
763    /// Set job title.
764    pub fn with_job_title(mut self, title: impl Into<String>) -> Self {
765        self.job_title = title.into();
766        self
767    }
768
769    /// Set manager.
770    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    /// Set department.
776    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    /// Set cost center.
782    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    /// Set approval limit.
788    pub fn with_approval_limit(mut self, limit: Decimal) -> Self {
789        self.approval_limit = limit;
790        self
791    }
792
793    /// Add authorized company code.
794    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    /// Add system role.
800    pub fn with_role(mut self, role: SystemRole) -> Self {
801        self.system_roles.push(role);
802        self
803    }
804
805    /// Set hire date.
806    pub fn with_hire_date(mut self, date: chrono::NaiveDate) -> Self {
807        self.hire_date = Some(date);
808        self
809    }
810
811    /// Add a direct report.
812    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    /// Check if employee can approve an amount.
819    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    /// Check if employee can approve in a company code.
827    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    /// Check if employee has a specific role.
839    pub fn has_role(&self, role: &SystemRole) -> bool {
840        self.system_roles.contains(role)
841    }
842
843    /// Get the depth in the org hierarchy (0 = top).
844    pub fn hierarchy_depth(&self) -> u8 {
845        // This would need the full employee registry to compute properly
846        // For now, estimate based on job level
847        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    /// Convert to a User (for backward compatibility).
860    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/// Pool of employees with organizational hierarchy support.
879#[derive(Debug, Clone, Default, Serialize, Deserialize)]
880pub struct EmployeePool {
881    /// All employees
882    pub employees: Vec<Employee>,
883    /// Index by employee ID
884    #[serde(skip)]
885    id_index: std::collections::HashMap<String, usize>,
886    /// Index by manager ID (for finding direct reports)
887    #[serde(skip)]
888    manager_index: std::collections::HashMap<String, Vec<usize>>,
889    /// Index by persona
890    #[serde(skip)]
891    persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
892    /// Index by department
893    #[serde(skip)]
894    department_index: std::collections::HashMap<String, Vec<usize>>,
895}
896
897impl EmployeePool {
898    /// Create a new empty employee pool.
899    pub fn new() -> Self {
900        Self::default()
901    }
902
903    /// Add an employee to the pool.
904    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    /// Get employee by ID.
932    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    /// Get mutable employee by ID.
939    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    /// Get direct reports of a manager.
947    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    /// Get employees by persona.
955    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    /// Get employees by department.
963    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    /// Get a random employee with approval authority.
971    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    /// Get approver for a specific amount.
984    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    /// Get all managers (employees with direct reports).
1001    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    /// Get org chart path from employee to top.
1009    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(&current_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    /// Rebuild indices after deserialization.
1026    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    /// Get count.
1057    pub fn len(&self) -> usize {
1058        self.employees.len()
1059    }
1060
1061    /// Check if empty.
1062    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        // Test getting direct reports
1135        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        // Test reporting chain
1140        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}