1use 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#[derive(Debug, Clone)]
16pub struct EmployeeGeneratorConfig {
17 pub job_level_distribution: Vec<(JobLevel, f64)>,
19 pub approval_limits: Vec<(JobLevel, Decimal)>,
21 pub culture_distribution: Vec<(NameCulture, f64)>,
23 pub email_domain: String,
25 pub leave_rate: f64,
27 pub termination_rate: f64,
29 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#[derive(Debug, Clone)]
71pub struct DepartmentDefinition {
72 pub code: String,
74 pub name: String,
76 pub cost_center: String,
78 pub headcount: usize,
80 pub system_roles: Vec<SystemRole>,
82 pub transaction_codes: Vec<String>,
84}
85
86impl DepartmentDefinition {
87 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 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 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 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 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 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
188pub struct EmployeeGenerator {
190 rng: ChaCha8Rng,
191 seed: u64,
192 config: EmployeeGeneratorConfig,
193 name_generator: MultiCultureNameGenerator,
194 employee_counter: usize,
195}
196
197impl EmployeeGenerator {
198 pub fn new(seed: u64) -> Self {
200 Self::with_config(seed, EmployeeGeneratorConfig::default())
201 }
202
203 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 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 employee.email = email;
245 employee.job_level = job_level;
246
247 employee.department_id = Some(department.name.clone());
249 employee.cost_center = Some(department.cost_center.clone());
250
251 employee.hire_date = Some(hire_date);
253
254 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 if !department.system_roles.is_empty() {
275 let role_idx = self.rng.gen_range(0..department.system_roles.len());
276 employee
277 .system_roles
278 .push(department.system_roles[role_idx].clone());
279 }
280
281 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 employee.status = self.select_status();
292 if employee.status == EmployeeStatus::Terminated {
293 employee.termination_date =
294 Some(hire_date + chrono::Duration::days(self.rng.gen_range(365..1825) as i64));
295 }
296
297 employee
298 }
299
300 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 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 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.gen_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 for _ in 1..department.headcount {
357 let hire_date =
358 start_date + chrono::Duration::days(self.rng.gen_range(0..=days_range) as i64);
359 let mut employee = self.generate_employee(company_code, department, hire_date);
360
361 employee.manager_id = Some(dept_head_id.clone());
363
364 pool.add_employee(employee);
365 }
366
367 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 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 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 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 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 for mut employee in dept_pool.employees {
428 if employee.manager_id.is_none() {
429 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 self.update_direct_reports(&mut pool);
442
443 pool
444 }
445
446 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 fn update_direct_reports(&self, pool: &mut EmployeePool) {
485 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 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 fn select_job_level(&mut self) -> JobLevel {
508 let roll: f64 = self.rng.gen();
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 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 fn select_status(&mut self) -> EmployeeStatus {
533 let roll: f64 = self.rng.gen();
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 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 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 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 let executives: Vec<_> = pool
610 .employees
611 .iter()
612 .filter(|e| e.job_level == JobLevel::Executive)
613 .collect();
614 assert!(executives.len() >= 3); 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 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 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}