1use chrono::NaiveDate;
4use datasynth_core::models::{
5 Employee, EmployeeChangeEvent, EmployeeEventType, EmployeePool, EmployeeStatus, JobLevel,
6 SystemRole, TransactionCodeAuth,
7};
8use datasynth_core::templates::{MultiCultureNameGenerator, NameCulture};
9use datasynth_core::utils::seeded_rng;
10use rand::prelude::*;
11use rand_chacha::ChaCha8Rng;
12use rust_decimal::Decimal;
13use tracing::debug;
14
15#[derive(Debug, Clone)]
17pub struct EmployeeGeneratorConfig {
18 pub job_level_distribution: Vec<(JobLevel, f64)>,
20 pub approval_limits: Vec<(JobLevel, Decimal)>,
22 pub culture_distribution: Vec<(NameCulture, f64)>,
24 pub email_domain: String,
26 pub leave_rate: f64,
28 pub termination_rate: f64,
30 pub span_of_control: (usize, usize),
32}
33
34impl Default for EmployeeGeneratorConfig {
35 fn default() -> Self {
36 Self {
37 job_level_distribution: vec![
38 (JobLevel::Staff, 0.50),
39 (JobLevel::Senior, 0.25),
40 (JobLevel::Manager, 0.12),
41 (JobLevel::Director, 0.08),
42 (JobLevel::VicePresident, 0.04),
43 (JobLevel::Executive, 0.01),
44 ],
45 approval_limits: vec![
46 (JobLevel::Staff, Decimal::from(1_000)),
47 (JobLevel::Senior, Decimal::from(5_000)),
48 (JobLevel::Manager, Decimal::from(25_000)),
49 (JobLevel::Director, Decimal::from(100_000)),
50 (JobLevel::VicePresident, Decimal::from(500_000)),
51 (JobLevel::Executive, Decimal::from(10_000_000)),
52 ],
53 culture_distribution: vec![
54 (NameCulture::WesternUs, 0.40),
55 (NameCulture::Hispanic, 0.20),
56 (NameCulture::German, 0.10),
57 (NameCulture::French, 0.05),
58 (NameCulture::Chinese, 0.10),
59 (NameCulture::Japanese, 0.05),
60 (NameCulture::Indian, 0.10),
61 ],
62 email_domain: "company.com".to_string(),
63 leave_rate: 0.02,
64 termination_rate: 0.01,
65 span_of_control: (3, 8),
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct DepartmentDefinition {
73 pub code: String,
75 pub name: String,
77 pub cost_center: String,
79 pub headcount: usize,
81 pub system_roles: Vec<SystemRole>,
83 pub transaction_codes: Vec<String>,
85}
86
87impl DepartmentDefinition {
88 pub fn finance(company_code: &str) -> Self {
90 Self {
91 code: format!("{company_code}-FIN"),
92 name: "Finance".to_string(),
93 cost_center: format!("CC-{company_code}-FIN"),
94 headcount: 15,
95 system_roles: vec![
96 SystemRole::ApAccountant,
97 SystemRole::ArAccountant,
98 SystemRole::GeneralAccountant,
99 SystemRole::FinancialAnalyst,
100 ],
101 transaction_codes: vec![
102 "FB01".to_string(),
103 "FB02".to_string(),
104 "FB03".to_string(),
105 "F-28".to_string(),
106 "F-53".to_string(),
107 "FBL1N".to_string(),
108 ],
109 }
110 }
111
112 pub fn procurement(company_code: &str) -> Self {
114 Self {
115 code: format!("{company_code}-PROC"),
116 name: "Procurement".to_string(),
117 cost_center: format!("CC-{company_code}-PROC"),
118 headcount: 10,
119 system_roles: vec![SystemRole::Buyer, SystemRole::Approver],
120 transaction_codes: vec![
121 "ME21N".to_string(),
122 "ME22N".to_string(),
123 "ME23N".to_string(),
124 "MIGO".to_string(),
125 "ME2M".to_string(),
126 ],
127 }
128 }
129
130 pub fn sales(company_code: &str) -> Self {
132 Self {
133 code: format!("{company_code}-SALES"),
134 name: "Sales".to_string(),
135 cost_center: format!("CC-{company_code}-SALES"),
136 headcount: 20,
137 system_roles: vec![SystemRole::Creator, SystemRole::Approver],
138 transaction_codes: vec![
139 "VA01".to_string(),
140 "VA02".to_string(),
141 "VA03".to_string(),
142 "VL01N".to_string(),
143 "VF01".to_string(),
144 ],
145 }
146 }
147
148 pub fn warehouse(company_code: &str) -> Self {
150 Self {
151 code: format!("{company_code}-WH"),
152 name: "Warehouse".to_string(),
153 cost_center: format!("CC-{company_code}-WH"),
154 headcount: 12,
155 system_roles: vec![SystemRole::Creator, SystemRole::Viewer],
156 transaction_codes: vec![
157 "MIGO".to_string(),
158 "MB51".to_string(),
159 "MMBE".to_string(),
160 "LT01".to_string(),
161 ],
162 }
163 }
164
165 pub fn it(company_code: &str) -> Self {
167 Self {
168 code: format!("{company_code}-IT"),
169 name: "Information Technology".to_string(),
170 cost_center: format!("CC-{company_code}-IT"),
171 headcount: 8,
172 system_roles: vec![SystemRole::Admin],
173 transaction_codes: vec!["SU01".to_string(), "PFCG".to_string(), "SM21".to_string()],
174 }
175 }
176
177 pub fn standard_departments(company_code: &str) -> Vec<Self> {
179 vec![
180 Self::finance(company_code),
181 Self::procurement(company_code),
182 Self::sales(company_code),
183 Self::warehouse(company_code),
184 Self::it(company_code),
185 ]
186 }
187}
188
189pub struct EmployeeGenerator {
191 rng: ChaCha8Rng,
192 seed: u64,
193 config: EmployeeGeneratorConfig,
194 name_generator: MultiCultureNameGenerator,
195 employee_counter: usize,
196 country_pack: Option<datasynth_core::CountryPack>,
198 template_provider: Option<datasynth_core::templates::SharedTemplateProvider>,
200}
201
202impl EmployeeGenerator {
203 pub fn new(seed: u64) -> Self {
205 Self::with_config(seed, EmployeeGeneratorConfig::default())
206 }
207
208 pub fn with_config(seed: u64, config: EmployeeGeneratorConfig) -> Self {
210 let mut name_gen =
211 MultiCultureNameGenerator::with_distribution(config.culture_distribution.clone());
212 name_gen.set_email_domain(&config.email_domain);
213
214 Self {
215 rng: seeded_rng(seed, 0),
216 seed,
217 name_generator: name_gen,
218 config,
219 employee_counter: 0,
220 country_pack: None,
221 template_provider: None,
222 }
223 }
224
225 pub fn set_template_provider(
228 &mut self,
229 provider: datasynth_core::templates::SharedTemplateProvider,
230 ) {
231 self.template_provider = Some(provider);
232 }
233
234 fn resolve_department_name(&mut self, department_code: &str, factory_name: &str) -> String {
239 if let Some(ref provider) = self.template_provider {
240 if let Some(custom) = provider.get_department_name(department_code, &mut self.rng) {
241 return custom;
242 }
243 }
244 factory_name.to_string()
245 }
246
247 pub fn set_country_pack(&mut self, pack: datasynth_core::CountryPack) {
254 self.country_pack = Some(pack);
255 }
256
257 pub fn generate_employee(
259 &mut self,
260 company_code: &str,
261 department: &DepartmentDefinition,
262 hire_date: NaiveDate,
263 ) -> Employee {
264 self.employee_counter += 1;
265
266 let name = self.name_generator.generate_name(&mut self.rng);
267 let employee_id = format!("EMP-{}-{:06}", company_code, self.employee_counter);
268 let user_id = name.to_user_id(self.employee_counter);
273 let email = self.name_generator.generate_email(&name);
274
275 let job_level = self.select_job_level();
276 let approval_limit = self.get_approval_limit(&job_level);
277
278 let mut employee = Employee::new(
279 employee_id,
280 user_id,
281 name.first_name.clone(),
282 name.last_name.clone(),
283 company_code.to_string(),
284 );
285
286 employee.email = email;
288 employee.job_level = job_level;
289
290 employee.department_id = Some(department.name.clone());
292 employee.cost_center = Some(department.cost_center.clone());
293
294 employee.hire_date = Some(hire_date);
296
297 employee.approval_limit = approval_limit;
299 employee.can_approve_pr = matches!(
300 job_level,
301 JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
302 );
303 employee.can_approve_po = matches!(
304 job_level,
305 JobLevel::Senior
306 | JobLevel::Manager
307 | JobLevel::Director
308 | JobLevel::VicePresident
309 | JobLevel::Executive
310 );
311 employee.can_approve_je = matches!(
312 job_level,
313 JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
314 );
315
316 if !department.system_roles.is_empty() {
318 let role_idx = self.rng.random_range(0..department.system_roles.len());
319 employee
320 .system_roles
321 .push(department.system_roles[role_idx].clone());
322 }
323
324 for tcode in &department.transaction_codes {
326 employee.transaction_codes.push(TransactionCodeAuth {
327 tcode: tcode.clone(),
328 activity: datasynth_core::models::ActivityType::Create,
329 active: true,
330 });
331 }
332
333 let (salary_min, salary_max): (u64, u64) = match job_level {
336 JobLevel::Staff => (40_000, 60_000),
337 JobLevel::Senior => (60_000, 90_000),
338 JobLevel::Lead => (75_000, 105_000),
339 JobLevel::Supervisor => (70_000, 100_000),
340 JobLevel::Manager => (80_000, 120_000),
341 JobLevel::Director => (100_000, 160_000),
342 JobLevel::VicePresident => (130_000, 200_000),
343 JobLevel::Executive => (150_000, 250_000),
344 };
345 let salary_range = salary_max - salary_min;
346 let salary_raw =
347 salary_min + (self.rng.random::<f64>() * salary_range as f64).round() as u64;
348 employee.base_salary = Decimal::from(salary_raw);
349
350 employee.status = self.select_status();
352 if employee.status == EmployeeStatus::Terminated {
353 employee.termination_date =
354 Some(hire_date + chrono::Duration::days(self.rng.random_range(365..1825) as i64));
355 }
356
357 employee
358 }
359
360 pub fn generate_employee_with_level(
362 &mut self,
363 company_code: &str,
364 department: &DepartmentDefinition,
365 job_level: JobLevel,
366 hire_date: NaiveDate,
367 ) -> Employee {
368 let mut employee = self.generate_employee(company_code, department, hire_date);
369 employee.job_level = job_level;
370 employee.approval_limit = self.get_approval_limit(&job_level);
371 employee.can_approve_pr = matches!(
372 job_level,
373 JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
374 );
375 employee.can_approve_po = matches!(
376 job_level,
377 JobLevel::Senior
378 | JobLevel::Manager
379 | JobLevel::Director
380 | JobLevel::VicePresident
381 | JobLevel::Executive
382 );
383 employee.can_approve_je = matches!(
384 job_level,
385 JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
386 );
387 employee
388 }
389
390 pub fn generate_department_pool(
392 &mut self,
393 company_code: &str,
394 department: &DepartmentDefinition,
395 hire_date_range: (NaiveDate, NaiveDate),
396 ) -> EmployeePool {
397 let mut pool = EmployeePool::new();
398
399 let (start_date, end_date) = hire_date_range;
400 let days_range = (end_date - start_date).num_days() as u64;
401
402 let head_level = if department.headcount >= 15 {
404 JobLevel::Director
405 } else {
406 JobLevel::Manager
407 };
408 let hire_date =
409 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range / 2) as i64);
410 let dept_head =
411 self.generate_employee_with_level(company_code, department, head_level, hire_date);
412 let dept_head_id = dept_head.employee_id.clone();
413 pool.add_employee(dept_head);
414
415 for _ in 1..department.headcount {
417 let hire_date =
418 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
419 let mut employee = self.generate_employee(company_code, department, hire_date);
420
421 employee.manager_id = Some(dept_head_id.clone());
423
424 pool.add_employee(employee);
425 }
426
427 let direct_reports: Vec<String> = pool
429 .employees
430 .iter()
431 .filter(|e| e.manager_id.as_ref() == Some(&dept_head_id))
432 .map(|e| e.employee_id.clone())
433 .collect();
434
435 if let Some(head) = pool
437 .employees
438 .iter_mut()
439 .find(|e| e.employee_id == dept_head_id)
440 {
441 head.direct_reports = direct_reports;
442 }
443
444 pool
445 }
446
447 pub fn generate_company_pool(
449 &mut self,
450 company_code: &str,
451 hire_date_range: (NaiveDate, NaiveDate),
452 ) -> EmployeePool {
453 debug!(company_code, "Generating employee company pool");
454 let mut pool = EmployeePool::new();
455
456 let (start_date, end_date) = hire_date_range;
457 let _days_range = (end_date - start_date).num_days() as u64;
458
459 let ceo = self.generate_executive(company_code, "CEO", start_date);
461 let ceo_id = ceo.employee_id.clone();
462 pool.add_employee(ceo);
463
464 let cfo = self.generate_executive(company_code, "CFO", start_date);
465 let cfo_id = cfo.employee_id.clone();
466 pool.employees
467 .last_mut()
468 .expect("just added CEO")
469 .manager_id = Some(ceo_id.clone());
470 pool.add_employee(cfo);
471
472 let coo = self.generate_executive(company_code, "COO", start_date);
473 let coo_id = coo.employee_id.clone();
474 pool.employees
475 .last_mut()
476 .expect("just added CFO")
477 .manager_id = Some(ceo_id.clone());
478 pool.add_employee(coo);
479
480 let mut departments = DepartmentDefinition::standard_departments(company_code);
488 for dept in departments.iter_mut() {
489 let code_suffix = dept.code.rsplit('-').next().unwrap_or("");
492 let key = match code_suffix {
493 "FIN" => "finance",
494 "PROC" => "procurement",
495 "SALES" => "sales",
496 "WH" => "warehouse",
497 "IT" => "it",
498 _ => "",
499 };
500 if !key.is_empty() {
501 let resolved = self.resolve_department_name(key, &dept.name);
502 dept.name = resolved;
503 }
504 }
505 let departments = departments;
506
507 for dept in &departments {
508 let dept_pool = self.generate_department_pool(company_code, dept, hire_date_range);
509
510 let is_finance = dept.code.ends_with("-FIN");
516 for mut employee in dept_pool.employees {
517 if employee.manager_id.is_none() {
518 employee.manager_id = if is_finance {
519 Some(cfo_id.clone())
520 } else {
521 Some(coo_id.clone())
522 };
523 }
524 pool.add_employee(employee);
525 }
526 }
527
528 self.update_direct_reports(&mut pool);
530
531 pool
532 }
533
534 fn generate_executive(
536 &mut self,
537 company_code: &str,
538 title: &str,
539 hire_date: NaiveDate,
540 ) -> Employee {
541 self.employee_counter += 1;
542
543 let name = self.name_generator.generate_name(&mut self.rng);
544 let employee_id = format!("EMP-{}-{:06}", company_code, self.employee_counter);
545 let user_id = name.to_user_id(self.employee_counter);
547 let email = self.name_generator.generate_email(&name);
548
549 let mut employee = Employee::new(
550 employee_id,
551 user_id,
552 name.first_name.clone(),
553 name.last_name.clone(),
554 company_code.to_string(),
555 );
556
557 employee.email = email;
558 employee.job_level = JobLevel::Executive;
559 employee.job_title = title.to_string();
560 employee.department_id = Some("Executive".to_string());
561 employee.cost_center = Some(format!("CC-{company_code}-EXEC"));
562 employee.hire_date = Some(hire_date);
563 employee.approval_limit = Decimal::from(100_000_000);
564 employee.can_approve_pr = true;
565 employee.can_approve_po = true;
566 employee.can_approve_je = true;
567 employee.system_roles.push(SystemRole::Executive);
568
569 employee
570 }
571
572 fn update_direct_reports(&self, pool: &mut EmployeePool) {
574 let mut direct_reports_map: std::collections::HashMap<String, Vec<String>> =
576 std::collections::HashMap::new();
577
578 for employee in &pool.employees {
579 if let Some(manager_id) = &employee.manager_id {
580 direct_reports_map
581 .entry(manager_id.clone())
582 .or_default()
583 .push(employee.employee_id.clone());
584 }
585 }
586
587 for employee in &mut pool.employees {
589 if let Some(reports) = direct_reports_map.get(&employee.employee_id) {
590 employee.direct_reports = reports.clone();
591 }
592 }
593 }
594
595 fn select_job_level(&mut self) -> JobLevel {
597 let roll: f64 = self.rng.random();
598 let mut cumulative = 0.0;
599
600 for (level, prob) in &self.config.job_level_distribution {
601 cumulative += prob;
602 if roll < cumulative {
603 return *level;
604 }
605 }
606
607 JobLevel::Staff
608 }
609
610 fn get_approval_limit(&self, job_level: &JobLevel) -> Decimal {
612 for (level, limit) in &self.config.approval_limits {
613 if level == job_level {
614 return *limit;
615 }
616 }
617 Decimal::from(1_000)
618 }
619
620 fn select_status(&mut self) -> EmployeeStatus {
622 let roll: f64 = self.rng.random();
623
624 if roll < self.config.termination_rate {
625 EmployeeStatus::Terminated
626 } else if roll < self.config.termination_rate + self.config.leave_rate {
627 EmployeeStatus::OnLeave
628 } else {
629 EmployeeStatus::Active
630 }
631 }
632
633 pub fn generate_change_history(
640 &mut self,
641 employee: &Employee,
642 period_end: NaiveDate,
643 ) -> Vec<EmployeeChangeEvent> {
644 let hire_date = match employee.hire_date {
645 Some(d) => d,
646 None => return Vec::new(),
647 };
648
649 let mut events: Vec<EmployeeChangeEvent> = Vec::with_capacity(5);
650
651 events.push(EmployeeChangeEvent::hired(
653 employee.employee_id.clone(),
654 hire_date,
655 ));
656
657 let tenure_end = employee
659 .termination_date
660 .unwrap_or(period_end)
661 .min(period_end);
662 let tenure_days = (tenure_end - hire_date).num_days().max(1);
663
664 if tenure_days < 60 {
666 return events;
667 }
668
669 let additional_count = self.rng.random_range(1u32..=4);
671 let mut offsets: Vec<i64> = (0..additional_count)
673 .map(|_| self.rng.random_range(30i64..tenure_days))
674 .collect();
675 offsets.sort_unstable();
676
677 let event_types = [
678 EmployeeEventType::Promoted,
679 EmployeeEventType::SalaryAdjustment,
680 EmployeeEventType::Transfer,
681 ];
682
683 for offset in offsets {
684 let event_date = hire_date + chrono::Duration::days(offset);
685
686 let idx = self.rng.random_range(0..event_types.len());
688 let event_type = event_types[idx];
689
690 let (old_val, new_val) = match event_type {
691 EmployeeEventType::Promoted => {
692 let old = format!("{:?}", employee.job_level);
693 let new = format!("{:?}_promoted", employee.job_level);
694 (Some(old), Some(new))
695 }
696 EmployeeEventType::SalaryAdjustment => {
697 let pct = self.rng.random_range(2u32..=15);
698 let old = employee.base_salary.to_string();
699 let new_salary =
700 employee.base_salary * rust_decimal::Decimal::new(100 + pct as i64, 2);
701 (Some(old), Some(new_salary.round_dp(2).to_string()))
702 }
703 EmployeeEventType::Transfer => {
704 let old = employee
705 .department_id
706 .clone()
707 .unwrap_or_else(|| "unknown".to_string());
708 let new = format!("{}_new", old);
709 (Some(old), Some(new))
710 }
711 _ => (None, None),
712 };
713
714 events.push(EmployeeChangeEvent {
715 employee_id: employee.employee_id.clone(),
716 event_date,
717 event_type,
718 old_value: old_val,
719 new_value: new_val,
720 effective_date: event_date,
721 });
722 }
723
724 if employee.status == EmployeeStatus::Terminated {
726 if let Some(term_date) = employee.termination_date {
727 let term_capped = term_date.min(period_end);
728 events.push(EmployeeChangeEvent {
729 employee_id: employee.employee_id.clone(),
730 event_date: term_capped,
731 event_type: EmployeeEventType::Terminated,
732 old_value: Some("active".to_string()),
733 new_value: Some("terminated".to_string()),
734 effective_date: term_capped,
735 });
736 }
737 }
738
739 events
740 }
741
742 pub fn generate_all_change_history(
744 &mut self,
745 pool: &EmployeePool,
746 period_end: NaiveDate,
747 ) -> Vec<EmployeeChangeEvent> {
748 pool.employees
749 .iter()
750 .flat_map(|e| self.generate_change_history(e, period_end))
751 .collect()
752 }
753
754 pub fn reset(&mut self) {
756 self.rng = seeded_rng(self.seed, 0);
757 self.employee_counter = 0;
758 }
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764
765 #[test]
766 fn test_employee_generation() {
767 let mut gen = EmployeeGenerator::new(42);
768 let dept = DepartmentDefinition::finance("1000");
769 let employee =
770 gen.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
771
772 assert!(!employee.employee_id.is_empty());
773 assert!(!employee.display_name.is_empty());
774 assert!(!employee.email.is_empty());
775 assert!(employee.approval_limit > Decimal::ZERO);
776 }
777
778 #[test]
779 fn test_department_pool() {
780 let mut gen = EmployeeGenerator::new(42);
781 let dept = DepartmentDefinition::finance("1000");
782 let pool = gen.generate_department_pool(
783 "1000",
784 &dept,
785 (
786 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
787 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
788 ),
789 );
790
791 assert_eq!(pool.employees.len(), dept.headcount);
792
793 let managers: Vec<_> = pool
795 .employees
796 .iter()
797 .filter(|e| matches!(e.job_level, JobLevel::Manager | JobLevel::Director))
798 .collect();
799 assert!(!managers.is_empty());
800
801 let dept_head = managers.first().unwrap();
803 assert!(!dept_head.direct_reports.is_empty());
804 }
805
806 #[test]
807 fn test_company_pool() {
808 let mut gen = EmployeeGenerator::new(42);
809 let pool = gen.generate_company_pool(
810 "1000",
811 (
812 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
813 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
814 ),
815 );
816
817 let executives: Vec<_> = pool
819 .employees
820 .iter()
821 .filter(|e| e.job_level == JobLevel::Executive)
822 .collect();
823 assert!(executives.len() >= 3); let cfo = pool.employees.iter().find(|e| e.job_title == "CFO");
827 assert!(cfo.is_some());
828 }
829
830 #[test]
831 fn test_hierarchy() {
832 let mut gen = EmployeeGenerator::new(42);
833 let pool = gen.generate_company_pool(
834 "1000",
835 (
836 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
837 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
838 ),
839 );
840
841 let non_ceo_without_manager: Vec<_> = pool
843 .employees
844 .iter()
845 .filter(|e| e.job_title != "CEO")
846 .filter(|e| e.manager_id.is_none())
847 .collect();
848
849 assert!(non_ceo_without_manager.len() <= 1);
851 }
852
853 #[test]
854 fn test_deterministic_generation() {
855 let mut gen1 = EmployeeGenerator::new(42);
856 let mut gen2 = EmployeeGenerator::new(42);
857
858 let dept = DepartmentDefinition::finance("1000");
859 let employee1 =
860 gen1.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
861 let employee2 =
862 gen2.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
863
864 assert_eq!(employee1.employee_id, employee2.employee_id);
865 assert_eq!(employee1.display_name, employee2.display_name);
866 }
867
868 #[test]
869 fn test_approval_limits() {
870 let mut gen = EmployeeGenerator::new(42);
871 let dept = DepartmentDefinition::finance("1000");
872
873 let staff = gen.generate_employee_with_level(
874 "1000",
875 &dept,
876 JobLevel::Staff,
877 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
878 );
879 let manager = gen.generate_employee_with_level(
880 "1000",
881 &dept,
882 JobLevel::Manager,
883 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
884 );
885
886 assert!(manager.approval_limit > staff.approval_limit);
887 assert!(!staff.can_approve_pr);
888 assert!(manager.can_approve_pr);
889 }
890
891 #[test]
892 fn test_country_pack_does_not_break_generation() {
893 let mut gen = EmployeeGenerator::new(42);
894 gen.set_country_pack(datasynth_core::CountryPack::default());
896
897 let dept = DepartmentDefinition::finance("1000");
898 let employee =
899 gen.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
900
901 assert!(!employee.employee_id.is_empty());
902 assert!(!employee.display_name.is_empty());
903 assert!(!employee.email.is_empty());
904 assert!(employee.approval_limit > Decimal::ZERO);
905 }
906}