Skip to main content

datasynth_generators/master_data/
cost_center_generator.rs

1//! Cost center hierarchy generator.
2//!
3//! Generates a two-level cost center hierarchy (departments → sub-departments)
4//! per company: typically 5 level-1 department nodes and 2-4 sub-department
5//! nodes per department, resulting in 10-25 cost centers per company.
6
7use datasynth_core::models::{CostCenter, CostCenterCategory};
8use datasynth_core::utils::seeded_rng;
9use rand::prelude::*;
10use rand_chacha::ChaCha8Rng;
11use tracing::debug;
12
13/// Seed discriminator for cost center generator (avoids UUID collisions).
14const SEED_DISCRIMINATOR: u64 = 0x4343_434e; // "CCCN"
15
16/// Template for generating a department and its sub-departments.
17struct DeptTemplate {
18    code: &'static str,
19    name: &'static str,
20    category: CostCenterCategory,
21    sub_depts: &'static [(&'static str, &'static str)],
22}
23
24const DEPT_TEMPLATES: &[DeptTemplate] = &[
25    DeptTemplate {
26        code: "FIN",
27        name: "Finance",
28        category: CostCenterCategory::Administration,
29        sub_depts: &[
30            ("AP", "Accounts Payable"),
31            ("AR", "Accounts Receivable"),
32            ("GL", "General Ledger"),
33            ("TAX", "Tax"),
34        ],
35    },
36    DeptTemplate {
37        code: "PROD",
38        name: "Production",
39        category: CostCenterCategory::Production,
40        sub_depts: &[
41            ("ASSY", "Assembly"),
42            ("QC", "Quality Control"),
43            ("MAINT", "Maintenance"),
44        ],
45    },
46    DeptTemplate {
47        code: "SALES",
48        name: "Sales & Marketing",
49        category: CostCenterCategory::Sales,
50        sub_depts: &[
51            ("DOM", "Domestic Sales"),
52            ("INTL", "International Sales"),
53            ("MKT", "Marketing"),
54        ],
55    },
56    DeptTemplate {
57        code: "RD",
58        name: "Research & Development",
59        category: CostCenterCategory::RAndD,
60        sub_depts: &[("RSCH", "Research"), ("DEV", "Development")],
61    },
62    DeptTemplate {
63        code: "CORP",
64        name: "Corporate",
65        category: CostCenterCategory::Corporate,
66        sub_depts: &[
67            ("EXEC", "Executive"),
68            ("HR", "Human Resources"),
69            ("IT", "Information Technology"),
70            ("LEGAL", "Legal"),
71        ],
72    },
73];
74
75/// Generator for cost center hierarchies.
76pub struct CostCenterGenerator {
77    rng: ChaCha8Rng,
78}
79
80impl CostCenterGenerator {
81    /// Create a new cost center generator with the given seed.
82    pub fn new(seed: u64) -> Self {
83        Self {
84            rng: seeded_rng(seed, SEED_DISCRIMINATOR),
85        }
86    }
87
88    /// Generate all cost centers for a single company.
89    ///
90    /// Produces level-1 department nodes and level-2 sub-department nodes in a
91    /// 2-level hierarchy.  Each department gets all of its configured
92    /// sub-departments, and each node has a 20 % chance of being assigned a
93    /// responsible person from `employee_ids` (if provided).
94    pub fn generate_for_company(
95        &mut self,
96        company_code: &str,
97        employee_ids: &[String],
98    ) -> Vec<CostCenter> {
99        let mut cost_centers: Vec<CostCenter> = Vec::with_capacity(25);
100
101        for tmpl in DEPT_TEMPLATES {
102            let dept_id = format!("CC-{}-{}", company_code, tmpl.code);
103
104            // Level-1: department node
105            let mut dept = CostCenter::department(
106                dept_id.clone(),
107                format!("{} — {}", company_code, tmpl.name),
108                company_code,
109                tmpl.category,
110            );
111            dept.responsible_person = self.pick_employee(employee_ids);
112            cost_centers.push(dept);
113
114            // Level-2: sub-department nodes
115            for (sub_code, sub_name) in tmpl.sub_depts {
116                let sub_id = format!("CC-{}-{}-{}", company_code, tmpl.code, sub_code);
117                let mut sub = CostCenter::sub_department(
118                    sub_id,
119                    format!("{} / {}", tmpl.name, sub_name),
120                    dept_id.clone(),
121                    company_code,
122                    tmpl.category,
123                );
124                sub.responsible_person = self.pick_employee(employee_ids);
125                cost_centers.push(sub);
126            }
127        }
128
129        debug!(
130            company_code,
131            count = cost_centers.len(),
132            "Generated cost centers"
133        );
134        cost_centers
135    }
136
137    /// Randomly pick an employee ID (20 % chance of assignment).
138    fn pick_employee(&mut self, employee_ids: &[String]) -> Option<String> {
139        if employee_ids.is_empty() || self.rng.random::<f64>() > 0.20 {
140            return None;
141        }
142        let idx = self.rng.random_range(0..employee_ids.len());
143        Some(employee_ids[idx].clone())
144    }
145}