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{index:04}"),
270            UserPersona::SeniorAccountant => format!("SACC{index:04}"),
271            UserPersona::Controller => format!("CTRL{index:04}"),
272            UserPersona::Manager => format!("MGR{index:04}"),
273            UserPersona::Executive => format!("EXEC{index:04}"),
274            UserPersona::AutomatedSystem => format!("BATCH{index:04}"),
275            UserPersona::ExternalAuditor => format!("AUDIT{index:04}"),
276            UserPersona::FraudActor => format!("USER{index:04}"), // 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    /// Annual base salary in the company's local currency.
655    ///
656    /// Used by the payroll generator to compute monthly gross pay
657    /// (`base_salary / 12`) instead of a hardcoded default.
658    #[serde(with = "rust_decimal::serde::str")]
659    pub base_salary: rust_decimal::Decimal,
660}
661
662impl Employee {
663    /// Create a new employee.
664    pub fn new(
665        employee_id: impl Into<String>,
666        user_id: impl Into<String>,
667        first_name: impl Into<String>,
668        last_name: impl Into<String>,
669        company_code: impl Into<String>,
670    ) -> Self {
671        let first = first_name.into();
672        let last = last_name.into();
673        let uid = user_id.into();
674        let display_name = format!("{first} {last}");
675        let email = format!(
676            "{}.{}@company.com",
677            first.to_lowercase(),
678            last.to_lowercase()
679        );
680
681        Self {
682            employee_id: employee_id.into(),
683            user_id: uid,
684            display_name,
685            first_name: first,
686            last_name: last,
687            email,
688            persona: UserPersona::JuniorAccountant,
689            job_level: JobLevel::Staff,
690            job_title: "Staff Accountant".to_string(),
691            department_id: None,
692            cost_center: None,
693            manager_id: None,
694            direct_reports: Vec::new(),
695            status: EmployeeStatus::Active,
696            company_code: company_code.into(),
697            working_hours: WorkingHoursPattern::default(),
698            authorized_company_codes: Vec::new(),
699            authorized_cost_centers: Vec::new(),
700            approval_limit: Decimal::ZERO,
701            can_approve_pr: false,
702            can_approve_po: false,
703            can_approve_invoice: false,
704            can_approve_je: false,
705            can_release_payment: false,
706            system_roles: Vec::new(),
707            transaction_codes: Vec::new(),
708            hire_date: None,
709            termination_date: None,
710            location: None,
711            is_shared_services: false,
712            phone: None,
713            base_salary: Decimal::ZERO,
714        }
715    }
716
717    /// Set persona and adjust defaults accordingly.
718    pub fn with_persona(mut self, persona: UserPersona) -> Self {
719        self.persona = persona;
720
721        // Adjust job level and approval limit based on persona
722        match persona {
723            UserPersona::JuniorAccountant => {
724                self.job_level = JobLevel::Staff;
725                self.job_title = "Junior Accountant".to_string();
726                self.approval_limit = Decimal::from(1000);
727            }
728            UserPersona::SeniorAccountant => {
729                self.job_level = JobLevel::Senior;
730                self.job_title = "Senior Accountant".to_string();
731                self.approval_limit = Decimal::from(10000);
732                self.can_approve_je = true;
733            }
734            UserPersona::Controller => {
735                self.job_level = JobLevel::Manager;
736                self.job_title = "Controller".to_string();
737                self.approval_limit = Decimal::from(100000);
738                self.can_approve_pr = true;
739                self.can_approve_po = true;
740                self.can_approve_invoice = true;
741                self.can_approve_je = true;
742            }
743            UserPersona::Manager => {
744                self.job_level = JobLevel::Director;
745                self.job_title = "Finance Director".to_string();
746                self.approval_limit = Decimal::from(500000);
747                self.can_approve_pr = true;
748                self.can_approve_po = true;
749                self.can_approve_invoice = true;
750                self.can_approve_je = true;
751                self.can_release_payment = true;
752            }
753            UserPersona::Executive => {
754                self.job_level = JobLevel::Executive;
755                self.job_title = "CFO".to_string();
756                self.approval_limit = Decimal::from(999_999_999); // Unlimited
757                self.can_approve_pr = true;
758                self.can_approve_po = true;
759                self.can_approve_invoice = true;
760                self.can_approve_je = true;
761                self.can_release_payment = true;
762            }
763            UserPersona::AutomatedSystem => {
764                self.job_level = JobLevel::Staff;
765                self.job_title = "Batch Process".to_string();
766                self.working_hours = WorkingHoursPattern::batch_processing();
767            }
768            UserPersona::ExternalAuditor => {
769                self.job_level = JobLevel::Senior;
770                self.job_title = "External Auditor".to_string();
771                self.approval_limit = Decimal::ZERO;
772            }
773            UserPersona::FraudActor => {
774                self.job_level = JobLevel::Staff;
775                self.job_title = "Staff Accountant".to_string();
776                self.approval_limit = Decimal::from(10000);
777            }
778        }
779        self
780    }
781
782    /// Set job level.
783    pub fn with_job_level(mut self, level: JobLevel) -> Self {
784        self.job_level = level;
785        self
786    }
787
788    /// Set job title.
789    pub fn with_job_title(mut self, title: impl Into<String>) -> Self {
790        self.job_title = title.into();
791        self
792    }
793
794    /// Set manager.
795    pub fn with_manager(mut self, manager_id: impl Into<String>) -> Self {
796        self.manager_id = Some(manager_id.into());
797        self
798    }
799
800    /// Set department.
801    pub fn with_department(mut self, department_id: impl Into<String>) -> Self {
802        self.department_id = Some(department_id.into());
803        self
804    }
805
806    /// Set cost center.
807    pub fn with_cost_center(mut self, cost_center: impl Into<String>) -> Self {
808        self.cost_center = Some(cost_center.into());
809        self
810    }
811
812    /// Set approval limit.
813    pub fn with_approval_limit(mut self, limit: Decimal) -> Self {
814        self.approval_limit = limit;
815        self
816    }
817
818    /// Add authorized company code.
819    pub fn with_authorized_company(mut self, company_code: impl Into<String>) -> Self {
820        self.authorized_company_codes.push(company_code.into());
821        self
822    }
823
824    /// Add system role.
825    pub fn with_role(mut self, role: SystemRole) -> Self {
826        self.system_roles.push(role);
827        self
828    }
829
830    /// Set hire date.
831    pub fn with_hire_date(mut self, date: chrono::NaiveDate) -> Self {
832        self.hire_date = Some(date);
833        self
834    }
835
836    /// Add a direct report.
837    pub fn add_direct_report(&mut self, employee_id: String) {
838        if !self.direct_reports.contains(&employee_id) {
839            self.direct_reports.push(employee_id);
840        }
841    }
842
843    /// Check if employee can approve an amount.
844    pub fn can_approve_amount(&self, amount: Decimal) -> bool {
845        if self.status != EmployeeStatus::Active {
846            return false;
847        }
848        amount <= self.approval_limit
849    }
850
851    /// Check if employee can approve in a company code.
852    pub fn can_approve_in_company(&self, company_code: &str) -> bool {
853        if self.status != EmployeeStatus::Active {
854            return false;
855        }
856        self.authorized_company_codes.is_empty()
857            || self
858                .authorized_company_codes
859                .iter()
860                .any(|c| c == company_code)
861    }
862
863    /// Check if employee has a specific role.
864    pub fn has_role(&self, role: &SystemRole) -> bool {
865        self.system_roles.contains(role)
866    }
867
868    /// Get the depth in the org hierarchy (0 = top).
869    pub fn hierarchy_depth(&self) -> u8 {
870        // This would need the full employee registry to compute properly
871        // For now, estimate based on job level
872        match self.job_level {
873            JobLevel::Executive => 0,
874            JobLevel::VicePresident => 1,
875            JobLevel::Director => 2,
876            JobLevel::Manager => 3,
877            JobLevel::Supervisor => 4,
878            JobLevel::Lead => 5,
879            JobLevel::Senior => 6,
880            JobLevel::Staff => 7,
881        }
882    }
883
884    /// Convert to a User (for backward compatibility).
885    pub fn to_user(&self) -> User {
886        let mut user = User::new(
887            self.user_id.clone(),
888            self.display_name.clone(),
889            self.persona,
890        );
891        user.email = Some(self.email.clone());
892        user.department = self.department_id.clone();
893        user.working_hours = self.working_hours.clone();
894        user.company_codes = self.authorized_company_codes.clone();
895        user.cost_centers = self.authorized_cost_centers.clone();
896        user.is_active = self.status.can_transact();
897        user.start_date = self.hire_date;
898        user.end_date = self.termination_date;
899        user
900    }
901}
902
903/// Pool of employees with organizational hierarchy support.
904#[derive(Debug, Clone, Default, Serialize, Deserialize)]
905pub struct EmployeePool {
906    /// All employees
907    pub employees: Vec<Employee>,
908    /// Index by employee ID
909    #[serde(skip)]
910    id_index: std::collections::HashMap<String, usize>,
911    /// Index by manager ID (for finding direct reports)
912    #[serde(skip)]
913    manager_index: std::collections::HashMap<String, Vec<usize>>,
914    /// Index by persona
915    #[serde(skip)]
916    persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
917    /// Index by department
918    #[serde(skip)]
919    department_index: std::collections::HashMap<String, Vec<usize>>,
920}
921
922impl EmployeePool {
923    /// Create a new empty employee pool.
924    pub fn new() -> Self {
925        Self::default()
926    }
927
928    /// Add an employee to the pool.
929    pub fn add_employee(&mut self, employee: Employee) {
930        let idx = self.employees.len();
931
932        self.id_index.insert(employee.employee_id.clone(), idx);
933
934        if let Some(ref mgr_id) = employee.manager_id {
935            self.manager_index
936                .entry(mgr_id.clone())
937                .or_default()
938                .push(idx);
939        }
940
941        self.persona_index
942            .entry(employee.persona)
943            .or_default()
944            .push(idx);
945
946        if let Some(ref dept_id) = employee.department_id {
947            self.department_index
948                .entry(dept_id.clone())
949                .or_default()
950                .push(idx);
951        }
952
953        self.employees.push(employee);
954    }
955
956    /// Get employee by ID.
957    pub fn get_by_id(&self, employee_id: &str) -> Option<&Employee> {
958        self.id_index
959            .get(employee_id)
960            .map(|&idx| &self.employees[idx])
961    }
962
963    /// Get mutable employee by ID.
964    pub fn get_by_id_mut(&mut self, employee_id: &str) -> Option<&mut Employee> {
965        self.id_index
966            .get(employee_id)
967            .copied()
968            .map(|idx| &mut self.employees[idx])
969    }
970
971    /// Get direct reports of a manager.
972    pub fn get_direct_reports(&self, manager_id: &str) -> Vec<&Employee> {
973        self.manager_index
974            .get(manager_id)
975            .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
976            .unwrap_or_default()
977    }
978
979    /// Get employees by persona.
980    pub fn get_by_persona(&self, persona: UserPersona) -> Vec<&Employee> {
981        self.persona_index
982            .get(&persona)
983            .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
984            .unwrap_or_default()
985    }
986
987    /// Get employees by department.
988    pub fn get_by_department(&self, department_id: &str) -> Vec<&Employee> {
989        self.department_index
990            .get(department_id)
991            .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
992            .unwrap_or_default()
993    }
994
995    /// Get a random employee with approval authority.
996    pub fn get_random_approver(&self, rng: &mut impl rand::Rng) -> Option<&Employee> {
997        use rand::seq::IndexedRandom;
998
999        let approvers: Vec<_> = self
1000            .employees
1001            .iter()
1002            .filter(|e| e.persona.has_approval_authority() && e.status == EmployeeStatus::Active)
1003            .collect();
1004
1005        approvers.choose(rng).copied()
1006    }
1007
1008    /// Get approver for a specific amount.
1009    pub fn get_approver_for_amount(
1010        &self,
1011        amount: Decimal,
1012        rng: &mut impl rand::Rng,
1013    ) -> Option<&Employee> {
1014        use rand::seq::IndexedRandom;
1015
1016        let approvers: Vec<_> = self
1017            .employees
1018            .iter()
1019            .filter(|e| e.can_approve_amount(amount))
1020            .collect();
1021
1022        approvers.choose(rng).copied()
1023    }
1024
1025    /// Get all managers (employees with direct reports).
1026    pub fn get_managers(&self) -> Vec<&Employee> {
1027        self.employees
1028            .iter()
1029            .filter(|e| !e.direct_reports.is_empty() || e.job_level.is_manager())
1030            .collect()
1031    }
1032
1033    /// Get org chart path from employee to top.
1034    pub fn get_reporting_chain(&self, employee_id: &str) -> Vec<&Employee> {
1035        let mut chain = Vec::new();
1036        let mut current_id = employee_id.to_string();
1037
1038        while let Some(emp) = self.get_by_id(&current_id) {
1039            chain.push(emp);
1040            if let Some(ref mgr_id) = emp.manager_id {
1041                current_id = mgr_id.clone();
1042            } else {
1043                break;
1044            }
1045        }
1046
1047        chain
1048    }
1049
1050    /// Rebuild indices after deserialization.
1051    pub fn rebuild_indices(&mut self) {
1052        self.id_index.clear();
1053        self.manager_index.clear();
1054        self.persona_index.clear();
1055        self.department_index.clear();
1056
1057        for (idx, employee) in self.employees.iter().enumerate() {
1058            self.id_index.insert(employee.employee_id.clone(), idx);
1059
1060            if let Some(ref mgr_id) = employee.manager_id {
1061                self.manager_index
1062                    .entry(mgr_id.clone())
1063                    .or_default()
1064                    .push(idx);
1065            }
1066
1067            self.persona_index
1068                .entry(employee.persona)
1069                .or_default()
1070                .push(idx);
1071
1072            if let Some(ref dept_id) = employee.department_id {
1073                self.department_index
1074                    .entry(dept_id.clone())
1075                    .or_default()
1076                    .push(idx);
1077            }
1078        }
1079    }
1080
1081    /// Get count.
1082    pub fn len(&self) -> usize {
1083        self.employees.len()
1084    }
1085
1086    /// Check if empty.
1087    pub fn is_empty(&self) -> bool {
1088        self.employees.is_empty()
1089    }
1090}
1091
1092/// Type of employee lifecycle event recorded in the change history.
1093#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1094#[serde(rename_all = "snake_case")]
1095pub enum EmployeeEventType {
1096    /// Employee was hired (always the first event).
1097    #[default]
1098    Hired,
1099    /// Employee received a promotion to a higher job level.
1100    Promoted,
1101    /// Employee received a salary adjustment (increase or decrease).
1102    SalaryAdjustment,
1103    /// Employee was transferred to a different department or cost center.
1104    Transfer,
1105    /// Employee's employment was terminated.
1106    Terminated,
1107}
1108
1109impl std::fmt::Display for EmployeeEventType {
1110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1111        match self {
1112            Self::Hired => write!(f, "hired"),
1113            Self::Promoted => write!(f, "promoted"),
1114            Self::SalaryAdjustment => write!(f, "salary_adjustment"),
1115            Self::Transfer => write!(f, "transfer"),
1116            Self::Terminated => write!(f, "terminated"),
1117        }
1118    }
1119}
1120
1121/// A single entry in an employee's change history.
1122///
1123/// Captures point-in-time changes to an employee record (hire, promotion,
1124/// salary adjustment, transfer, or termination).  The `old_value` /
1125/// `new_value` fields carry a string representation of the changed attribute
1126/// (e.g., job level code, salary amount, department name) to keep the schema
1127/// generic and easy to consume from downstream analytics.
1128#[derive(Debug, Clone, Serialize, Deserialize)]
1129pub struct EmployeeChangeEvent {
1130    /// Employee this event belongs to.
1131    pub employee_id: String,
1132
1133    /// Calendar date on which the event was recorded in the HR system.
1134    pub event_date: chrono::NaiveDate,
1135
1136    /// Type of HR event.
1137    pub event_type: EmployeeEventType,
1138
1139    /// Previous value of the changed attribute (`None` for the Hired event).
1140    pub old_value: Option<String>,
1141
1142    /// New value of the changed attribute.
1143    pub new_value: Option<String>,
1144
1145    /// Date from which the change is effective (may differ from `event_date`
1146    /// for retroactive or future-dated transactions).
1147    pub effective_date: chrono::NaiveDate,
1148}
1149
1150impl EmployeeChangeEvent {
1151    /// Create a Hired event for a new employee.
1152    pub fn hired(employee_id: impl Into<String>, hire_date: chrono::NaiveDate) -> Self {
1153        Self {
1154            employee_id: employee_id.into(),
1155            event_date: hire_date,
1156            event_type: EmployeeEventType::Hired,
1157            old_value: None,
1158            new_value: Some("active".to_string()),
1159            effective_date: hire_date,
1160        }
1161    }
1162}
1163
1164#[cfg(test)]
1165#[allow(clippy::unwrap_used)]
1166mod tests {
1167    use super::*;
1168
1169    #[test]
1170    fn test_persona_properties() {
1171        assert!(UserPersona::JuniorAccountant.is_human());
1172        assert!(!UserPersona::AutomatedSystem.is_human());
1173        assert!(UserPersona::Controller.has_approval_authority());
1174        assert!(!UserPersona::JuniorAccountant.has_approval_authority());
1175    }
1176
1177    #[test]
1178    fn test_persona_display_snake_case() {
1179        assert_eq!(
1180            UserPersona::JuniorAccountant.to_string(),
1181            "junior_accountant"
1182        );
1183        assert_eq!(
1184            UserPersona::SeniorAccountant.to_string(),
1185            "senior_accountant"
1186        );
1187        assert_eq!(UserPersona::Controller.to_string(), "controller");
1188        assert_eq!(UserPersona::Manager.to_string(), "manager");
1189        assert_eq!(UserPersona::Executive.to_string(), "executive");
1190        assert_eq!(UserPersona::AutomatedSystem.to_string(), "automated_system");
1191        assert_eq!(UserPersona::ExternalAuditor.to_string(), "external_auditor");
1192        assert_eq!(UserPersona::FraudActor.to_string(), "fraud_actor");
1193
1194        // Verify no persona produces concatenated words (the bug this fixes)
1195        for persona in [
1196            UserPersona::JuniorAccountant,
1197            UserPersona::SeniorAccountant,
1198            UserPersona::Controller,
1199            UserPersona::Manager,
1200            UserPersona::Executive,
1201            UserPersona::AutomatedSystem,
1202            UserPersona::ExternalAuditor,
1203            UserPersona::FraudActor,
1204        ] {
1205            let s = persona.to_string();
1206            assert!(
1207                !s.contains(char::is_uppercase),
1208                "Display output '{}' should be all lowercase snake_case",
1209                s
1210            );
1211        }
1212    }
1213
1214    #[test]
1215    fn test_user_pool() {
1216        let pool = UserPool::generate_standard(&["1000".to_string()]);
1217        assert!(!pool.users.is_empty());
1218        assert!(!pool
1219            .get_users_by_persona(UserPersona::JuniorAccountant)
1220            .is_empty());
1221    }
1222
1223    #[test]
1224    fn test_job_level_hierarchy() {
1225        assert!(JobLevel::Executive.management_level() > JobLevel::Manager.management_level());
1226        assert!(JobLevel::Manager.is_manager());
1227        assert!(!JobLevel::Staff.is_manager());
1228    }
1229
1230    #[test]
1231    fn test_employee_creation() {
1232        let employee = Employee::new("E-001", "jsmith", "John", "Smith", "1000")
1233            .with_persona(UserPersona::Controller);
1234
1235        assert_eq!(employee.employee_id, "E-001");
1236        assert_eq!(employee.display_name, "John Smith");
1237        assert!(employee.can_approve_je);
1238        assert_eq!(employee.job_level, JobLevel::Manager);
1239    }
1240
1241    #[test]
1242    fn test_employee_approval_limits() {
1243        let employee = Employee::new("E-001", "test", "Test", "User", "1000")
1244            .with_approval_limit(Decimal::from(10000));
1245
1246        assert!(employee.can_approve_amount(Decimal::from(5000)));
1247        assert!(!employee.can_approve_amount(Decimal::from(15000)));
1248    }
1249
1250    #[test]
1251    fn test_employee_pool_hierarchy() {
1252        let mut pool = EmployeePool::new();
1253
1254        let cfo = Employee::new("E-001", "cfo", "Jane", "CEO", "1000")
1255            .with_persona(UserPersona::Executive);
1256
1257        let controller = Employee::new("E-002", "ctrl", "Bob", "Controller", "1000")
1258            .with_persona(UserPersona::Controller)
1259            .with_manager("E-001");
1260
1261        let accountant = Employee::new("E-003", "acc", "Alice", "Accountant", "1000")
1262            .with_persona(UserPersona::JuniorAccountant)
1263            .with_manager("E-002");
1264
1265        pool.add_employee(cfo);
1266        pool.add_employee(controller);
1267        pool.add_employee(accountant);
1268
1269        // Test getting direct reports
1270        let direct_reports = pool.get_direct_reports("E-001");
1271        assert_eq!(direct_reports.len(), 1);
1272        assert_eq!(direct_reports[0].employee_id, "E-002");
1273
1274        // Test reporting chain
1275        let chain = pool.get_reporting_chain("E-003");
1276        assert_eq!(chain.len(), 3);
1277        assert_eq!(chain[0].employee_id, "E-003");
1278        assert_eq!(chain[1].employee_id, "E-002");
1279        assert_eq!(chain[2].employee_id, "E-001");
1280    }
1281
1282    #[test]
1283    fn test_employee_to_user() {
1284        let employee = Employee::new("E-001", "jdoe", "John", "Doe", "1000")
1285            .with_persona(UserPersona::SeniorAccountant);
1286
1287        let user = employee.to_user();
1288
1289        assert_eq!(user.user_id, "jdoe");
1290        assert_eq!(user.persona, UserPersona::SeniorAccountant);
1291        assert!(user.is_active);
1292    }
1293
1294    #[test]
1295    fn test_employee_status() {
1296        assert!(EmployeeStatus::Active.can_transact());
1297        assert!(EmployeeStatus::Contractor.can_transact());
1298        assert!(!EmployeeStatus::Terminated.can_transact());
1299        assert!(!EmployeeStatus::OnLeave.can_transact());
1300    }
1301}