Skip to main content

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