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)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_deterministic_generation() {
147 let mut g1 = ProfitCenterGenerator::new(42);
148 let mut g2 = ProfitCenterGenerator::new(42);
149 let employees = vec!["EMP-001".to_string(), "EMP-002".to_string()];
150 let p1 = g1.generate_for_company("C001", &employees);
151 let p2 = g2.generate_for_company("C001", &employees);
152 assert_eq!(p1.len(), p2.len());
153 for (a, b) in p1.iter().zip(p2.iter()) {
154 assert_eq!(a.id, b.id);
155 assert_eq!(a.responsible_person, b.responsible_person);
156 }
157 }
158
159 #[test]
160 fn test_hierarchy_shape() {
161 let mut pcgen = ProfitCenterGenerator::new(7);
162 let pcs = pcgen.generate_for_company("C001", &[]);
163
164 let level1_count = pcs.iter().filter(|p| p.level == 1).count();
165 let level2_count = pcs.iter().filter(|p| p.level == 2).count();
166 assert_eq!(level1_count, SEGMENT_TEMPLATES.len());
167 assert_eq!(
168 level2_count,
169 SEGMENT_TEMPLATES
170 .iter()
171 .map(|t| t.sub_units.len())
172 .sum::<usize>()
173 );
174
175 let level1_ids: std::collections::HashSet<&String> =
177 pcs.iter().filter(|p| p.level == 1).map(|p| &p.id).collect();
178 for sub in pcs.iter().filter(|p| p.level == 2) {
179 let parent = sub.parent_id.as_ref().expect("level-2 has parent");
180 assert!(level1_ids.contains(parent));
181 }
182 }
183
184 #[test]
185 fn test_segment_code_propagation() {
186 let mut pcgen = ProfitCenterGenerator::new(7);
187 let pcs = pcgen.generate_for_company("C001", &[]);
188
189 for pc in &pcs {
192 assert!(pc.segment_code.is_some(), "{} missing segment_code", pc.id);
193 }
194 let parent = pcs.iter().find(|p| p.level == 1).unwrap();
196 for child in pcs
197 .iter()
198 .filter(|p| p.parent_id.as_deref() == Some(parent.id.as_str()))
199 {
200 assert_eq!(parent.segment_code, child.segment_code);
201 }
202 }
203}