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.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 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 for mut employee in dept_pool.employees {
419 if employee.manager_id.is_none() {
420 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 self.update_direct_reports(&mut pool);
433
434 pool
435 }
436
437 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 fn update_direct_reports(&self, pool: &mut EmployeePool) {
476 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 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 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 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 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 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 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 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 let executives: Vec<_> = pool
600 .employees
601 .iter()
602 .filter(|e| e.job_level == JobLevel::Executive)
603 .collect();
604 assert!(executives.len() >= 3); 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 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 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}