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}
199
200impl EmployeeGenerator {
201 pub fn new(seed: u64) -> Self {
203 Self::with_config(seed, EmployeeGeneratorConfig::default())
204 }
205
206 pub fn with_config(seed: u64, config: EmployeeGeneratorConfig) -> Self {
208 let mut name_gen =
209 MultiCultureNameGenerator::with_distribution(config.culture_distribution.clone());
210 name_gen.set_email_domain(&config.email_domain);
211
212 Self {
213 rng: seeded_rng(seed, 0),
214 seed,
215 name_generator: name_gen,
216 config,
217 employee_counter: 0,
218 country_pack: None,
219 }
220 }
221
222 pub fn set_country_pack(&mut self, pack: datasynth_core::CountryPack) {
229 self.country_pack = Some(pack);
230 }
231
232 pub fn generate_employee(
234 &mut self,
235 company_code: &str,
236 department: &DepartmentDefinition,
237 hire_date: NaiveDate,
238 ) -> Employee {
239 self.employee_counter += 1;
240
241 let name = self.name_generator.generate_name(&mut self.rng);
242 let employee_id = format!("EMP-{}-{:06}", company_code, self.employee_counter);
243 let user_id = format!("u{:06}", self.employee_counter);
244 let email = self.name_generator.generate_email(&name);
245
246 let job_level = self.select_job_level();
247 let approval_limit = self.get_approval_limit(&job_level);
248
249 let mut employee = Employee::new(
250 employee_id,
251 user_id,
252 name.first_name.clone(),
253 name.last_name.clone(),
254 company_code.to_string(),
255 );
256
257 employee.email = email;
259 employee.job_level = job_level;
260
261 employee.department_id = Some(department.name.clone());
263 employee.cost_center = Some(department.cost_center.clone());
264
265 employee.hire_date = Some(hire_date);
267
268 employee.approval_limit = approval_limit;
270 employee.can_approve_pr = matches!(
271 job_level,
272 JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
273 );
274 employee.can_approve_po = matches!(
275 job_level,
276 JobLevel::Senior
277 | JobLevel::Manager
278 | JobLevel::Director
279 | JobLevel::VicePresident
280 | JobLevel::Executive
281 );
282 employee.can_approve_je = matches!(
283 job_level,
284 JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
285 );
286
287 if !department.system_roles.is_empty() {
289 let role_idx = self.rng.random_range(0..department.system_roles.len());
290 employee
291 .system_roles
292 .push(department.system_roles[role_idx].clone());
293 }
294
295 for tcode in &department.transaction_codes {
297 employee.transaction_codes.push(TransactionCodeAuth {
298 tcode: tcode.clone(),
299 activity: datasynth_core::models::ActivityType::Create,
300 active: true,
301 });
302 }
303
304 let (salary_min, salary_max): (u64, u64) = match job_level {
307 JobLevel::Staff => (40_000, 60_000),
308 JobLevel::Senior => (60_000, 90_000),
309 JobLevel::Lead => (75_000, 105_000),
310 JobLevel::Supervisor => (70_000, 100_000),
311 JobLevel::Manager => (80_000, 120_000),
312 JobLevel::Director => (100_000, 160_000),
313 JobLevel::VicePresident => (130_000, 200_000),
314 JobLevel::Executive => (150_000, 250_000),
315 };
316 let salary_range = salary_max - salary_min;
317 let salary_raw =
318 salary_min + (self.rng.random::<f64>() * salary_range as f64).round() as u64;
319 employee.base_salary = Decimal::from(salary_raw);
320
321 employee.status = self.select_status();
323 if employee.status == EmployeeStatus::Terminated {
324 employee.termination_date =
325 Some(hire_date + chrono::Duration::days(self.rng.random_range(365..1825) as i64));
326 }
327
328 employee
329 }
330
331 pub fn generate_employee_with_level(
333 &mut self,
334 company_code: &str,
335 department: &DepartmentDefinition,
336 job_level: JobLevel,
337 hire_date: NaiveDate,
338 ) -> Employee {
339 let mut employee = self.generate_employee(company_code, department, hire_date);
340 employee.job_level = job_level;
341 employee.approval_limit = self.get_approval_limit(&job_level);
342 employee.can_approve_pr = matches!(
343 job_level,
344 JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
345 );
346 employee.can_approve_po = matches!(
347 job_level,
348 JobLevel::Senior
349 | JobLevel::Manager
350 | JobLevel::Director
351 | JobLevel::VicePresident
352 | JobLevel::Executive
353 );
354 employee.can_approve_je = matches!(
355 job_level,
356 JobLevel::Manager | JobLevel::Director | JobLevel::VicePresident | JobLevel::Executive
357 );
358 employee
359 }
360
361 pub fn generate_department_pool(
363 &mut self,
364 company_code: &str,
365 department: &DepartmentDefinition,
366 hire_date_range: (NaiveDate, NaiveDate),
367 ) -> EmployeePool {
368 let mut pool = EmployeePool::new();
369
370 let (start_date, end_date) = hire_date_range;
371 let days_range = (end_date - start_date).num_days() as u64;
372
373 let head_level = if department.headcount >= 15 {
375 JobLevel::Director
376 } else {
377 JobLevel::Manager
378 };
379 let hire_date =
380 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range / 2) as i64);
381 let dept_head =
382 self.generate_employee_with_level(company_code, department, head_level, hire_date);
383 let dept_head_id = dept_head.employee_id.clone();
384 pool.add_employee(dept_head);
385
386 for _ in 1..department.headcount {
388 let hire_date =
389 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
390 let mut employee = self.generate_employee(company_code, department, hire_date);
391
392 employee.manager_id = Some(dept_head_id.clone());
394
395 pool.add_employee(employee);
396 }
397
398 let direct_reports: Vec<String> = pool
400 .employees
401 .iter()
402 .filter(|e| e.manager_id.as_ref() == Some(&dept_head_id))
403 .map(|e| e.employee_id.clone())
404 .collect();
405
406 if let Some(head) = pool
408 .employees
409 .iter_mut()
410 .find(|e| e.employee_id == dept_head_id)
411 {
412 head.direct_reports = direct_reports;
413 }
414
415 pool
416 }
417
418 pub fn generate_company_pool(
420 &mut self,
421 company_code: &str,
422 hire_date_range: (NaiveDate, NaiveDate),
423 ) -> EmployeePool {
424 debug!(company_code, "Generating employee company pool");
425 let mut pool = EmployeePool::new();
426
427 let (start_date, end_date) = hire_date_range;
428 let _days_range = (end_date - start_date).num_days() as u64;
429
430 let ceo = self.generate_executive(company_code, "CEO", start_date);
432 let ceo_id = ceo.employee_id.clone();
433 pool.add_employee(ceo);
434
435 let cfo = self.generate_executive(company_code, "CFO", start_date);
436 let cfo_id = cfo.employee_id.clone();
437 pool.employees
438 .last_mut()
439 .expect("just added CEO")
440 .manager_id = Some(ceo_id.clone());
441 pool.add_employee(cfo);
442
443 let coo = self.generate_executive(company_code, "COO", start_date);
444 let coo_id = coo.employee_id.clone();
445 pool.employees
446 .last_mut()
447 .expect("just added CFO")
448 .manager_id = Some(ceo_id.clone());
449 pool.add_employee(coo);
450
451 let departments = DepartmentDefinition::standard_departments(company_code);
453
454 for dept in &departments {
455 let dept_pool = self.generate_department_pool(company_code, dept, hire_date_range);
456
457 for mut employee in dept_pool.employees {
459 if employee.manager_id.is_none() {
460 employee.manager_id = if dept.name == "Finance" {
462 Some(cfo_id.clone())
463 } else {
464 Some(coo_id.clone())
465 };
466 }
467 pool.add_employee(employee);
468 }
469 }
470
471 self.update_direct_reports(&mut pool);
473
474 pool
475 }
476
477 fn generate_executive(
479 &mut self,
480 company_code: &str,
481 title: &str,
482 hire_date: NaiveDate,
483 ) -> Employee {
484 self.employee_counter += 1;
485
486 let name = self.name_generator.generate_name(&mut self.rng);
487 let employee_id = format!("EMP-{}-{:06}", company_code, self.employee_counter);
488 let user_id = format!("exec{:04}", self.employee_counter);
489 let email = self.name_generator.generate_email(&name);
490
491 let mut employee = Employee::new(
492 employee_id,
493 user_id,
494 name.first_name.clone(),
495 name.last_name.clone(),
496 company_code.to_string(),
497 );
498
499 employee.email = email;
500 employee.job_level = JobLevel::Executive;
501 employee.job_title = title.to_string();
502 employee.department_id = Some("Executive".to_string());
503 employee.cost_center = Some(format!("CC-{company_code}-EXEC"));
504 employee.hire_date = Some(hire_date);
505 employee.approval_limit = Decimal::from(100_000_000);
506 employee.can_approve_pr = true;
507 employee.can_approve_po = true;
508 employee.can_approve_je = true;
509 employee.system_roles.push(SystemRole::Executive);
510
511 employee
512 }
513
514 fn update_direct_reports(&self, pool: &mut EmployeePool) {
516 let mut direct_reports_map: std::collections::HashMap<String, Vec<String>> =
518 std::collections::HashMap::new();
519
520 for employee in &pool.employees {
521 if let Some(manager_id) = &employee.manager_id {
522 direct_reports_map
523 .entry(manager_id.clone())
524 .or_default()
525 .push(employee.employee_id.clone());
526 }
527 }
528
529 for employee in &mut pool.employees {
531 if let Some(reports) = direct_reports_map.get(&employee.employee_id) {
532 employee.direct_reports = reports.clone();
533 }
534 }
535 }
536
537 fn select_job_level(&mut self) -> JobLevel {
539 let roll: f64 = self.rng.random();
540 let mut cumulative = 0.0;
541
542 for (level, prob) in &self.config.job_level_distribution {
543 cumulative += prob;
544 if roll < cumulative {
545 return *level;
546 }
547 }
548
549 JobLevel::Staff
550 }
551
552 fn get_approval_limit(&self, job_level: &JobLevel) -> Decimal {
554 for (level, limit) in &self.config.approval_limits {
555 if level == job_level {
556 return *limit;
557 }
558 }
559 Decimal::from(1_000)
560 }
561
562 fn select_status(&mut self) -> EmployeeStatus {
564 let roll: f64 = self.rng.random();
565
566 if roll < self.config.termination_rate {
567 EmployeeStatus::Terminated
568 } else if roll < self.config.termination_rate + self.config.leave_rate {
569 EmployeeStatus::OnLeave
570 } else {
571 EmployeeStatus::Active
572 }
573 }
574
575 pub fn generate_change_history(
582 &mut self,
583 employee: &Employee,
584 period_end: NaiveDate,
585 ) -> Vec<EmployeeChangeEvent> {
586 let hire_date = match employee.hire_date {
587 Some(d) => d,
588 None => return Vec::new(),
589 };
590
591 let mut events: Vec<EmployeeChangeEvent> = Vec::with_capacity(5);
592
593 events.push(EmployeeChangeEvent::hired(
595 employee.employee_id.clone(),
596 hire_date,
597 ));
598
599 let tenure_end = employee
601 .termination_date
602 .unwrap_or(period_end)
603 .min(period_end);
604 let tenure_days = (tenure_end - hire_date).num_days().max(1);
605
606 if tenure_days < 60 {
608 return events;
609 }
610
611 let additional_count = self.rng.random_range(1u32..=4);
613 let mut offsets: Vec<i64> = (0..additional_count)
615 .map(|_| self.rng.random_range(30i64..tenure_days))
616 .collect();
617 offsets.sort_unstable();
618
619 let event_types = [
620 EmployeeEventType::Promoted,
621 EmployeeEventType::SalaryAdjustment,
622 EmployeeEventType::Transfer,
623 ];
624
625 for offset in offsets {
626 let event_date = hire_date + chrono::Duration::days(offset);
627
628 let idx = self.rng.random_range(0..event_types.len());
630 let event_type = event_types[idx];
631
632 let (old_val, new_val) = match event_type {
633 EmployeeEventType::Promoted => {
634 let old = format!("{:?}", employee.job_level);
635 let new = format!("{:?}_promoted", employee.job_level);
636 (Some(old), Some(new))
637 }
638 EmployeeEventType::SalaryAdjustment => {
639 let pct = self.rng.random_range(2u32..=15);
640 let old = employee.base_salary.to_string();
641 let new_salary =
642 employee.base_salary * rust_decimal::Decimal::new(100 + pct as i64, 2);
643 (Some(old), Some(new_salary.round_dp(2).to_string()))
644 }
645 EmployeeEventType::Transfer => {
646 let old = employee
647 .department_id
648 .clone()
649 .unwrap_or_else(|| "unknown".to_string());
650 let new = format!("{}_new", old);
651 (Some(old), Some(new))
652 }
653 _ => (None, None),
654 };
655
656 events.push(EmployeeChangeEvent {
657 employee_id: employee.employee_id.clone(),
658 event_date,
659 event_type,
660 old_value: old_val,
661 new_value: new_val,
662 effective_date: event_date,
663 });
664 }
665
666 if employee.status == EmployeeStatus::Terminated {
668 if let Some(term_date) = employee.termination_date {
669 let term_capped = term_date.min(period_end);
670 events.push(EmployeeChangeEvent {
671 employee_id: employee.employee_id.clone(),
672 event_date: term_capped,
673 event_type: EmployeeEventType::Terminated,
674 old_value: Some("active".to_string()),
675 new_value: Some("terminated".to_string()),
676 effective_date: term_capped,
677 });
678 }
679 }
680
681 events
682 }
683
684 pub fn generate_all_change_history(
686 &mut self,
687 pool: &EmployeePool,
688 period_end: NaiveDate,
689 ) -> Vec<EmployeeChangeEvent> {
690 pool.employees
691 .iter()
692 .flat_map(|e| self.generate_change_history(e, period_end))
693 .collect()
694 }
695
696 pub fn reset(&mut self) {
698 self.rng = seeded_rng(self.seed, 0);
699 self.employee_counter = 0;
700 }
701}
702
703#[cfg(test)]
704#[allow(clippy::unwrap_used)]
705mod tests {
706 use super::*;
707
708 #[test]
709 fn test_employee_generation() {
710 let mut gen = EmployeeGenerator::new(42);
711 let dept = DepartmentDefinition::finance("1000");
712 let employee =
713 gen.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
714
715 assert!(!employee.employee_id.is_empty());
716 assert!(!employee.display_name.is_empty());
717 assert!(!employee.email.is_empty());
718 assert!(employee.approval_limit > Decimal::ZERO);
719 }
720
721 #[test]
722 fn test_department_pool() {
723 let mut gen = EmployeeGenerator::new(42);
724 let dept = DepartmentDefinition::finance("1000");
725 let pool = gen.generate_department_pool(
726 "1000",
727 &dept,
728 (
729 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
730 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
731 ),
732 );
733
734 assert_eq!(pool.employees.len(), dept.headcount);
735
736 let managers: Vec<_> = pool
738 .employees
739 .iter()
740 .filter(|e| matches!(e.job_level, JobLevel::Manager | JobLevel::Director))
741 .collect();
742 assert!(!managers.is_empty());
743
744 let dept_head = managers.first().unwrap();
746 assert!(!dept_head.direct_reports.is_empty());
747 }
748
749 #[test]
750 fn test_company_pool() {
751 let mut gen = EmployeeGenerator::new(42);
752 let pool = gen.generate_company_pool(
753 "1000",
754 (
755 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
756 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
757 ),
758 );
759
760 let executives: Vec<_> = pool
762 .employees
763 .iter()
764 .filter(|e| e.job_level == JobLevel::Executive)
765 .collect();
766 assert!(executives.len() >= 3); let cfo = pool.employees.iter().find(|e| e.job_title == "CFO");
770 assert!(cfo.is_some());
771 }
772
773 #[test]
774 fn test_hierarchy() {
775 let mut gen = EmployeeGenerator::new(42);
776 let pool = gen.generate_company_pool(
777 "1000",
778 (
779 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
780 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
781 ),
782 );
783
784 let non_ceo_without_manager: Vec<_> = pool
786 .employees
787 .iter()
788 .filter(|e| e.job_title != "CEO")
789 .filter(|e| e.manager_id.is_none())
790 .collect();
791
792 assert!(non_ceo_without_manager.len() <= 1);
794 }
795
796 #[test]
797 fn test_deterministic_generation() {
798 let mut gen1 = EmployeeGenerator::new(42);
799 let mut gen2 = EmployeeGenerator::new(42);
800
801 let dept = DepartmentDefinition::finance("1000");
802 let employee1 =
803 gen1.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
804 let employee2 =
805 gen2.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
806
807 assert_eq!(employee1.employee_id, employee2.employee_id);
808 assert_eq!(employee1.display_name, employee2.display_name);
809 }
810
811 #[test]
812 fn test_approval_limits() {
813 let mut gen = EmployeeGenerator::new(42);
814 let dept = DepartmentDefinition::finance("1000");
815
816 let staff = gen.generate_employee_with_level(
817 "1000",
818 &dept,
819 JobLevel::Staff,
820 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
821 );
822 let manager = gen.generate_employee_with_level(
823 "1000",
824 &dept,
825 JobLevel::Manager,
826 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
827 );
828
829 assert!(manager.approval_limit > staff.approval_limit);
830 assert!(!staff.can_approve_pr);
831 assert!(manager.can_approve_pr);
832 }
833
834 #[test]
835 fn test_country_pack_does_not_break_generation() {
836 let mut gen = EmployeeGenerator::new(42);
837 gen.set_country_pack(datasynth_core::CountryPack::default());
839
840 let dept = DepartmentDefinition::finance("1000");
841 let employee =
842 gen.generate_employee("1000", &dept, NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
843
844 assert!(!employee.employee_id.is_empty());
845 assert!(!employee.display_name.is_empty());
846 assert!(!employee.email.is_empty());
847 assert!(employee.approval_limit > Decimal::ZERO);
848 }
849}