1use 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#[derive(Debug, Clone)]
14pub struct EmployeeGeneratorConfig {
15 pub job_level_distribution: Vec<(JobLevel, f64)>,
17 pub approval_limits: Vec<(JobLevel, Decimal)>,
19 pub culture_distribution: Vec<(NameCulture, f64)>,
21 pub email_domain: String,
23 pub leave_rate: f64,
25 pub termination_rate: f64,
27 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#[derive(Debug, Clone)]
69pub struct DepartmentDefinition {
70 pub code: String,
72 pub name: String,
74 pub cost_center: String,
76 pub headcount: usize,
78 pub system_roles: Vec<SystemRole>,
80 pub transaction_codes: Vec<String>,
82}
83
84impl DepartmentDefinition {
85 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 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 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 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 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 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
186pub struct EmployeeGenerator {
188 rng: ChaCha8Rng,
189 seed: u64,
190 config: EmployeeGeneratorConfig,
191 name_generator: MultiCultureNameGenerator,
192 employee_counter: usize,
193}
194
195impl EmployeeGenerator {
196 pub fn new(seed: u64) -> Self {
198 Self::with_config(seed, EmployeeGeneratorConfig::default())
199 }
200
201 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 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 employee.email = email;
243 employee.job_level = job_level;
244
245 employee.department_id = Some(department.name.clone());
247 employee.cost_center = Some(department.cost_center.clone());
248
249 employee.hire_date = Some(hire_date);
251
252 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 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 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 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 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 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 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 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 employee.manager_id = Some(dept_head_id.clone());
361
362 pool.add_employee(employee);
363 }
364
365 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 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 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 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
404 .last_mut()
405 .expect("just added CEO")
406 .manager_id = Some(ceo_id.clone());
407 pool.add_employee(cfo);
408
409 let coo = self.generate_executive(company_code, "COO", start_date);
410 let coo_id = coo.employee_id.clone();
411 pool.employees
412 .last_mut()
413 .expect("just added CFO")
414 .manager_id = Some(ceo_id.clone());
415 pool.add_employee(coo);
416
417 let departments = DepartmentDefinition::standard_departments(company_code);
419
420 for dept in &departments {
421 let dept_pool = self.generate_department_pool(company_code, dept, hire_date_range);
422
423 for mut employee in dept_pool.employees {
425 if employee.manager_id.is_none() {
426 employee.manager_id = if dept.name == "Finance" {
428 Some(cfo_id.clone())
429 } else {
430 Some(coo_id.clone())
431 };
432 }
433 pool.add_employee(employee);
434 }
435 }
436
437 self.update_direct_reports(&mut pool);
439
440 pool
441 }
442
443 fn generate_executive(
445 &mut self,
446 company_code: &str,
447 title: &str,
448 hire_date: NaiveDate,
449 ) -> Employee {
450 self.employee_counter += 1;
451
452 let name = self.name_generator.generate_name(&mut self.rng);
453 let employee_id = format!("EMP-{}-{:06}", company_code, self.employee_counter);
454 let user_id = format!("exec{:04}", self.employee_counter);
455 let email = self.name_generator.generate_email(&name);
456
457 let mut employee = Employee::new(
458 employee_id,
459 user_id,
460 name.first_name.clone(),
461 name.last_name.clone(),
462 company_code.to_string(),
463 );
464
465 employee.email = email;
466 employee.job_level = JobLevel::Executive;
467 employee.job_title = title.to_string();
468 employee.department_id = Some("Executive".to_string());
469 employee.cost_center = Some(format!("CC-{}-EXEC", company_code));
470 employee.hire_date = Some(hire_date);
471 employee.approval_limit = Decimal::from(100_000_000);
472 employee.can_approve_pr = true;
473 employee.can_approve_po = true;
474 employee.can_approve_je = true;
475 employee.system_roles.push(SystemRole::Executive);
476
477 employee
478 }
479
480 fn update_direct_reports(&self, pool: &mut EmployeePool) {
482 let mut direct_reports_map: std::collections::HashMap<String, Vec<String>> =
484 std::collections::HashMap::new();
485
486 for employee in &pool.employees {
487 if let Some(manager_id) = &employee.manager_id {
488 direct_reports_map
489 .entry(manager_id.clone())
490 .or_default()
491 .push(employee.employee_id.clone());
492 }
493 }
494
495 for employee in &mut pool.employees {
497 if let Some(reports) = direct_reports_map.get(&employee.employee_id) {
498 employee.direct_reports = reports.clone();
499 }
500 }
501 }
502
503 fn select_job_level(&mut self) -> JobLevel {
505 let roll: f64 = self.rng.gen();
506 let mut cumulative = 0.0;
507
508 for (level, prob) in &self.config.job_level_distribution {
509 cumulative += prob;
510 if roll < cumulative {
511 return *level;
512 }
513 }
514
515 JobLevel::Staff
516 }
517
518 fn get_approval_limit(&self, job_level: &JobLevel) -> Decimal {
520 for (level, limit) in &self.config.approval_limits {
521 if level == job_level {
522 return *limit;
523 }
524 }
525 Decimal::from(1_000)
526 }
527
528 fn select_status(&mut self) -> EmployeeStatus {
530 let roll: f64 = self.rng.gen();
531
532 if roll < self.config.termination_rate {
533 EmployeeStatus::Terminated
534 } else if roll < self.config.termination_rate + self.config.leave_rate {
535 EmployeeStatus::OnLeave
536 } else {
537 EmployeeStatus::Active
538 }
539 }
540
541 pub fn reset(&mut self) {
543 self.rng = ChaCha8Rng::seed_from_u64(self.seed);
544 self.employee_counter = 0;
545 }
546}
547
548#[cfg(test)]
549#[allow(clippy::unwrap_used)]
550mod tests {
551 use super::*;
552
553 #[test]
554 fn test_employee_generation() {
555 let mut gen = EmployeeGenerator::new(42);
556 let dept = DepartmentDefinition::finance("1000");
557 let employee =
558 gen.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
559
560 assert!(!employee.employee_id.is_empty());
561 assert!(!employee.display_name.is_empty());
562 assert!(!employee.email.is_empty());
563 assert!(employee.approval_limit > Decimal::ZERO);
564 }
565
566 #[test]
567 fn test_department_pool() {
568 let mut gen = EmployeeGenerator::new(42);
569 let dept = DepartmentDefinition::finance("1000");
570 let pool = gen.generate_department_pool(
571 "1000",
572 &dept,
573 (
574 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
575 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
576 ),
577 );
578
579 assert_eq!(pool.employees.len(), dept.headcount);
580
581 let managers: Vec<_> = pool
583 .employees
584 .iter()
585 .filter(|e| matches!(e.job_level, JobLevel::Manager | JobLevel::Director))
586 .collect();
587 assert!(!managers.is_empty());
588
589 let dept_head = managers.first().unwrap();
591 assert!(!dept_head.direct_reports.is_empty());
592 }
593
594 #[test]
595 fn test_company_pool() {
596 let mut gen = EmployeeGenerator::new(42);
597 let pool = gen.generate_company_pool(
598 "1000",
599 (
600 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
601 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
602 ),
603 );
604
605 let executives: Vec<_> = pool
607 .employees
608 .iter()
609 .filter(|e| e.job_level == JobLevel::Executive)
610 .collect();
611 assert!(executives.len() >= 3); let cfo = pool.employees.iter().find(|e| e.job_title == "CFO");
615 assert!(cfo.is_some());
616 }
617
618 #[test]
619 fn test_hierarchy() {
620 let mut gen = EmployeeGenerator::new(42);
621 let pool = gen.generate_company_pool(
622 "1000",
623 (
624 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
625 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
626 ),
627 );
628
629 let non_ceo_without_manager: Vec<_> = pool
631 .employees
632 .iter()
633 .filter(|e| e.job_title != "CEO")
634 .filter(|e| e.manager_id.is_none())
635 .collect();
636
637 assert!(non_ceo_without_manager.len() <= 1);
639 }
640
641 #[test]
642 fn test_deterministic_generation() {
643 let mut gen1 = EmployeeGenerator::new(42);
644 let mut gen2 = EmployeeGenerator::new(42);
645
646 let dept = DepartmentDefinition::finance("1000");
647 let employee1 =
648 gen1.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
649 let employee2 =
650 gen2.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
651
652 assert_eq!(employee1.employee_id, employee2.employee_id);
653 assert_eq!(employee1.display_name, employee2.display_name);
654 }
655
656 #[test]
657 fn test_approval_limits() {
658 let mut gen = EmployeeGenerator::new(42);
659 let dept = DepartmentDefinition::finance("1000");
660
661 let staff = gen.generate_employee_with_level(
662 "1000",
663 &dept,
664 JobLevel::Staff,
665 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
666 );
667 let manager = gen.generate_employee_with_level(
668 "1000",
669 &dept,
670 JobLevel::Manager,
671 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
672 );
673
674 assert!(manager.approval_limit > staff.approval_limit);
675 assert!(!staff.can_approve_pr);
676 assert!(manager.can_approve_pr);
677 }
678}