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    /// Build a `UserPool` from a slice of generated [`Employee`] records.
301    ///
302    /// Each employee contributes one user with the same `user_id`,
303    /// `persona`, `working_hours`, and display name.  This is the
304    /// canonical way to source user identities from master data so
305    /// that `JE.created_by` joins back to `employees.user_id`
306    /// (closes the v5.9.0 linkage gap that had JE creators using a
307    /// pool disjoint from the employees master).
308    pub fn from_employees(employees: &[Employee]) -> Self {
309        let mut pool = Self::new();
310        for emp in employees {
311            let display_name = if emp.first_name.is_empty() && emp.last_name.is_empty() {
312                emp.user_id.clone()
313            } else {
314                format!("{} {}", emp.first_name, emp.last_name)
315            };
316            let mut user = User::new(emp.user_id.clone(), display_name, emp.persona);
317            user.email = Some(emp.email.clone());
318            user.department = emp.department_id.clone();
319            user.cost_centers = emp.cost_center.iter().cloned().collect();
320            user.working_hours = emp.working_hours.clone();
321            pool.add_user(user);
322        }
323        pool
324    }
325
326    /// Add a user to the pool.
327    pub fn add_user(&mut self, user: User) {
328        let idx = self.users.len();
329        let persona = user.persona;
330        self.users.push(user);
331        self.persona_index.entry(persona).or_default().push(idx);
332    }
333
334    /// Get all users of a specific persona.
335    pub fn get_users_by_persona(&self, persona: UserPersona) -> Vec<&User> {
336        self.persona_index
337            .get(&persona)
338            .map(|indices| indices.iter().map(|&i| &self.users[i]).collect())
339            .unwrap_or_default()
340    }
341
342    /// Get a random user of a specific persona.
343    pub fn get_random_user(&self, persona: UserPersona, rng: &mut impl rand::Rng) -> Option<&User> {
344        use rand::seq::IndexedRandom;
345        self.get_users_by_persona(persona).choose(rng).copied()
346    }
347
348    /// Rebuild the persona index (call after deserialization).
349    pub fn rebuild_index(&mut self) {
350        self.persona_index.clear();
351        for (idx, user) in self.users.iter().enumerate() {
352            self.persona_index
353                .entry(user.persona)
354                .or_default()
355                .push(idx);
356        }
357    }
358
359    /// Generate a standard user pool with typical distribution.
360    pub fn generate_standard(company_codes: &[String]) -> Self {
361        let mut pool = Self::new();
362
363        // Junior accountants (many)
364        for i in 0..10 {
365            let mut user = User::new(
366                User::generate_username(UserPersona::JuniorAccountant, i),
367                format!("Junior Accountant {}", i + 1),
368                UserPersona::JuniorAccountant,
369            );
370            user.company_codes = company_codes.to_vec();
371            pool.add_user(user);
372        }
373
374        // Senior accountants
375        for i in 0..5 {
376            let mut user = User::new(
377                User::generate_username(UserPersona::SeniorAccountant, i),
378                format!("Senior Accountant {}", i + 1),
379                UserPersona::SeniorAccountant,
380            );
381            user.company_codes = company_codes.to_vec();
382            pool.add_user(user);
383        }
384
385        // Controllers
386        for i in 0..2 {
387            let mut user = User::new(
388                User::generate_username(UserPersona::Controller, i),
389                format!("Controller {}", i + 1),
390                UserPersona::Controller,
391            );
392            user.company_codes = company_codes.to_vec();
393            pool.add_user(user);
394        }
395
396        // Managers
397        for i in 0..3 {
398            let mut user = User::new(
399                User::generate_username(UserPersona::Manager, i),
400                format!("Finance Manager {}", i + 1),
401                UserPersona::Manager,
402            );
403            user.company_codes = company_codes.to_vec();
404            pool.add_user(user);
405        }
406
407        // Automated systems (many)
408        for i in 0..20 {
409            let mut user = User::new(
410                User::generate_username(UserPersona::AutomatedSystem, i),
411                format!("Batch Job {}", i + 1),
412                UserPersona::AutomatedSystem,
413            );
414            user.company_codes = company_codes.to_vec();
415            pool.add_user(user);
416        }
417
418        pool
419    }
420}
421
422impl Default for UserPool {
423    fn default() -> Self {
424        Self::new()
425    }
426}
427
428/// Employee job level in the organization hierarchy.
429#[derive(
430    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
431)]
432#[serde(rename_all = "snake_case")]
433pub enum JobLevel {
434    /// Individual contributor
435    #[default]
436    Staff,
437    /// Senior individual contributor
438    Senior,
439    /// Lead/principal
440    Lead,
441    /// First-line manager
442    Supervisor,
443    /// Middle management
444    Manager,
445    /// Senior manager / director
446    Director,
447    /// VP level
448    VicePresident,
449    /// C-level executive
450    Executive,
451}
452
453impl JobLevel {
454    /// Get the management level (0 = IC, higher = more senior).
455    pub fn management_level(&self) -> u8 {
456        match self {
457            Self::Staff => 0,
458            Self::Senior => 0,
459            Self::Lead => 1,
460            Self::Supervisor => 2,
461            Self::Manager => 3,
462            Self::Director => 4,
463            Self::VicePresident => 5,
464            Self::Executive => 6,
465        }
466    }
467
468    /// Check if this is a management position.
469    pub fn is_manager(&self) -> bool {
470        self.management_level() >= 2
471    }
472
473    /// Get typical direct reports range.
474    pub fn typical_direct_reports(&self) -> (u16, u16) {
475        match self {
476            Self::Staff | Self::Senior => (0, 0),
477            Self::Lead => (0, 3),
478            Self::Supervisor => (3, 10),
479            Self::Manager => (5, 15),
480            Self::Director => (3, 8),
481            Self::VicePresident => (3, 6),
482            Self::Executive => (5, 12),
483        }
484    }
485}
486
487/// Employee status in HR system.
488#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
489#[serde(rename_all = "snake_case")]
490pub enum EmployeeStatus {
491    /// Active employee
492    #[default]
493    Active,
494    /// On leave (sabbatical, parental, etc.)
495    OnLeave,
496    /// Suspended
497    Suspended,
498    /// Notice period
499    NoticePeriod,
500    /// Terminated
501    Terminated,
502    /// Retired
503    Retired,
504    /// Contractor (not full employee)
505    Contractor,
506}
507
508impl EmployeeStatus {
509    /// Check if employee can perform transactions.
510    pub fn can_transact(&self) -> bool {
511        matches!(self, Self::Active | Self::Contractor)
512    }
513
514    /// Check if employee is active in some capacity.
515    pub fn is_active(&self) -> bool {
516        !matches!(self, Self::Terminated | Self::Retired)
517    }
518}
519
520/// System role for access control.
521#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
522#[serde(rename_all = "snake_case")]
523pub enum SystemRole {
524    /// View-only access
525    Viewer,
526    /// Can create documents
527    Creator,
528    /// Can approve documents
529    Approver,
530    /// Can release payments
531    PaymentReleaser,
532    /// Can perform bank transactions
533    BankProcessor,
534    /// Can post journal entries
535    JournalPoster,
536    /// Can perform period close activities
537    PeriodClose,
538    /// System administrator
539    Admin,
540    /// AP Accountant
541    ApAccountant,
542    /// AR Accountant
543    ArAccountant,
544    /// Buyer / Procurement
545    Buyer,
546    /// Executive / Management
547    Executive,
548    /// Financial Analyst
549    FinancialAnalyst,
550    /// General Accountant
551    GeneralAccountant,
552    /// Custom role with name
553    Custom(String),
554}
555
556/// Transaction code authorization.
557#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct TransactionCodeAuth {
559    /// Transaction code (e.g., "FB01", "ME21N")
560    pub tcode: String,
561    /// Activity type (create, change, display, delete)
562    pub activity: ActivityType,
563    /// Is authorization active?
564    pub active: bool,
565}
566
567/// Activity types for authorization.
568#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
569#[serde(rename_all = "snake_case")]
570pub enum ActivityType {
571    /// Display only
572    #[default]
573    Display,
574    /// Create new
575    Create,
576    /// Change existing
577    Change,
578    /// Delete
579    Delete,
580    /// Execute (for reports)
581    Execute,
582}
583
584/// Employee master data with organizational hierarchy.
585#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct Employee {
587    /// Employee ID (e.g., "E-001234")
588    pub employee_id: String,
589
590    /// User ID (login name, links to User)
591    pub user_id: String,
592
593    /// Display name
594    pub display_name: String,
595
596    /// First name
597    pub first_name: String,
598
599    /// Last name
600    pub last_name: String,
601
602    /// Email address
603    pub email: String,
604
605    /// Persona classification
606    pub persona: UserPersona,
607
608    /// Job level
609    pub job_level: JobLevel,
610
611    /// Job title
612    pub job_title: String,
613
614    /// Department ID
615    pub department_id: Option<String>,
616
617    /// Cost center
618    pub cost_center: Option<String>,
619
620    /// Manager's employee ID (for hierarchy)
621    pub manager_id: Option<String>,
622
623    /// Direct reports (employee IDs)
624    pub direct_reports: Vec<String>,
625
626    /// Employment status
627    pub status: EmployeeStatus,
628
629    /// Company code
630    pub company_code: String,
631
632    /// Working hours pattern
633    pub working_hours: WorkingHoursPattern,
634
635    /// Authorized company codes
636    pub authorized_company_codes: Vec<String>,
637
638    /// Authorized cost centers
639    pub authorized_cost_centers: Vec<String>,
640
641    /// Approval limit (monetary threshold)
642    pub approval_limit: Decimal,
643
644    /// Can approve purchase requisitions
645    pub can_approve_pr: bool,
646
647    /// Can approve purchase orders
648    pub can_approve_po: bool,
649
650    /// Can approve invoices
651    pub can_approve_invoice: bool,
652
653    /// Can approve journal entries
654    pub can_approve_je: bool,
655
656    /// Can release payments
657    pub can_release_payment: bool,
658
659    /// System roles
660    pub system_roles: Vec<SystemRole>,
661
662    /// Authorized transaction codes
663    pub transaction_codes: Vec<TransactionCodeAuth>,
664
665    /// Hire date
666    pub hire_date: Option<chrono::NaiveDate>,
667
668    /// Termination date (if applicable)
669    pub termination_date: Option<chrono::NaiveDate>,
670
671    /// Location / plant
672    pub location: Option<String>,
673
674    /// Is this an intercompany employee (works for multiple entities)?
675    pub is_shared_services: bool,
676
677    /// Phone number
678    pub phone: Option<String>,
679
680    /// Annual base salary in the company's local currency.
681    ///
682    /// Used by the payroll generator to compute monthly gross pay
683    /// (`base_salary / 12`) instead of a hardcoded default.
684    #[serde(with = "crate::serde_decimal")]
685    pub base_salary: rust_decimal::Decimal,
686}
687
688impl Employee {
689    /// Create a new employee.
690    pub fn new(
691        employee_id: impl Into<String>,
692        user_id: impl Into<String>,
693        first_name: impl Into<String>,
694        last_name: impl Into<String>,
695        company_code: impl Into<String>,
696    ) -> Self {
697        let first = first_name.into();
698        let last = last_name.into();
699        let uid = user_id.into();
700        let display_name = format!("{first} {last}");
701        let email = format!(
702            "{}.{}@company.com",
703            first.to_lowercase(),
704            last.to_lowercase()
705        );
706
707        Self {
708            employee_id: employee_id.into(),
709            user_id: uid,
710            display_name,
711            first_name: first,
712            last_name: last,
713            email,
714            persona: UserPersona::JuniorAccountant,
715            job_level: JobLevel::Staff,
716            job_title: "Staff Accountant".to_string(),
717            department_id: None,
718            cost_center: None,
719            manager_id: None,
720            direct_reports: Vec::new(),
721            status: EmployeeStatus::Active,
722            company_code: company_code.into(),
723            working_hours: WorkingHoursPattern::default(),
724            authorized_company_codes: Vec::new(),
725            authorized_cost_centers: Vec::new(),
726            approval_limit: Decimal::ZERO,
727            can_approve_pr: false,
728            can_approve_po: false,
729            can_approve_invoice: false,
730            can_approve_je: false,
731            can_release_payment: false,
732            system_roles: Vec::new(),
733            transaction_codes: Vec::new(),
734            hire_date: None,
735            termination_date: None,
736            location: None,
737            is_shared_services: false,
738            phone: None,
739            base_salary: Decimal::ZERO,
740        }
741    }
742
743    /// Set persona and adjust defaults accordingly.
744    pub fn with_persona(mut self, persona: UserPersona) -> Self {
745        self.persona = persona;
746
747        // Adjust job level and approval limit based on persona
748        match persona {
749            UserPersona::JuniorAccountant => {
750                self.job_level = JobLevel::Staff;
751                self.job_title = "Junior Accountant".to_string();
752                self.approval_limit = Decimal::from(1000);
753            }
754            UserPersona::SeniorAccountant => {
755                self.job_level = JobLevel::Senior;
756                self.job_title = "Senior Accountant".to_string();
757                self.approval_limit = Decimal::from(10000);
758                self.can_approve_je = true;
759            }
760            UserPersona::Controller => {
761                self.job_level = JobLevel::Manager;
762                self.job_title = "Controller".to_string();
763                self.approval_limit = Decimal::from(100000);
764                self.can_approve_pr = true;
765                self.can_approve_po = true;
766                self.can_approve_invoice = true;
767                self.can_approve_je = true;
768            }
769            UserPersona::Manager => {
770                self.job_level = JobLevel::Director;
771                self.job_title = "Finance Director".to_string();
772                self.approval_limit = Decimal::from(500000);
773                self.can_approve_pr = true;
774                self.can_approve_po = true;
775                self.can_approve_invoice = true;
776                self.can_approve_je = true;
777                self.can_release_payment = true;
778            }
779            UserPersona::Executive => {
780                self.job_level = JobLevel::Executive;
781                self.job_title = "CFO".to_string();
782                self.approval_limit = Decimal::from(999_999_999); // Unlimited
783                self.can_approve_pr = true;
784                self.can_approve_po = true;
785                self.can_approve_invoice = true;
786                self.can_approve_je = true;
787                self.can_release_payment = true;
788            }
789            UserPersona::AutomatedSystem => {
790                self.job_level = JobLevel::Staff;
791                self.job_title = "Batch Process".to_string();
792                self.working_hours = WorkingHoursPattern::batch_processing();
793            }
794            UserPersona::ExternalAuditor => {
795                self.job_level = JobLevel::Senior;
796                self.job_title = "External Auditor".to_string();
797                self.approval_limit = Decimal::ZERO;
798            }
799            UserPersona::FraudActor => {
800                self.job_level = JobLevel::Staff;
801                self.job_title = "Staff Accountant".to_string();
802                self.approval_limit = Decimal::from(10000);
803            }
804        }
805        self
806    }
807
808    /// Set job level.
809    pub fn with_job_level(mut self, level: JobLevel) -> Self {
810        self.job_level = level;
811        self
812    }
813
814    /// Set job title.
815    pub fn with_job_title(mut self, title: impl Into<String>) -> Self {
816        self.job_title = title.into();
817        self
818    }
819
820    /// Set manager.
821    pub fn with_manager(mut self, manager_id: impl Into<String>) -> Self {
822        self.manager_id = Some(manager_id.into());
823        self
824    }
825
826    /// Set department.
827    pub fn with_department(mut self, department_id: impl Into<String>) -> Self {
828        self.department_id = Some(department_id.into());
829        self
830    }
831
832    /// Set cost center.
833    pub fn with_cost_center(mut self, cost_center: impl Into<String>) -> Self {
834        self.cost_center = Some(cost_center.into());
835        self
836    }
837
838    /// Set approval limit.
839    pub fn with_approval_limit(mut self, limit: Decimal) -> Self {
840        self.approval_limit = limit;
841        self
842    }
843
844    /// Add authorized company code.
845    pub fn with_authorized_company(mut self, company_code: impl Into<String>) -> Self {
846        self.authorized_company_codes.push(company_code.into());
847        self
848    }
849
850    /// Add system role.
851    pub fn with_role(mut self, role: SystemRole) -> Self {
852        self.system_roles.push(role);
853        self
854    }
855
856    /// Set hire date.
857    pub fn with_hire_date(mut self, date: chrono::NaiveDate) -> Self {
858        self.hire_date = Some(date);
859        self
860    }
861
862    /// Add a direct report.
863    pub fn add_direct_report(&mut self, employee_id: String) {
864        if !self.direct_reports.contains(&employee_id) {
865            self.direct_reports.push(employee_id);
866        }
867    }
868
869    /// Check if employee can approve an amount.
870    pub fn can_approve_amount(&self, amount: Decimal) -> bool {
871        if self.status != EmployeeStatus::Active {
872            return false;
873        }
874        amount <= self.approval_limit
875    }
876
877    /// Check if employee can approve in a company code.
878    pub fn can_approve_in_company(&self, company_code: &str) -> bool {
879        if self.status != EmployeeStatus::Active {
880            return false;
881        }
882        self.authorized_company_codes.is_empty()
883            || self
884                .authorized_company_codes
885                .iter()
886                .any(|c| c == company_code)
887    }
888
889    /// Check if employee has a specific role.
890    pub fn has_role(&self, role: &SystemRole) -> bool {
891        self.system_roles.contains(role)
892    }
893
894    /// Get the depth in the org hierarchy (0 = top).
895    pub fn hierarchy_depth(&self) -> u8 {
896        // This would need the full employee registry to compute properly
897        // For now, estimate based on job level
898        match self.job_level {
899            JobLevel::Executive => 0,
900            JobLevel::VicePresident => 1,
901            JobLevel::Director => 2,
902            JobLevel::Manager => 3,
903            JobLevel::Supervisor => 4,
904            JobLevel::Lead => 5,
905            JobLevel::Senior => 6,
906            JobLevel::Staff => 7,
907        }
908    }
909
910    /// Convert to a User (for backward compatibility).
911    pub fn to_user(&self) -> User {
912        let mut user = User::new(
913            self.user_id.clone(),
914            self.display_name.clone(),
915            self.persona,
916        );
917        user.email = Some(self.email.clone());
918        user.department = self.department_id.clone();
919        user.working_hours = self.working_hours.clone();
920        user.company_codes = self.authorized_company_codes.clone();
921        user.cost_centers = self.authorized_cost_centers.clone();
922        user.is_active = self.status.can_transact();
923        user.start_date = self.hire_date;
924        user.end_date = self.termination_date;
925        user
926    }
927}
928
929/// Pool of employees with organizational hierarchy support.
930#[derive(Debug, Clone, Default, Serialize, Deserialize)]
931pub struct EmployeePool {
932    /// All employees
933    pub employees: Vec<Employee>,
934    /// Index by employee ID
935    #[serde(skip)]
936    id_index: std::collections::HashMap<String, usize>,
937    /// Index by manager ID (for finding direct reports)
938    #[serde(skip)]
939    manager_index: std::collections::HashMap<String, Vec<usize>>,
940    /// Index by persona
941    #[serde(skip)]
942    persona_index: std::collections::HashMap<UserPersona, Vec<usize>>,
943    /// Index by department
944    #[serde(skip)]
945    department_index: std::collections::HashMap<String, Vec<usize>>,
946}
947
948impl EmployeePool {
949    /// Create a new empty employee pool.
950    pub fn new() -> Self {
951        Self::default()
952    }
953
954    /// Add an employee to the pool.
955    pub fn add_employee(&mut self, employee: Employee) {
956        let idx = self.employees.len();
957
958        self.id_index.insert(employee.employee_id.clone(), idx);
959
960        if let Some(ref mgr_id) = employee.manager_id {
961            self.manager_index
962                .entry(mgr_id.clone())
963                .or_default()
964                .push(idx);
965        }
966
967        self.persona_index
968            .entry(employee.persona)
969            .or_default()
970            .push(idx);
971
972        if let Some(ref dept_id) = employee.department_id {
973            self.department_index
974                .entry(dept_id.clone())
975                .or_default()
976                .push(idx);
977        }
978
979        self.employees.push(employee);
980    }
981
982    /// Get employee by ID.
983    pub fn get_by_id(&self, employee_id: &str) -> Option<&Employee> {
984        self.id_index
985            .get(employee_id)
986            .map(|&idx| &self.employees[idx])
987    }
988
989    /// Get mutable employee by ID.
990    pub fn get_by_id_mut(&mut self, employee_id: &str) -> Option<&mut Employee> {
991        self.id_index
992            .get(employee_id)
993            .copied()
994            .map(|idx| &mut self.employees[idx])
995    }
996
997    /// Get direct reports of a manager.
998    pub fn get_direct_reports(&self, manager_id: &str) -> Vec<&Employee> {
999        self.manager_index
1000            .get(manager_id)
1001            .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
1002            .unwrap_or_default()
1003    }
1004
1005    /// Get employees by persona.
1006    pub fn get_by_persona(&self, persona: UserPersona) -> Vec<&Employee> {
1007        self.persona_index
1008            .get(&persona)
1009            .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
1010            .unwrap_or_default()
1011    }
1012
1013    /// Get employees by department.
1014    pub fn get_by_department(&self, department_id: &str) -> Vec<&Employee> {
1015        self.department_index
1016            .get(department_id)
1017            .map(|indices| indices.iter().map(|&i| &self.employees[i]).collect())
1018            .unwrap_or_default()
1019    }
1020
1021    /// Get a random employee with approval authority.
1022    pub fn get_random_approver(&self, rng: &mut impl rand::Rng) -> Option<&Employee> {
1023        use rand::seq::IndexedRandom;
1024
1025        let approvers: Vec<_> = self
1026            .employees
1027            .iter()
1028            .filter(|e| e.persona.has_approval_authority() && e.status == EmployeeStatus::Active)
1029            .collect();
1030
1031        approvers.choose(rng).copied()
1032    }
1033
1034    /// Get approver for a specific amount.
1035    pub fn get_approver_for_amount(
1036        &self,
1037        amount: Decimal,
1038        rng: &mut impl rand::Rng,
1039    ) -> Option<&Employee> {
1040        use rand::seq::IndexedRandom;
1041
1042        let approvers: Vec<_> = self
1043            .employees
1044            .iter()
1045            .filter(|e| e.can_approve_amount(amount))
1046            .collect();
1047
1048        approvers.choose(rng).copied()
1049    }
1050
1051    /// Get all managers (employees with direct reports).
1052    pub fn get_managers(&self) -> Vec<&Employee> {
1053        self.employees
1054            .iter()
1055            .filter(|e| !e.direct_reports.is_empty() || e.job_level.is_manager())
1056            .collect()
1057    }
1058
1059    /// Get org chart path from employee to top.
1060    pub fn get_reporting_chain(&self, employee_id: &str) -> Vec<&Employee> {
1061        let mut chain = Vec::new();
1062        let mut current_id = employee_id.to_string();
1063
1064        while let Some(emp) = self.get_by_id(&current_id) {
1065            chain.push(emp);
1066            if let Some(ref mgr_id) = emp.manager_id {
1067                current_id = mgr_id.clone();
1068            } else {
1069                break;
1070            }
1071        }
1072
1073        chain
1074    }
1075
1076    /// Rebuild indices after deserialization.
1077    pub fn rebuild_indices(&mut self) {
1078        self.id_index.clear();
1079        self.manager_index.clear();
1080        self.persona_index.clear();
1081        self.department_index.clear();
1082
1083        for (idx, employee) in self.employees.iter().enumerate() {
1084            self.id_index.insert(employee.employee_id.clone(), idx);
1085
1086            if let Some(ref mgr_id) = employee.manager_id {
1087                self.manager_index
1088                    .entry(mgr_id.clone())
1089                    .or_default()
1090                    .push(idx);
1091            }
1092
1093            self.persona_index
1094                .entry(employee.persona)
1095                .or_default()
1096                .push(idx);
1097
1098            if let Some(ref dept_id) = employee.department_id {
1099                self.department_index
1100                    .entry(dept_id.clone())
1101                    .or_default()
1102                    .push(idx);
1103            }
1104        }
1105    }
1106
1107    /// Get count.
1108    pub fn len(&self) -> usize {
1109        self.employees.len()
1110    }
1111
1112    /// Check if empty.
1113    pub fn is_empty(&self) -> bool {
1114        self.employees.is_empty()
1115    }
1116}
1117
1118/// Type of employee lifecycle event recorded in the change history.
1119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1120#[serde(rename_all = "snake_case")]
1121pub enum EmployeeEventType {
1122    /// Employee was hired (always the first event).
1123    #[default]
1124    Hired,
1125    /// Employee received a promotion to a higher job level.
1126    Promoted,
1127    /// Employee received a salary adjustment (increase or decrease).
1128    SalaryAdjustment,
1129    /// Employee was transferred to a different department or cost center.
1130    Transfer,
1131    /// Employee's employment was terminated.
1132    Terminated,
1133}
1134
1135impl std::fmt::Display for EmployeeEventType {
1136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1137        match self {
1138            Self::Hired => write!(f, "hired"),
1139            Self::Promoted => write!(f, "promoted"),
1140            Self::SalaryAdjustment => write!(f, "salary_adjustment"),
1141            Self::Transfer => write!(f, "transfer"),
1142            Self::Terminated => write!(f, "terminated"),
1143        }
1144    }
1145}
1146
1147/// A single entry in an employee's change history.
1148///
1149/// Captures point-in-time changes to an employee record (hire, promotion,
1150/// salary adjustment, transfer, or termination).  The `old_value` /
1151/// `new_value` fields carry a string representation of the changed attribute
1152/// (e.g., job level code, salary amount, department name) to keep the schema
1153/// generic and easy to consume from downstream analytics.
1154#[derive(Debug, Clone, Serialize, Deserialize)]
1155pub struct EmployeeChangeEvent {
1156    /// Employee this event belongs to.
1157    pub employee_id: String,
1158
1159    /// Calendar date on which the event was recorded in the HR system.
1160    pub event_date: chrono::NaiveDate,
1161
1162    /// Type of HR event.
1163    pub event_type: EmployeeEventType,
1164
1165    /// Previous value of the changed attribute (`None` for the Hired event).
1166    pub old_value: Option<String>,
1167
1168    /// New value of the changed attribute.
1169    pub new_value: Option<String>,
1170
1171    /// Date from which the change is effective (may differ from `event_date`
1172    /// for retroactive or future-dated transactions).
1173    pub effective_date: chrono::NaiveDate,
1174}
1175
1176impl EmployeeChangeEvent {
1177    /// Create a Hired event for a new employee.
1178    pub fn hired(employee_id: impl Into<String>, hire_date: chrono::NaiveDate) -> Self {
1179        Self {
1180            employee_id: employee_id.into(),
1181            event_date: hire_date,
1182            event_type: EmployeeEventType::Hired,
1183            old_value: None,
1184            new_value: Some("active".to_string()),
1185            effective_date: hire_date,
1186        }
1187    }
1188}
1189
1190#[cfg(test)]
1191mod tests {
1192    use super::*;
1193
1194    #[test]
1195    fn test_persona_properties() {
1196        assert!(UserPersona::JuniorAccountant.is_human());
1197        assert!(!UserPersona::AutomatedSystem.is_human());
1198        assert!(UserPersona::Controller.has_approval_authority());
1199        assert!(!UserPersona::JuniorAccountant.has_approval_authority());
1200    }
1201
1202    #[test]
1203    fn test_persona_display_snake_case() {
1204        assert_eq!(
1205            UserPersona::JuniorAccountant.to_string(),
1206            "junior_accountant"
1207        );
1208        assert_eq!(
1209            UserPersona::SeniorAccountant.to_string(),
1210            "senior_accountant"
1211        );
1212        assert_eq!(UserPersona::Controller.to_string(), "controller");
1213        assert_eq!(UserPersona::Manager.to_string(), "manager");
1214        assert_eq!(UserPersona::Executive.to_string(), "executive");
1215        assert_eq!(UserPersona::AutomatedSystem.to_string(), "automated_system");
1216        assert_eq!(UserPersona::ExternalAuditor.to_string(), "external_auditor");
1217        assert_eq!(UserPersona::FraudActor.to_string(), "fraud_actor");
1218
1219        // Verify no persona produces concatenated words (the bug this fixes)
1220        for persona in [
1221            UserPersona::JuniorAccountant,
1222            UserPersona::SeniorAccountant,
1223            UserPersona::Controller,
1224            UserPersona::Manager,
1225            UserPersona::Executive,
1226            UserPersona::AutomatedSystem,
1227            UserPersona::ExternalAuditor,
1228            UserPersona::FraudActor,
1229        ] {
1230            let s = persona.to_string();
1231            assert!(
1232                !s.contains(char::is_uppercase),
1233                "Display output '{}' should be all lowercase snake_case",
1234                s
1235            );
1236        }
1237    }
1238
1239    #[test]
1240    fn test_user_pool() {
1241        let pool = UserPool::generate_standard(&["1000".to_string()]);
1242        assert!(!pool.users.is_empty());
1243        assert!(!pool
1244            .get_users_by_persona(UserPersona::JuniorAccountant)
1245            .is_empty());
1246    }
1247
1248    #[test]
1249    fn test_job_level_hierarchy() {
1250        assert!(JobLevel::Executive.management_level() > JobLevel::Manager.management_level());
1251        assert!(JobLevel::Manager.is_manager());
1252        assert!(!JobLevel::Staff.is_manager());
1253    }
1254
1255    #[test]
1256    fn test_employee_creation() {
1257        let employee = Employee::new("E-001", "jsmith", "John", "Smith", "1000")
1258            .with_persona(UserPersona::Controller);
1259
1260        assert_eq!(employee.employee_id, "E-001");
1261        assert_eq!(employee.display_name, "John Smith");
1262        assert!(employee.can_approve_je);
1263        assert_eq!(employee.job_level, JobLevel::Manager);
1264    }
1265
1266    #[test]
1267    fn test_employee_approval_limits() {
1268        let employee = Employee::new("E-001", "test", "Test", "User", "1000")
1269            .with_approval_limit(Decimal::from(10000));
1270
1271        assert!(employee.can_approve_amount(Decimal::from(5000)));
1272        assert!(!employee.can_approve_amount(Decimal::from(15000)));
1273    }
1274
1275    #[test]
1276    fn test_employee_pool_hierarchy() {
1277        let mut pool = EmployeePool::new();
1278
1279        let cfo = Employee::new("E-001", "cfo", "Jane", "CEO", "1000")
1280            .with_persona(UserPersona::Executive);
1281
1282        let controller = Employee::new("E-002", "ctrl", "Bob", "Controller", "1000")
1283            .with_persona(UserPersona::Controller)
1284            .with_manager("E-001");
1285
1286        let accountant = Employee::new("E-003", "acc", "Alice", "Accountant", "1000")
1287            .with_persona(UserPersona::JuniorAccountant)
1288            .with_manager("E-002");
1289
1290        pool.add_employee(cfo);
1291        pool.add_employee(controller);
1292        pool.add_employee(accountant);
1293
1294        // Test getting direct reports
1295        let direct_reports = pool.get_direct_reports("E-001");
1296        assert_eq!(direct_reports.len(), 1);
1297        assert_eq!(direct_reports[0].employee_id, "E-002");
1298
1299        // Test reporting chain
1300        let chain = pool.get_reporting_chain("E-003");
1301        assert_eq!(chain.len(), 3);
1302        assert_eq!(chain[0].employee_id, "E-003");
1303        assert_eq!(chain[1].employee_id, "E-002");
1304        assert_eq!(chain[2].employee_id, "E-001");
1305    }
1306
1307    #[test]
1308    fn test_employee_to_user() {
1309        let employee = Employee::new("E-001", "jdoe", "John", "Doe", "1000")
1310            .with_persona(UserPersona::SeniorAccountant);
1311
1312        let user = employee.to_user();
1313
1314        assert_eq!(user.user_id, "jdoe");
1315        assert_eq!(user.persona, UserPersona::SeniorAccountant);
1316        assert!(user.is_active);
1317    }
1318
1319    #[test]
1320    fn test_employee_status() {
1321        assert!(EmployeeStatus::Active.can_transact());
1322        assert!(EmployeeStatus::Contractor.can_transact());
1323        assert!(!EmployeeStatus::Terminated.can_transact());
1324        assert!(!EmployeeStatus::OnLeave.can_transact());
1325    }
1326}