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 = format!("u{:06}", self.employee_counter);
269 let email = self.name_generator.generate_email(&name);
270
271 let job_level = self.select_job_level();
272 let approval_limit = self.get_approval_limit(&job_level);
273
274 let mut employee = Employee::new(
275 employee_id,
276 user_id,
277 name.first_name.clone(),
278 name.last_name.clone(),
279 company_code.to_string(),
280 );
281
282 employee.email = email;
284 employee.job_level = job_level;
285
286 employee.department_id = Some(department.name.clone());
288 employee.cost_center = Some(department.cost_center.clone());
289
290 employee.hire_date = Some(hire_date);
292
293 employee.approval_limit = approval_limit;
295 employee.can_approve_pr = matches!(
296 job_level,
297 JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
298 );
299 employee.can_approve_po = matches!(
300 job_level,
301 JobLevel::Senior
302 | JobLevel::Manager
303 | JobLevel::Director
304 | JobLevel::VicePresident
305 | JobLevel::Executive
306 );
307 employee.can_approve_je = matches!(
308 job_level,
309 JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
310 );
311
312 if !department.system_roles.is_empty() {
314 let role_idx = self.rng.random_range(0..department.system_roles.len());
315 employee
316 .system_roles
317 .push(department.system_roles[role_idx].clone());
318 }
319
320 for tcode in &department.transaction_codes {
322 employee.transaction_codes.push(TransactionCodeAuth {
323 tcode: tcode.clone(),
324 activity: datasynth_core::models::ActivityType::Create,
325 active: true,
326 });
327 }
328
329 let (salary_min, salary_max): (u64, u64) = match job_level {
332 JobLevel::Staff => (40_000, 60_000),
333 JobLevel::Senior => (60_000, 90_000),
334 JobLevel::Lead => (75_000, 105_000),
335 JobLevel::Supervisor => (70_000, 100_000),
336 JobLevel::Manager => (80_000, 120_000),
337 JobLevel::Director => (100_000, 160_000),
338 JobLevel::VicePresident => (130_000, 200_000),
339 JobLevel::Executive => (150_000, 250_000),
340 };
341 let salary_range = salary_max - salary_min;
342 let salary_raw =
343 salary_min + (self.rng.random::<f64>() * salary_range as f64).round() as u64;
344 employee.base_salary = Decimal::from(salary_raw);
345
346 employee.status = self.select_status();
348 if employee.status == EmployeeStatus::Terminated {
349 employee.termination_date =
350 Some(hire_date + chrono::Duration::days(self.rng.random_range(365..1825) as i64));
351 }
352
353 employee
354 }
355
356 pub fn generate_employee_with_level(
358 &mut self,
359 company_code: &str,
360 department: &DepartmentDefinition,
361 job_level: JobLevel,
362 hire_date: NaiveDate,
363 ) -> Employee {
364 let mut employee = self.generate_employee(company_code, department, hire_date);
365 employee.job_level = job_level;
366 employee.approval_limit = self.get_approval_limit(&job_level);
367 employee.can_approve_pr = matches!(
368 job_level,
369 JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
370 );
371 employee.can_approve_po = matches!(
372 job_level,
373 JobLevel::Senior
374 | JobLevel::Manager
375 | JobLevel::Director
376 | JobLevel::VicePresident
377 | JobLevel::Executive
378 );
379 employee.can_approve_je = matches!(
380 job_level,
381 JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
382 );
383 employee
384 }
385
386 pub fn generate_department_pool(
388 &mut self,
389 company_code: &str,
390 department: &DepartmentDefinition,
391 hire_date_range: (NaiveDate, NaiveDate),
392 ) -> EmployeePool {
393 let mut pool = EmployeePool::new();
394
395 let (start_date, end_date) = hire_date_range;
396 let days_range = (end_date - start_date).num_days() as u64;
397
398 let head_level = if department.headcount >= 15 {
400 JobLevel::Director
401 } else {
402 JobLevel::Manager
403 };
404 let hire_date =
405 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range / 2) as i64);
406 let dept_head =
407 self.generate_employee_with_level(company_code, department, head_level, hire_date);
408 let dept_head_id = dept_head.employee_id.clone();
409 pool.add_employee(dept_head);
410
411 for _ in 1..department.headcount {
413 let hire_date =
414 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
415 let mut employee = self.generate_employee(company_code, department, hire_date);
416
417 employee.manager_id = Some(dept_head_id.clone());
419
420 pool.add_employee(employee);
421 }
422
423 let direct_reports: Vec<String> = pool
425 .employees
426 .iter()
427 .filter(|e| e.manager_id.as_ref() == Some(&dept_head_id))
428 .map(|e| e.employee_id.clone())
429 .collect();
430
431 if let Some(head) = pool
433 .employees
434 .iter_mut()
435 .find(|e| e.employee_id == dept_head_id)
436 {
437 head.direct_reports = direct_reports;
438 }
439
440 pool
441 }
442
443 pub fn generate_company_pool(
445 &mut self,
446 company_code: &str,
447 hire_date_range: (NaiveDate, NaiveDate),
448 ) -> EmployeePool {
449 debug!(company_code, "Generating employee company pool");
450 let mut pool = EmployeePool::new();
451
452 let (start_date, end_date) = hire_date_range;
453 let _days_range = (end_date - start_date).num_days() as u64;
454
455 let ceo = self.generate_executive(company_code, "CEO", start_date);
457 let ceo_id = ceo.employee_id.clone();
458 pool.add_employee(ceo);
459
460 let cfo = self.generate_executive(company_code, "CFO", start_date);
461 let cfo_id = cfo.employee_id.clone();
462 pool.employees
463 .last_mut()
464 .expect("just added CEO")
465 .manager_id = Some(ceo_id.clone());
466 pool.add_employee(cfo);
467
468 let coo = self.generate_executive(company_code, "COO", start_date);
469 let coo_id = coo.employee_id.clone();
470 pool.employees
471 .last_mut()
472 .expect("just added CFO")
473 .manager_id = Some(ceo_id.clone());
474 pool.add_employee(coo);
475
476 let mut departments = DepartmentDefinition::standard_departments(company_code);
484 for dept in departments.iter_mut() {
485 let code_suffix = dept.code.rsplit('-').next().unwrap_or("");
488 let key = match code_suffix {
489 "FIN" => "finance",
490 "PROC" => "procurement",
491 "SALES" => "sales",
492 "WH" => "warehouse",
493 "IT" => "it",
494 _ => "",
495 };
496 if !key.is_empty() {
497 let resolved = self.resolve_department_name(key, &dept.name);
498 dept.name = resolved;
499 }
500 }
501 let departments = departments;
502
503 for dept in &departments {
504 let dept_pool = self.generate_department_pool(company_code, dept, hire_date_range);
505
506 let is_finance = dept.code.ends_with("-FIN");
512 for mut employee in dept_pool.employees {
513 if employee.manager_id.is_none() {
514 employee.manager_id = if is_finance {
515 Some(cfo_id.clone())
516 } else {
517 Some(coo_id.clone())
518 };
519 }
520 pool.add_employee(employee);
521 }
522 }
523
524 self.update_direct_reports(&mut pool);
526
527 pool
528 }
529
530 fn generate_executive(
532 &mut self,
533 company_code: &str,
534 title: &str,
535 hire_date: NaiveDate,
536 ) -> Employee {
537 self.employee_counter += 1;
538
539 let name = self.name_generator.generate_name(&mut self.rng);
540 let employee_id = format!("EMP-{}-{:06}", company_code, self.employee_counter);
541 let user_id = format!("exec{:04}", self.employee_counter);
542 let email = self.name_generator.generate_email(&name);
543
544 let mut employee = Employee::new(
545 employee_id,
546 user_id,
547 name.first_name.clone(),
548 name.last_name.clone(),
549 company_code.to_string(),
550 );
551
552 employee.email = email;
553 employee.job_level = JobLevel::Executive;
554 employee.job_title = title.to_string();
555 employee.department_id = Some("Executive".to_string());
556 employee.cost_center = Some(format!("CC-{company_code}-EXEC"));
557 employee.hire_date = Some(hire_date);
558 employee.approval_limit = Decimal::from(100_000_000);
559 employee.can_approve_pr = true;
560 employee.can_approve_po = true;
561 employee.can_approve_je = true;
562 employee.system_roles.push(SystemRole::Executive);
563
564 employee
565 }
566
567 fn update_direct_reports(&self, pool: &mut EmployeePool) {
569 let mut direct_reports_map: std::collections::HashMap<String, Vec<String>> =
571 std::collections::HashMap::new();
572
573 for employee in &pool.employees {
574 if let Some(manager_id) = &employee.manager_id {
575 direct_reports_map
576 .entry(manager_id.clone())
577 .or_default()
578 .push(employee.employee_id.clone());
579 }
580 }
581
582 for employee in &mut pool.employees {
584 if let Some(reports) = direct_reports_map.get(&employee.employee_id) {
585 employee.direct_reports = reports.clone();
586 }
587 }
588 }
589
590 fn select_job_level(&mut self) -> JobLevel {
592 let roll: f64 = self.rng.random();
593 let mut cumulative = 0.0;
594
595 for (level, prob) in &self.config.job_level_distribution {
596 cumulative += prob;
597 if roll < cumulative {
598 return *level;
599 }
600 }
601
602 JobLevel::Staff
603 }
604
605 fn get_approval_limit(&self, job_level: &JobLevel) -> Decimal {
607 for (level, limit) in &self.config.approval_limits {
608 if level == job_level {
609 return *limit;
610 }
611 }
612 Decimal::from(1_000)
613 }
614
615 fn select_status(&mut self) -> EmployeeStatus {
617 let roll: f64 = self.rng.random();
618
619 if roll < self.config.termination_rate {
620 EmployeeStatus::Terminated
621 } else if roll < self.config.termination_rate + self.config.leave_rate {
622 EmployeeStatus::OnLeave
623 } else {
624 EmployeeStatus::Active
625 }
626 }
627
628 pub fn generate_change_history(
635 &mut self,
636 employee: &Employee,
637 period_end: NaiveDate,
638 ) -> Vec<EmployeeChangeEvent> {
639 let hire_date = match employee.hire_date {
640 Some(d) => d,
641 None => return Vec::new(),
642 };
643
644 let mut events: Vec<EmployeeChangeEvent> = Vec::with_capacity(5);
645
646 events.push(EmployeeChangeEvent::hired(
648 employee.employee_id.clone(),
649 hire_date,
650 ));
651
652 let tenure_end = employee
654 .termination_date
655 .unwrap_or(period_end)
656 .min(period_end);
657 let tenure_days = (tenure_end - hire_date).num_days().max(1);
658
659 if tenure_days < 60 {
661 return events;
662 }
663
664 let additional_count = self.rng.random_range(1u32..=4);
666 let mut offsets: Vec<i64> = (0..additional_count)
668 .map(|_| self.rng.random_range(30i64..tenure_days))
669 .collect();
670 offsets.sort_unstable();
671
672 let event_types = [
673 EmployeeEventType::Promoted,
674 EmployeeEventType::SalaryAdjustment,
675 EmployeeEventType::Transfer,
676 ];
677
678 for offset in offsets {
679 let event_date = hire_date + chrono::Duration::days(offset);
680
681 let idx = self.rng.random_range(0..event_types.len());
683 let event_type = event_types[idx];
684
685 let (old_val, new_val) = match event_type {
686 EmployeeEventType::Promoted => {
687 let old = format!("{:?}", employee.job_level);
688 let new = format!("{:?}_promoted", employee.job_level);
689 (Some(old), Some(new))
690 }
691 EmployeeEventType::SalaryAdjustment => {
692 let pct = self.rng.random_range(2u32..=15);
693 let old = employee.base_salary.to_string();
694 let new_salary =
695 employee.base_salary * rust_decimal::Decimal::new(100 + pct as i64, 2);
696 (Some(old), Some(new_salary.round_dp(2).to_string()))
697 }
698 EmployeeEventType::Transfer => {
699 let old = employee
700 .department_id
701 .clone()
702 .unwrap_or_else(|| "unknown".to_string());
703 let new = format!("{}_new", old);
704 (Some(old), Some(new))
705 }
706 _ => (None, None),
707 };
708
709 events.push(EmployeeChangeEvent {
710 employee_id: employee.employee_id.clone(),
711 event_date,
712 event_type,
713 old_value: old_val,
714 new_value: new_val,
715 effective_date: event_date,
716 });
717 }
718
719 if employee.status == EmployeeStatus::Terminated {
721 if let Some(term_date) = employee.termination_date {
722 let term_capped = term_date.min(period_end);
723 events.push(EmployeeChangeEvent {
724 employee_id: employee.employee_id.clone(),
725 event_date: term_capped,
726 event_type: EmployeeEventType::Terminated,
727 old_value: Some("active".to_string()),
728 new_value: Some("terminated".to_string()),
729 effective_date: term_capped,
730 });
731 }
732 }
733
734 events
735 }
736
737 pub fn generate_all_change_history(
739 &mut self,
740 pool: &EmployeePool,
741 period_end: NaiveDate,
742 ) -> Vec<EmployeeChangeEvent> {
743 pool.employees
744 .iter()
745 .flat_map(|e| self.generate_change_history(e, period_end))
746 .collect()
747 }
748
749 pub fn reset(&mut self) {
751 self.rng = seeded_rng(self.seed, 0);
752 self.employee_counter = 0;
753 }
754}
755
756#[cfg(test)]
757#[allow(clippy::unwrap_used)]
758mod tests {
759 use super::*;
760
761 #[test]
762 fn test_employee_generation() {
763 let mut gen = EmployeeGenerator::new(42);
764 let dept = DepartmentDefinition::finance("1000");
765 let employee =
766 gen.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
767
768 assert!(!employee.employee_id.is_empty());
769 assert!(!employee.display_name.is_empty());
770 assert!(!employee.email.is_empty());
771 assert!(employee.approval_limit > Decimal::ZERO);
772 }
773
774 #[test]
775 fn test_department_pool() {
776 let mut gen = EmployeeGenerator::new(42);
777 let dept = DepartmentDefinition::finance("1000");
778 let pool = gen.generate_department_pool(
779 "1000",
780 &dept,
781 (
782 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
783 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
784 ),
785 );
786
787 assert_eq!(pool.employees.len(), dept.headcount);
788
789 let managers: Vec<_> = pool
791 .employees
792 .iter()
793 .filter(|e| matches!(e.job_level, JobLevel::Manager | JobLevel::Director))
794 .collect();
795 assert!(!managers.is_empty());
796
797 let dept_head = managers.first().unwrap();
799 assert!(!dept_head.direct_reports.is_empty());
800 }
801
802 #[test]
803 fn test_company_pool() {
804 let mut gen = EmployeeGenerator::new(42);
805 let pool = gen.generate_company_pool(
806 "1000",
807 (
808 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
809 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
810 ),
811 );
812
813 let executives: Vec<_> = pool
815 .employees
816 .iter()
817 .filter(|e| e.job_level == JobLevel::Executive)
818 .collect();
819 assert!(executives.len() >= 3); let cfo = pool.employees.iter().find(|e| e.job_title == "CFO");
823 assert!(cfo.is_some());
824 }
825
826 #[test]
827 fn test_hierarchy() {
828 let mut gen = EmployeeGenerator::new(42);
829 let pool = gen.generate_company_pool(
830 "1000",
831 (
832 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
833 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
834 ),
835 );
836
837 let non_ceo_without_manager: Vec<_> = pool
839 .employees
840 .iter()
841 .filter(|e| e.job_title != "CEO")
842 .filter(|e| e.manager_id.is_none())
843 .collect();
844
845 assert!(non_ceo_without_manager.len() <= 1);
847 }
848
849 #[test]
850 fn test_deterministic_generation() {
851 let mut gen1 = EmployeeGenerator::new(42);
852 let mut gen2 = EmployeeGenerator::new(42);
853
854 let dept = DepartmentDefinition::finance("1000");
855 let employee1 =
856 gen1.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
857 let employee2 =
858 gen2.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
859
860 assert_eq!(employee1.employee_id, employee2.employee_id);
861 assert_eq!(employee1.display_name, employee2.display_name);
862 }
863
864 #[test]
865 fn test_approval_limits() {
866 let mut gen = EmployeeGenerator::new(42);
867 let dept = DepartmentDefinition::finance("1000");
868
869 let staff = gen.generate_employee_with_level(
870 "1000",
871 &dept,
872 JobLevel::Staff,
873 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
874 );
875 let manager = gen.generate_employee_with_level(
876 "1000",
877 &dept,
878 JobLevel::Manager,
879 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
880 );
881
882 assert!(manager.approval_limit > staff.approval_limit);
883 assert!(!staff.can_approve_pr);
884 assert!(manager.can_approve_pr);
885 }
886
887 #[test]
888 fn test_country_pack_does_not_break_generation() {
889 let mut gen = EmployeeGenerator::new(42);
890 gen.set_country_pack(datasynth_core::CountryPack::default());
892
893 let dept = DepartmentDefinition::finance("1000");
894 let employee =
895 gen.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
896
897 assert!(!employee.employee_id.is_empty());
898 assert!(!employee.display_name.is_empty());
899 assert!(!employee.email.is_empty());
900 assert!(employee.approval_limit > Decimal::ZERO);
901 }
902}