datasynth_generators/master_data/
profit_center_generator.rs1use datasynth_core::models::{ProfitCenter, ProfitCenterCategory};
9use datasynth_core::utils::seeded_rng;
10use rand::prelude::*;
11use rand_chacha::ChaCha8Rng;
12use tracing::debug;
13
14const SEED_DISCRIMINATOR: u64 = 0x5043_434e; struct SegmentTemplate {
19 code: &'static str,
20 name: &'static str,
21 category: ProfitCenterCategory,
22 segment_code: &'static str,
25 sub_units: &'static [(&'static str, &'static str)],
26}
27
28const SEGMENT_TEMPLATES: &[SegmentTemplate] = &[
29 SegmentTemplate {
30 code: "CONSUMER",
31 name: "Consumer Products",
32 category: ProfitCenterCategory::Segment,
33 segment_code: "SEG-CONSUMER",
34 sub_units: &[
35 ("FOOD", "Food & Beverage"),
36 ("HHC", "Home & Health Care"),
37 ("PERS", "Personal Care"),
38 ],
39 },
40 SegmentTemplate {
41 code: "INDUSTRIAL",
42 name: "Industrial",
43 category: ProfitCenterCategory::Segment,
44 segment_code: "SEG-INDUSTRIAL",
45 sub_units: &[("MFG", "Manufacturing"), ("CHEM", "Specialty Chemicals")],
46 },
47 SegmentTemplate {
48 code: "SERVICES",
49 name: "Services",
50 category: ProfitCenterCategory::Service,
51 segment_code: "SEG-SERVICES",
52 sub_units: &[
53 ("PROF", "Professional Services"),
54 ("LIC", "Licensing & Royalties"),
55 ],
56 },
57 SegmentTemplate {
58 code: "CORP",
59 name: "Corporate / Unallocated",
60 category: ProfitCenterCategory::Corporate,
61 segment_code: "SEG-CORP",
62 sub_units: &[("ELIM", "Eliminations & Adjustments")],
63 },
64];
65
66pub struct ProfitCenterGenerator {
68 rng: ChaCha8Rng,
69}
70
71impl ProfitCenterGenerator {
72 pub fn new(seed: u64) -> Self {
74 Self {
75 rng: seeded_rng(seed, SEED_DISCRIMINATOR),
76 }
77 }
78
79 pub fn generate_for_company(
87 &mut self,
88 company_code: &str,
89 employee_ids: &[String],
90 ) -> Vec<ProfitCenter> {
91 let mut profit_centers: Vec<ProfitCenter> = Vec::with_capacity(16);
92
93 for tmpl in SEGMENT_TEMPLATES {
94 let seg_id = format!("PC-{}-{}", company_code, tmpl.code);
95
96 let mut top = ProfitCenter::top_level(
98 seg_id.clone(),
99 format!("{} — {}", company_code, tmpl.name),
100 company_code,
101 tmpl.category,
102 );
103 top.segment_code = Some(tmpl.segment_code.to_string());
104 top.responsible_person = self.pick_employee(employee_ids);
105 profit_centers.push(top);
106
107 for (sub_code, sub_name) in tmpl.sub_units {
109 let sub_id = format!("PC-{}-{}-{}", company_code, tmpl.code, sub_code);
110 let mut sub = ProfitCenter::sub_unit(
111 sub_id,
112 format!("{} / {}", tmpl.name, sub_name),
113 seg_id.clone(),
114 company_code,
115 tmpl.category,
116 );
117 sub.segment_code = Some(tmpl.segment_code.to_string());
118 sub.responsible_person = self.pick_employee(employee_ids);
119 profit_centers.push(sub);
120 }
121 }
122
123 debug!(
124 company_code,
125 count = profit_centers.len(),
126 "Generated profit centres"
127 );
128 profit_centers
129 }
130
131 fn pick_employee(&mut self, employee_ids: &[String]) -> Option<String> {
133 if employee_ids.is_empty() || self.rng.random::<f64>() > 0.20 {
134 return None;
135 }
136 let idx = self.rng.random_range(0..employee_ids.len());
137 Some(employee_ids[idx].clone())
138 }
139}
140
141#[cfg(test)]
142#[allow(clippy::unwrap_used)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_deterministic_generation() {
148 let mut g1 = ProfitCenterGenerator::new(42);
149 let mut g2 = ProfitCenterGenerator::new(42);
150 let employees = vec!["EMP-001".to_string(), "EMP-002".to_string()];
151 let p1 = g1.generate_for_company("C001", &employees);
152 let p2 = g2.generate_for_company("C001", &employees);
153 assert_eq!(p1.len(), p2.len());
154 for (a, b) in p1.iter().zip(p2.iter()) {
155 assert_eq!(a.id, b.id);
156 assert_eq!(a.responsible_person, b.responsible_person);
157 }
158 }
159
160 #[test]
161 fn test_hierarchy_shape() {
162 let mut pcgen = ProfitCenterGenerator::new(7);
163 let pcs = pcgen.generate_for_company("C001", &[]);
164
165 let level1_count = pcs.iter().filter(|p| p.level == 1).count();
166 let level2_count = pcs.iter().filter(|p| p.level == 2).count();
167 assert_eq!(level1_count, SEGMENT_TEMPLATES.len());
168 assert_eq!(
169 level2_count,
170 SEGMENT_TEMPLATES
171 .iter()
172 .map(|t| t.sub_units.len())
173 .sum::<usize>()
174 );
175
176 let level1_ids: std::collections::HashSet<&String> =
178 pcs.iter().filter(|p| p.level == 1).map(|p| &p.id).collect();
179 for sub in pcs.iter().filter(|p| p.level == 2) {
180 let parent = sub.parent_id.as_ref().expect("level-2 has parent");
181 assert!(level1_ids.contains(parent));
182 }
183 }
184
185 #[test]
186 fn test_segment_code_propagation() {
187 let mut pcgen = ProfitCenterGenerator::new(7);
188 let pcs = pcgen.generate_for_company("C001", &[]);
189
190 for pc in &pcs {
193 assert!(pc.segment_code.is_some(), "{} missing segment_code", pc.id);
194 }
195 let parent = pcs.iter().find(|p| p.level == 1).unwrap();
197 for child in pcs
198 .iter()
199 .filter(|p| p.parent_id.as_deref() == Some(parent.id.as_str()))
200 {
201 assert_eq!(parent.segment_code, child.segment_code);
202 }
203 }
204}