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 std::fmt;
9
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize};
12
13/// User persona classification for behavioral modeling.
14///
15/// Different personas exhibit different transaction patterns, timing,
16/// error rates, and access to accounts/functions.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
18#[serde(rename_all = "snake_case")]
19pub enum UserPersona {
20    /// Entry-level accountant with limited access
21    JuniorAccountant,
22    /// Experienced accountant with broader access
23    SeniorAccountant,
24    /// Financial controller with approval authority
25    Controller,
26    /// Management with override capabilities
27    Manager,
28    /// CFO/Finance Director with full access
29    Executive,
30    /// Automated batch job or interface
31    #[default]
32    AutomatedSystem,
33    /// External auditor with read access
34    ExternalAuditor,
35    /// Fraud actor for simulation scenarios
36    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    /// Check if this persona represents a human user.
56    pub fn is_human(&self) -> bool {
57        !matches!(self, Self::AutomatedSystem)
58    }
59
60    /// Check if this persona has approval authority.
61    pub fn has_approval_authority(&self) -> bool {
62        matches!(self, Self::Controller | Self::Manager | Self::Executive)
63    }
64
65    /// Get typical error rate for this persona (0.0-1.0).
66    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    /// Get typical transaction volume per day.
80    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    /// Get approval threshold amount.
94    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, // Unlimited
101            Self::AutomatedSystem => Some(1000000.0),
102            Self::ExternalAuditor => Some(0.0), // Read-only
103            Self::FraudActor => Some(10000.0),
104        }
105    }
106}
107
108/// Working hours pattern for human users.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct WorkingHoursPattern {
111    /// Start hour (0-23)
112    pub start_hour: u8,
113    /// End hour (0-23)
114    pub end_hour: u8,
115    /// Peak hours (typically mid-morning and mid-afternoon)
116    pub peak_hours: Vec<u8>,
117    /// Probability of weekend work
118    pub weekend_probability: f64,
119    /// Probability of after-hours work
120    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    /// Pattern for European office hours.
137    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    /// Pattern for US office hours.
148    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    /// Pattern for Asian office hours.
159    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    /// Pattern for 24/7 batch processing.
170    pub fn batch_processing() -> Self {
171        Self {
172            start_hour: 0,
173            end_hour: 24,
174            peak_hours: vec![2, 3, 4, 22, 23], // Off-peak hours for systems
175            weekend_probability: 1.0,
176            after_hours_probability: 1.0,
177        }
178    }
179
180    /// Check if an hour is within working hours.
181    pub fn is_working_hour(&self, hour: u8) -> bool {
182        hour >= self.start_hour && hour < self.end_hour
183    }
184
185    /// Check if an hour is a peak hour.
186    pub fn is_peak_hour(&self, hour: u8) -> bool {
187        self.peak_hours.contains(&hour)
188    }
189}
190
191/// Individual user account for transaction attribution.
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct User {
194    /// User ID (login name)
195    pub user_id: String,
196
197    /// Display name
198    pub display_name: String,
199
200    /// Email address
201    pub email: Option<String>,
202
203    /// Persona classification
204    pub persona: UserPersona,
205
206    /// Department
207    pub department: Option<String>,
208
209    /// Working hours pattern
210    pub working_hours: WorkingHoursPattern,
211
212    /// Assigned company codes
213    pub company_codes: Vec<String>,
214
215    /// Assigned cost centers (can post to)
216    pub cost_centers: Vec<String>,
217
218    /// Is this user currently active
219    pub is_active: bool,
220
221    /// Start date of employment
222    pub start_date: Option<chrono::NaiveDate>,
223
224    /// End date of employment (if terminated)
225    pub end_date: Option<chrono::NaiveDate>,
226}
227
228impl User {
229    /// Create a new user with minimal required fields.
230    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    /// Create a system/batch user.
253    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    /// Check if user can post to a company code.
262    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    /// Generate a typical username for a persona.
267    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), // Appears normal
277        }
278    }
279}
280
281/// Pool of users for transaction attribution.
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct UserPool {
284    /// All users in the pool
285    pub users: Vec<User>,
286    /// Index by persona for quick lookup
287    #[serde(skip)]
288    persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
289}
290
291impl UserPool {
292    /// Create a new empty user pool.
293    pub fn new() -> Self {
294        Self {
295            users: Vec::new(),
296            persona_index: std::collections::HashMap::new(),
297        }
298    }
299
300    /// Add a user to the pool.
301    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    /// Get all users of a specific persona.
309    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    /// Get a random user of a specific persona.
317    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    /// Rebuild the persona index (call after deserialization).
323    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    /// Generate a standard user pool with typical distribution.
334    pub fn generate_standard(company_codes: &[String]) -> Self {
335        let mut pool = Self::new();
336
337        // Junior accountants (many)
338        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        // Senior accountants
349        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        // Controllers
360        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        // Managers
371        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        // Automated systems (many)
382        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/// Employee job level in the organization hierarchy.
403#[derive(
404    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
405)]
406#[serde(rename_all = "snake_case")]
407pub enum JobLevel {
408    /// Individual contributor
409    #[default]
410    Staff,
411    /// Senior individual contributor
412    Senior,
413    /// Lead/principal
414    Lead,
415    /// First-line manager
416    Supervisor,
417    /// Middle management
418    Manager,
419    /// Senior manager / director
420    Director,
421    /// VP level
422    VicePresident,
423    /// C-level executive
424    Executive,
425}
426
427impl JobLevel {
428    /// Get the management level (0 = IC, higher = more senior).
429    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    /// Check if this is a management position.
443    pub fn is_manager(&self) -> bool {
444        self.management_level() >= 2
445    }
446
447    /// Get typical direct reports range.
448    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/// Employee status in HR system.
462#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
463#[serde(rename_all = "snake_case")]
464pub enum EmployeeStatus {
465    /// Active employee
466    #[default]
467    Active,
468    /// On leave (sabbatical, parental, etc.)
469    OnLeave,
470    /// Suspended
471    Suspended,
472    /// Notice period
473    NoticePeriod,
474    /// Terminated
475    Terminated,
476    /// Retired
477    Retired,
478    /// Contractor (not full employee)
479    Contractor,
480}
481
482impl EmployeeStatus {
483    /// Check if employee can perform transactions.
484    pub fn can_transact(&self) -> bool {
485        matches!(self, Self::Active | Self::Contractor)
486    }
487
488    /// Check if employee is active in some capacity.
489    pub fn is_active(&self) -> bool {
490        !matches!(self, Self::Terminated | Self::Retired)
491    }
492}
493
494/// System role for access control.
495#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
496#[serde(rename_all = "snake_case")]
497pub enum SystemRole {
498    /// View-only access
499    Viewer,
500    /// Can create documents
501    Creator,
502    /// Can approve documents
503    Approver,
504    /// Can release payments
505    PaymentReleaser,
506    /// Can perform bank transactions
507    BankProcessor,
508    /// Can post journal entries
509    JournalPoster,
510    /// Can perform period close activities
511    PeriodClose,
512    /// System administrator
513    Admin,
514    /// AP Accountant
515    ApAccountant,
516    /// AR Accountant
517    ArAccountant,
518    /// Buyer / Procurement
519    Buyer,
520    /// Executive / Management
521    Executive,
522    /// Financial Analyst
523    FinancialAnalyst,
524    /// General Accountant
525    GeneralAccountant,
526    /// Custom role with name
527    Custom(String),
528}
529
530/// Transaction code authorization.
531#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct TransactionCodeAuth {
533    /// Transaction code (e.g., "FB01", "ME21N")
534    pub tcode: String,
535    /// Activity type (create, change, display, delete)
536    pub activity: ActivityType,
537    /// Is authorization active?
538    pub active: bool,
539}
540
541/// Activity types for authorization.
542#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
543#[serde(rename_all = "snake_case")]
544pub enum ActivityType {
545    /// Display only
546    #[default]
547    Display,
548    /// Create new
549    Create,
550    /// Change existing
551    Change,
552    /// Delete
553    Delete,
554    /// Execute (for reports)
555    Execute,
556}
557
558/// Employee master data with organizational hierarchy.
559#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct Employee {
561    /// Employee ID (e.g., "E-001234")
562    pub employee_id: String,
563
564    /// User ID (login name, links to User)
565    pub user_id: String,
566
567    /// Display name
568    pub display_name: String,
569
570    /// First name
571    pub first_name: String,
572
573    /// Last name
574    pub last_name: String,
575
576    /// Email address
577    pub email: String,
578
579    /// Persona classification
580    pub persona: UserPersona,
581
582    /// Job level
583    pub job_level: JobLevel,
584
585    /// Job title
586    pub job_title: String,
587
588    /// Department ID
589    pub department_id: Option<String>,
590
591    /// Cost center
592    pub cost_center: Option<String>,
593
594    /// Manager's employee ID (for hierarchy)
595    pub manager_id: Option<String>,
596
597    /// Direct reports (employee IDs)
598    pub direct_reports: Vec<String>,
599
600    /// Employment status
601    pub status: EmployeeStatus,
602
603    /// Company code
604    pub company_code: String,
605
606    /// Working hours pattern
607    pub working_hours: WorkingHoursPattern,
608
609    /// Authorized company codes
610    pub authorized_company_codes: Vec<String>,
611
612    /// Authorized cost centers
613    pub authorized_cost_centers: Vec<String>,
614
615    /// Approval limit (monetary threshold)
616    pub approval_limit: Decimal,
617
618    /// Can approve purchase requisitions
619    pub can_approve_pr: bool,
620
621    /// Can approve purchase orders
622    pub can_approve_po: bool,
623
624    /// Can approve invoices
625    pub can_approve_invoice: bool,
626
627    /// Can approve journal entries
628    pub can_approve_je: bool,
629
630    /// Can release payments
631    pub can_release_payment: bool,
632
633    /// System roles
634    pub system_roles: Vec<SystemRole>,
635
636    /// Authorized transaction codes
637    pub transaction_codes: Vec<TransactionCodeAuth>,
638
639    /// Hire date
640    pub hire_date: Option<chrono::NaiveDate>,
641
642    /// Termination date (if applicable)
643    pub termination_date: Option<chrono::NaiveDate>,
644
645    /// Location / plant
646    pub location: Option<String>,
647
648    /// Is this an intercompany employee (works for multiple entities)?
649    pub is_shared_services: bool,
650
651    /// Phone number
652    pub phone: Option<String>,
653}
654
655impl Employee {
656    /// Create a new employee.
657    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    /// Set persona and adjust defaults accordingly.
710    pub fn with_persona(mut self, persona: UserPersona) -> Self {
711        self.persona = persona;
712
713        // Adjust job level and approval limit based on persona
714        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); // Unlimited
749                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    /// Set job level.
775    pub fn with_job_level(mut self, level: JobLevel) -> Self {
776        self.job_level = level;
777        self
778    }
779
780    /// Set job title.
781    pub fn with_job_title(mut self, title: impl Into<String>) -> Self {
782        self.job_title = title.into();
783        self
784    }
785
786    /// Set manager.
787    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    /// Set department.
793    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    /// Set cost center.
799    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    /// Set approval limit.
805    pub fn with_approval_limit(mut self, limit: Decimal) -> Self {
806        self.approval_limit = limit;
807        self
808    }
809
810    /// Add authorized company code.
811    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    /// Add system role.
817    pub fn with_role(mut self, role: SystemRole) -> Self {
818        self.system_roles.push(role);
819        self
820    }
821
822    /// Set hire date.
823    pub fn with_hire_date(mut self, date: chrono::NaiveDate) -> Self {
824        self.hire_date = Some(date);
825        self
826    }
827
828    /// Add a direct report.
829    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    /// Check if employee can approve an amount.
836    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    /// Check if employee can approve in a company code.
844    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    /// Check if employee has a specific role.
856    pub fn has_role(&self, role: &SystemRole) -> bool {
857        self.system_roles.contains(role)
858    }
859
860    /// Get the depth in the org hierarchy (0 = top).
861    pub fn hierarchy_depth(&self) -> u8 {
862        // This would need the full employee registry to compute properly
863        // For now, estimate based on job level
864        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    /// Convert to a User (for backward compatibility).
877    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/// Pool of employees with organizational hierarchy support.
896#[derive(Debug, Clone, Default, Serialize, Deserialize)]
897pub struct EmployeePool {
898    /// All employees
899    pub employees: Vec<Employee>,
900    /// Index by employee ID
901    #[serde(skip)]
902    id_index: std::collections::HashMap<String, usize>,
903    /// Index by manager ID (for finding direct reports)
904    #[serde(skip)]
905    manager_index: std::collections::HashMap<String, Vec<usize>>,
906    /// Index by persona
907    #[serde(skip)]
908    persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
909    /// Index by department
910    #[serde(skip)]
911    department_index: std::collections::HashMap<String, Vec<usize>>,
912}
913
914impl EmployeePool {
915    /// Create a new empty employee pool.
916    pub fn new() -> Self {
917        Self::default()
918    }
919
920    /// Add an employee to the pool.
921    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    /// Get employee by ID.
949    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    /// Get mutable employee by ID.
956    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    /// Get direct reports of a manager.
964    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    /// Get employees by persona.
972    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    /// Get employees by department.
980    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    /// Get a random employee with approval authority.
988    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    /// Get approver for a specific amount.
1001    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    /// Get all managers (employees with direct reports).
1018    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    /// Get org chart path from employee to top.
1026    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(&current_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    /// Rebuild indices after deserialization.
1043    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    /// Get count.
1074    pub fn len(&self) -> usize {
1075        self.employees.len()
1076    }
1077
1078    /// Check if empty.
1079    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        // Verify no persona produces concatenated words (the bug this fixes)
1115        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        // Test getting direct reports
1190        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        // Test reporting chain
1195        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}