datasynth_generators/master_data/
employee_generator.rs

1//! Employee generator with org hierarchy and approval limits.
2
3use chrono::NaiveDate;
4use datasynth_core::models::{
5    Employee, EmployeePool, EmployeeStatus, JobLevel, SystemRole, TransactionCodeAuth,
6};
7use datasynth_core::templates::{MultiCultureNameGenerator, NameCulture};
8use rand::prelude::*;
9use rand_chacha::ChaCha8Rng;
10use rust_decimal::Decimal;
11
12/// Configuration for employee generation.
13#[derive(Debug, Clone)]
14pub struct EmployeeGeneratorConfig {
15    /// Distribution of job levels (level, probability)
16    pub job_level_distribution: Vec<(JobLevel, f64)>,
17    /// Approval limits by job level (level, limit)
18    pub approval_limits: Vec<(JobLevel, Decimal)>,
19    /// Name culture distribution
20    pub culture_distribution: Vec<(NameCulture, f64)>,
21    /// Email domain
22    pub email_domain: String,
23    /// Probability of employee being on leave
24    pub leave_rate: f64,
25    /// Probability of employee being terminated
26    pub termination_rate: f64,
27    /// Manager span of control (min, max direct reports)
28    pub span_of_control: (usize, usize),
29}
30
31impl Default for EmployeeGeneratorConfig {
32    fn default() -> Self {
33        Self {
34            job_level_distribution: vec![
35                (JobLevel::Staff, 0.50),
36                (JobLevel::Senior, 0.25),
37                (JobLevel::Manager, 0.12),
38                (JobLevel::Director, 0.08),
39                (JobLevel::VicePresident, 0.04),
40                (JobLevel::Executive, 0.01),
41            ],
42            approval_limits: vec![
43                (JobLevel::Staff, Decimal::from(1_000)),
44                (JobLevel::Senior, Decimal::from(5_000)),
45                (JobLevel::Manager, Decimal::from(25_000)),
46                (JobLevel::Director, Decimal::from(100_000)),
47                (JobLevel::VicePresident, Decimal::from(500_000)),
48                (JobLevel::Executive, Decimal::from(10_000_000)),
49            ],
50            culture_distribution: vec![
51                (NameCulture::WesternUs, 0.40),
52                (NameCulture::Hispanic, 0.20),
53                (NameCulture::German, 0.10),
54                (NameCulture::French, 0.05),
55                (NameCulture::Chinese, 0.10),
56                (NameCulture::Japanese, 0.05),
57                (NameCulture::Indian, 0.10),
58            ],
59            email_domain: "company.com".to_string(),
60            leave_rate: 0.02,
61            termination_rate: 0.01,
62            span_of_control: (3, 8),
63        }
64    }
65}
66
67/// Department definitions for employee assignment.
68#[derive(Debug, Clone)]
69pub struct DepartmentDefinition {
70    /// Department code
71    pub code: String,
72    /// Department name
73    pub name: String,
74    /// Cost center
75    pub cost_center: String,
76    /// Target headcount
77    pub headcount: usize,
78    /// System roles for this department
79    pub system_roles: Vec<SystemRole>,
80    /// Transaction codes for this department
81    pub transaction_codes: Vec<String>,
82}
83
84impl DepartmentDefinition {
85    /// Finance department.
86    pub fn finance(company_code: &str) -> Self {
87        Self {
88            code: format!("{}-FIN", company_code),
89            name: "Finance".to_string(),
90            cost_center: format!("CC-{}-FIN", company_code),
91            headcount: 15,
92            system_roles: vec![
93                SystemRole::ApAccountant,
94                SystemRole::ArAccountant,
95                SystemRole::GeneralAccountant,
96                SystemRole::FinancialAnalyst,
97            ],
98            transaction_codes: vec![
99                "FB01".to_string(),
100                "FB02".to_string(),
101                "FB03".to_string(),
102                "F-28".to_string(),
103                "F-53".to_string(),
104                "FBL1N".to_string(),
105            ],
106        }
107    }
108
109    /// Procurement department.
110    pub fn procurement(company_code: &str) -> Self {
111        Self {
112            code: format!("{}-PROC", company_code),
113            name: "Procurement".to_string(),
114            cost_center: format!("CC-{}-PROC", company_code),
115            headcount: 10,
116            system_roles: vec![SystemRole::Buyer, SystemRole::Approver],
117            transaction_codes: vec![
118                "ME21N".to_string(),
119                "ME22N".to_string(),
120                "ME23N".to_string(),
121                "MIGO".to_string(),
122                "ME2M".to_string(),
123            ],
124        }
125    }
126
127    /// Sales department.
128    pub fn sales(company_code: &str) -> Self {
129        Self {
130            code: format!("{}-SALES", company_code),
131            name: "Sales".to_string(),
132            cost_center: format!("CC-{}-SALES", company_code),
133            headcount: 20,
134            system_roles: vec![SystemRole::Creator, SystemRole::Approver],
135            transaction_codes: vec![
136                "VA01".to_string(),
137                "VA02".to_string(),
138                "VA03".to_string(),
139                "VL01N".to_string(),
140                "VF01".to_string(),
141            ],
142        }
143    }
144
145    /// Warehouse/Logistics department.
146    pub fn warehouse(company_code: &str) -> Self {
147        Self {
148            code: format!("{}-WH", company_code),
149            name: "Warehouse".to_string(),
150            cost_center: format!("CC-{}-WH", company_code),
151            headcount: 12,
152            system_roles: vec![SystemRole::Creator, SystemRole::Viewer],
153            transaction_codes: vec![
154                "MIGO".to_string(),
155                "MB51".to_string(),
156                "MMBE".to_string(),
157                "LT01".to_string(),
158            ],
159        }
160    }
161
162    /// IT department.
163    pub fn it(company_code: &str) -> Self {
164        Self {
165            code: format!("{}-IT", company_code),
166            name: "Information Technology".to_string(),
167            cost_center: format!("CC-{}-IT", company_code),
168            headcount: 8,
169            system_roles: vec![SystemRole::Admin],
170            transaction_codes: vec!["SU01".to_string(), "PFCG".to_string(), "SM21".to_string()],
171        }
172    }
173
174    /// Standard departments for a company.
175    pub fn standard_departments(company_code: &str) -> Vec<Self> {
176        vec![
177            Self::finance(company_code),
178            Self::procurement(company_code),
179            Self::sales(company_code),
180            Self::warehouse(company_code),
181            Self::it(company_code),
182        ]
183    }
184}
185
186/// Generator for employee master data with org hierarchy.
187pub struct EmployeeGenerator {
188    rng: ChaCha8Rng,
189    seed: u64,
190    config: EmployeeGeneratorConfig,
191    name_generator: MultiCultureNameGenerator,
192    employee_counter: usize,
193}
194
195impl EmployeeGenerator {
196    /// Create a new employee generator.
197    pub fn new(seed: u64) -> Self {
198        Self::with_config(seed, EmployeeGeneratorConfig::default())
199    }
200
201    /// Create a new employee generator with custom configuration.
202    pub fn with_config(seed: u64, config: EmployeeGeneratorConfig) -> Self {
203        let mut name_gen =
204            MultiCultureNameGenerator::with_distribution(config.culture_distribution.clone());
205        name_gen.set_email_domain(&config.email_domain);
206
207        Self {
208            rng: ChaCha8Rng::seed_from_u64(seed),
209            seed,
210            name_generator: name_gen,
211            config,
212            employee_counter: 0,
213        }
214    }
215
216    /// Generate a single employee.
217    pub fn generate_employee(
218        &mut self,
219        company_code: &str,
220        department: &DepartmentDefinition,
221        hire_date: NaiveDate,
222    ) -> Employee {
223        self.employee_counter += 1;
224
225        let name = self.name_generator.generate_name(&mut self.rng);
226        let employee_id = format!("EMP-{}-{:06}", company_code, self.employee_counter);
227        let user_id = format!("u{:06}", self.employee_counter);
228        let email = self.name_generator.generate_email(&name);
229
230        let job_level = self.select_job_level();
231        let approval_limit = self.get_approval_limit(&job_level);
232
233        let mut employee = Employee::new(
234            employee_id,
235            user_id,
236            name.first_name.clone(),
237            name.last_name.clone(),
238            company_code.to_string(),
239        );
240
241        // Set additional fields
242        employee.email = email;
243        employee.job_level = job_level;
244
245        // Set department info
246        employee.department_id = Some(department.name.clone());
247        employee.cost_center = Some(department.cost_center.clone());
248
249        // Set dates
250        employee.hire_date = Some(hire_date);
251
252        // Set approval limits based on job level
253        employee.approval_limit = approval_limit;
254        employee.can_approve_pr = matches!(
255            job_level,
256            JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
257        );
258        employee.can_approve_po = matches!(
259            job_level,
260            JobLevel::Senior
261                | JobLevel::Manager
262                | JobLevel::Director
263                | JobLevel::VicePresident
264                | JobLevel::Executive
265        );
266        employee.can_approve_je = matches!(
267            job_level,
268            JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
269        );
270
271        // Assign system roles
272        if !department.system_roles.is_empty() {
273            let role_idx = self.rng.gen_range(0..department.system_roles.len());
274            employee
275                .system_roles
276                .push(department.system_roles[role_idx].clone());
277        }
278
279        // Assign transaction codes
280        for tcode in &department.transaction_codes {
281            employee.transaction_codes.push(TransactionCodeAuth {
282                tcode: tcode.clone(),
283                activity: datasynth_core::models::ActivityType::Create,
284                active: true,
285            });
286        }
287
288        // Set status
289        employee.status = self.select_status();
290        if employee.status == EmployeeStatus::Terminated {
291            employee.termination_date =
292                Some(hire_date + chrono::Duration::days(self.rng.gen_range(365..1825) as i64));
293        }
294
295        employee
296    }
297
298    /// Generate an employee with specific job level.
299    pub fn generate_employee_with_level(
300        &mut self,
301        company_code: &str,
302        department: &DepartmentDefinition,
303        job_level: JobLevel,
304        hire_date: NaiveDate,
305    ) -> Employee {
306        let mut employee = self.generate_employee(company_code, department, hire_date);
307        employee.job_level = job_level;
308        employee.approval_limit = self.get_approval_limit(&job_level);
309        employee.can_approve_pr = matches!(
310            job_level,
311            JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
312        );
313        employee.can_approve_po = matches!(
314            job_level,
315            JobLevel::Senior
316                | JobLevel::Manager
317                | JobLevel::Director
318                | JobLevel::VicePresident
319                | JobLevel::Executive
320        );
321        employee.can_approve_je = matches!(
322            job_level,
323            JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
324        );
325        employee
326    }
327
328    /// Generate an employee pool for a department.
329    pub fn generate_department_pool(
330        &mut self,
331        company_code: &str,
332        department: &DepartmentDefinition,
333        hire_date_range: (NaiveDate, NaiveDate),
334    ) -> EmployeePool {
335        let mut pool = EmployeePool::new();
336
337        let (start_date, end_date) = hire_date_range;
338        let days_range = (end_date - start_date).num_days() as u64;
339
340        // Generate department head (Director or Manager)
341        let head_level = if department.headcount >= 15 {
342            JobLevel::Director
343        } else {
344            JobLevel::Manager
345        };
346        let hire_date =
347            start_date + chrono::Duration::days(self.rng.gen_range(0..=days_range / 2) as i64);
348        let dept_head =
349            self.generate_employee_with_level(company_code, department, head_level, hire_date);
350        let dept_head_id = dept_head.employee_id.clone();
351        pool.add_employee(dept_head);
352
353        // Generate remaining employees
354        for _ in 1..department.headcount {
355            let hire_date =
356                start_date + chrono::Duration::days(self.rng.gen_range(0..=days_range) as i64);
357            let mut employee = self.generate_employee(company_code, department, hire_date);
358
359            // Assign manager (department head)
360            employee.manager_id = Some(dept_head_id.clone());
361
362            pool.add_employee(employee);
363        }
364
365        // Collect direct reports first to avoid borrow conflict
366        let direct_reports: Vec<String> = pool
367            .employees
368            .iter()
369            .filter(|e| e.manager_id.as_ref() == Some(&dept_head_id))
370            .map(|e| e.employee_id.clone())
371            .collect();
372
373        // Update direct reports for department head
374        if let Some(head) = pool
375            .employees
376            .iter_mut()
377            .find(|e| e.employee_id == dept_head_id)
378        {
379            head.direct_reports = direct_reports;
380        }
381
382        pool
383    }
384
385    /// Generate a full company employee pool with hierarchy.
386    pub fn generate_company_pool(
387        &mut self,
388        company_code: &str,
389        hire_date_range: (NaiveDate, NaiveDate),
390    ) -> EmployeePool {
391        let mut pool = EmployeePool::new();
392
393        let (start_date, end_date) = hire_date_range;
394        let _days_range = (end_date - start_date).num_days() as u64;
395
396        // First, generate C-level executives
397        let ceo = self.generate_executive(company_code, "CEO", start_date);
398        let ceo_id = ceo.employee_id.clone();
399        pool.add_employee(ceo);
400
401        let cfo = self.generate_executive(company_code, "CFO", start_date);
402        let cfo_id = cfo.employee_id.clone();
403        pool.employees.last_mut().unwrap().manager_id = Some(ceo_id.clone());
404        pool.add_employee(cfo);
405
406        let coo = self.generate_executive(company_code, "COO", start_date);
407        let coo_id = coo.employee_id.clone();
408        pool.employees.last_mut().unwrap().manager_id = Some(ceo_id.clone());
409        pool.add_employee(coo);
410
411        // Generate department pools
412        let departments = DepartmentDefinition::standard_departments(company_code);
413
414        for dept in &departments {
415            let dept_pool = self.generate_department_pool(company_code, dept, hire_date_range);
416
417            // Assign department heads to executives
418            for mut employee in dept_pool.employees {
419                if employee.manager_id.is_none() {
420                    // Department head reports to CFO (finance) or COO (others)
421                    employee.manager_id = if dept.name == "Finance" {
422                        Some(cfo_id.clone())
423                    } else {
424                        Some(coo_id.clone())
425                    };
426                }
427                pool.add_employee(employee);
428            }
429        }
430
431        // Update executive direct reports
432        self.update_direct_reports(&mut pool);
433
434        pool
435    }
436
437    /// Generate an executive employee.
438    fn generate_executive(
439        &mut self,
440        company_code: &str,
441        title: &str,
442        hire_date: NaiveDate,
443    ) -> Employee {
444        self.employee_counter += 1;
445
446        let name = self.name_generator.generate_name(&mut self.rng);
447        let employee_id = format!("EMP-{}-{:06}", company_code, self.employee_counter);
448        let user_id = format!("exec{:04}", self.employee_counter);
449        let email = self.name_generator.generate_email(&name);
450
451        let mut employee = Employee::new(
452            employee_id,
453            user_id,
454            name.first_name.clone(),
455            name.last_name.clone(),
456            company_code.to_string(),
457        );
458
459        employee.email = email;
460        employee.job_level = JobLevel::Executive;
461        employee.job_title = title.to_string();
462        employee.department_id = Some("Executive".to_string());
463        employee.cost_center = Some(format!("CC-{}-EXEC", company_code));
464        employee.hire_date = Some(hire_date);
465        employee.approval_limit = Decimal::from(100_000_000);
466        employee.can_approve_pr = true;
467        employee.can_approve_po = true;
468        employee.can_approve_je = true;
469        employee.system_roles.push(SystemRole::Executive);
470
471        employee
472    }
473
474    /// Update direct reports for all managers.
475    fn update_direct_reports(&self, pool: &mut EmployeePool) {
476        // Collect manager -> direct reports mapping
477        let mut direct_reports_map: std::collections::HashMap<String, Vec<String>> =
478            std::collections::HashMap::new();
479
480        for employee in &pool.employees {
481            if let Some(manager_id) = &employee.manager_id {
482                direct_reports_map
483                    .entry(manager_id.clone())
484                    .or_default()
485                    .push(employee.employee_id.clone());
486            }
487        }
488
489        // Update each manager's direct reports
490        for employee in &mut pool.employees {
491            if let Some(reports) = direct_reports_map.get(&employee.employee_id) {
492                employee.direct_reports = reports.clone();
493            }
494        }
495    }
496
497    /// Select job level based on distribution.
498    fn select_job_level(&mut self) -> JobLevel {
499        let roll: f64 = self.rng.gen();
500        let mut cumulative = 0.0;
501
502        for (level, prob) in &self.config.job_level_distribution {
503            cumulative += prob;
504            if roll < cumulative {
505                return *level;
506            }
507        }
508
509        JobLevel::Staff
510    }
511
512    /// Get approval limit for job level.
513    fn get_approval_limit(&self, job_level: &JobLevel) -> Decimal {
514        for (level, limit) in &self.config.approval_limits {
515            if level == job_level {
516                return *limit;
517            }
518        }
519        Decimal::from(1_000)
520    }
521
522    /// Select employee status.
523    fn select_status(&mut self) -> EmployeeStatus {
524        let roll: f64 = self.rng.gen();
525
526        if roll < self.config.termination_rate {
527            EmployeeStatus::Terminated
528        } else if roll < self.config.termination_rate + self.config.leave_rate {
529            EmployeeStatus::OnLeave
530        } else {
531            EmployeeStatus::Active
532        }
533    }
534
535    /// Reset the generator.
536    pub fn reset(&mut self) {
537        self.rng = ChaCha8Rng::seed_from_u64(self.seed);
538        self.employee_counter = 0;
539    }
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545
546    #[test]
547    fn test_employee_generation() {
548        let mut gen = EmployeeGenerator::new(42);
549        let dept = DepartmentDefinition::finance("1000");
550        let employee =
551            gen.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
552
553        assert!(!employee.employee_id.is_empty());
554        assert!(!employee.display_name.is_empty());
555        assert!(!employee.email.is_empty());
556        assert!(employee.approval_limit > Decimal::ZERO);
557    }
558
559    #[test]
560    fn test_department_pool() {
561        let mut gen = EmployeeGenerator::new(42);
562        let dept = DepartmentDefinition::finance("1000");
563        let pool = gen.generate_department_pool(
564            "1000",
565            &dept,
566            (
567                NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
568                NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
569            ),
570        );
571
572        assert_eq!(pool.employees.len(), dept.headcount);
573
574        // Should have at least one manager
575        let managers: Vec<_> = pool
576            .employees
577            .iter()
578            .filter(|e| matches!(e.job_level, JobLevel::Manager | JobLevel::Director))
579            .collect();
580        assert!(!managers.is_empty());
581
582        // Department head should have direct reports
583        let dept_head = managers.first().unwrap();
584        assert!(!dept_head.direct_reports.is_empty());
585    }
586
587    #[test]
588    fn test_company_pool() {
589        let mut gen = EmployeeGenerator::new(42);
590        let pool = gen.generate_company_pool(
591            "1000",
592            (
593                NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
594                NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
595            ),
596        );
597
598        // Should have executives
599        let executives: Vec<_> = pool
600            .employees
601            .iter()
602            .filter(|e| e.job_level == JobLevel::Executive)
603            .collect();
604        assert!(executives.len() >= 3); // CEO, CFO, COO
605
606        // Executives should have direct reports
607        let cfo = pool.employees.iter().find(|e| e.job_title == "CFO");
608        assert!(cfo.is_some());
609    }
610
611    #[test]
612    fn test_hierarchy() {
613        let mut gen = EmployeeGenerator::new(42);
614        let pool = gen.generate_company_pool(
615            "1000",
616            (
617                NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
618                NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
619            ),
620        );
621
622        // Every non-CEO employee should have a manager
623        let non_ceo_without_manager: Vec<_> = pool
624            .employees
625            .iter()
626            .filter(|e| e.job_title != "CEO")
627            .filter(|e| e.manager_id.is_none())
628            .collect();
629
630        // Most employees should have managers (some edge cases may exist)
631        assert!(non_ceo_without_manager.len() <= 1);
632    }
633
634    #[test]
635    fn test_deterministic_generation() {
636        let mut gen1 = EmployeeGenerator::new(42);
637        let mut gen2 = EmployeeGenerator::new(42);
638
639        let dept = DepartmentDefinition::finance("1000");
640        let employee1 =
641            gen1.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
642        let employee2 =
643            gen2.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
644
645        assert_eq!(employee1.employee_id, employee2.employee_id);
646        assert_eq!(employee1.display_name, employee2.display_name);
647    }
648
649    #[test]
650    fn test_approval_limits() {
651        let mut gen = EmployeeGenerator::new(42);
652        let dept = DepartmentDefinition::finance("1000");
653
654        let staff = gen.generate_employee_with_level(
655            "1000",
656            &dept,
657            JobLevel::Staff,
658            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
659        );
660        let manager = gen.generate_employee_with_level(
661            "1000",
662            &dept,
663            JobLevel::Manager,
664            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
665        );
666
667        assert!(manager.approval_limit > staff.approval_limit);
668        assert!(!staff.can_approve_pr);
669        assert!(manager.can_approve_pr);
670    }
671}