datasynth_core/models/
department.rs

1//! Department and organizational structure models.
2//!
3//! Provides department definitions with associated cost centers,
4//! business processes, and typical user personas.
5
6use crate::models::{BusinessProcess, UserPersona};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Department definition for organizational structure.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Department {
13    /// Department code (e.g., "FIN", "AP", "AR")
14    pub code: String,
15
16    /// Department name (e.g., "Finance", "Accounts Payable")
17    pub name: String,
18
19    /// Parent department code (for hierarchy)
20    pub parent_code: Option<String>,
21
22    /// Associated cost center
23    pub cost_center: String,
24
25    /// Typical user personas in this department
26    pub typical_personas: Vec<UserPersona>,
27
28    /// Primary business processes handled
29    pub primary_processes: Vec<BusinessProcess>,
30
31    /// Standard headcount for this department
32    pub standard_headcount: DepartmentHeadcount,
33
34    /// Is this department active
35    pub is_active: bool,
36}
37
38impl Department {
39    /// Create a new department.
40    pub fn new(code: &str, name: &str, cost_center: &str) -> Self {
41        Self {
42            code: code.to_string(),
43            name: name.to_string(),
44            parent_code: None,
45            cost_center: cost_center.to_string(),
46            typical_personas: Vec::new(),
47            primary_processes: Vec::new(),
48            standard_headcount: DepartmentHeadcount::default(),
49            is_active: true,
50        }
51    }
52
53    /// Set parent department.
54    pub fn with_parent(mut self, parent_code: &str) -> Self {
55        self.parent_code = Some(parent_code.to_string());
56        self
57    }
58
59    /// Add typical personas.
60    pub fn with_personas(mut self, personas: Vec<UserPersona>) -> Self {
61        self.typical_personas = personas;
62        self
63    }
64
65    /// Add primary business processes.
66    pub fn with_processes(mut self, processes: Vec<BusinessProcess>) -> Self {
67        self.primary_processes = processes;
68        self
69    }
70
71    /// Set headcount.
72    pub fn with_headcount(mut self, headcount: DepartmentHeadcount) -> Self {
73        self.standard_headcount = headcount;
74        self
75    }
76
77    /// Check if this department handles a specific business process.
78    pub fn handles_process(&self, process: BusinessProcess) -> bool {
79        self.primary_processes.contains(&process)
80    }
81
82    /// Check if a persona is typical for this department.
83    pub fn is_typical_persona(&self, persona: UserPersona) -> bool {
84        self.typical_personas.contains(&persona)
85    }
86}
87
88/// Headcount configuration for a department.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct DepartmentHeadcount {
91    /// Number of junior accountants
92    pub junior_accountant: usize,
93    /// Number of senior accountants
94    pub senior_accountant: usize,
95    /// Number of controllers
96    pub controller: usize,
97    /// Number of managers
98    pub manager: usize,
99    /// Number of executives (usually 0 or 1)
100    pub executive: usize,
101    /// Number of automated systems/batch jobs
102    pub automated_system: usize,
103}
104
105impl Default for DepartmentHeadcount {
106    fn default() -> Self {
107        Self {
108            junior_accountant: 2,
109            senior_accountant: 1,
110            controller: 0,
111            manager: 0,
112            executive: 0,
113            automated_system: 1,
114        }
115    }
116}
117
118impl DepartmentHeadcount {
119    /// Create an empty headcount.
120    pub fn empty() -> Self {
121        Self {
122            junior_accountant: 0,
123            senior_accountant: 0,
124            controller: 0,
125            manager: 0,
126            executive: 0,
127            automated_system: 0,
128        }
129    }
130
131    /// Total headcount.
132    pub fn total(&self) -> usize {
133        self.junior_accountant
134            + self.senior_accountant
135            + self.controller
136            + self.manager
137            + self.executive
138            + self.automated_system
139    }
140
141    /// Apply a multiplier to all counts.
142    pub fn scaled(&self, multiplier: f64) -> Self {
143        Self {
144            junior_accountant: (self.junior_accountant as f64 * multiplier).round() as usize,
145            senior_accountant: (self.senior_accountant as f64 * multiplier).round() as usize,
146            controller: (self.controller as f64 * multiplier).round() as usize,
147            manager: (self.manager as f64 * multiplier).round() as usize,
148            executive: (self.executive as f64 * multiplier).round() as usize,
149            automated_system: (self.automated_system as f64 * multiplier).round() as usize,
150        }
151    }
152}
153
154/// Organization structure containing all departments.
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct OrganizationStructure {
157    /// Company code this structure belongs to
158    pub company_code: String,
159
160    /// All departments in the organization
161    pub departments: Vec<Department>,
162
163    /// Index by department code for quick lookup
164    #[serde(skip)]
165    department_index: HashMap<String, usize>,
166}
167
168impl OrganizationStructure {
169    /// Create a new empty organization structure.
170    pub fn new(company_code: &str) -> Self {
171        Self {
172            company_code: company_code.to_string(),
173            departments: Vec::new(),
174            department_index: HashMap::new(),
175        }
176    }
177
178    /// Add a department to the structure.
179    pub fn add_department(&mut self, department: Department) {
180        let idx = self.departments.len();
181        self.department_index.insert(department.code.clone(), idx);
182        self.departments.push(department);
183    }
184
185    /// Get a department by code.
186    pub fn get_department(&self, code: &str) -> Option<&Department> {
187        self.department_index
188            .get(code)
189            .map(|&idx| &self.departments[idx])
190    }
191
192    /// Get departments that handle a specific business process.
193    pub fn get_departments_for_process(&self, process: BusinessProcess) -> Vec<&Department> {
194        self.departments
195            .iter()
196            .filter(|d| d.handles_process(process))
197            .collect()
198    }
199
200    /// Get departments with a specific persona type.
201    pub fn get_departments_for_persona(&self, persona: UserPersona) -> Vec<&Department> {
202        self.departments
203            .iter()
204            .filter(|d| d.is_typical_persona(persona))
205            .collect()
206    }
207
208    /// Rebuild the index (call after deserialization).
209    pub fn rebuild_index(&mut self) {
210        self.department_index.clear();
211        for (idx, dept) in self.departments.iter().enumerate() {
212            self.department_index.insert(dept.code.clone(), idx);
213        }
214    }
215
216    /// Get total headcount across all departments.
217    pub fn total_headcount(&self) -> usize {
218        self.departments
219            .iter()
220            .map(|d| d.standard_headcount.total())
221            .sum()
222    }
223
224    /// Generate a standard organization structure.
225    pub fn standard(company_code: &str) -> Self {
226        let mut org = Self::new(company_code);
227
228        // Finance department (parent)
229        org.add_department(
230            Department::new("FIN", "Finance", "1000")
231                .with_personas(vec![
232                    UserPersona::Controller,
233                    UserPersona::SeniorAccountant,
234                    UserPersona::Manager,
235                    UserPersona::Executive,
236                ])
237                .with_processes(vec![BusinessProcess::R2R])
238                .with_headcount(DepartmentHeadcount {
239                    junior_accountant: 0,
240                    senior_accountant: 2,
241                    controller: 2,
242                    manager: 1,
243                    executive: 1,
244                    automated_system: 2,
245                }),
246        );
247
248        // Accounts Payable
249        org.add_department(
250            Department::new("AP", "Accounts Payable", "1100")
251                .with_parent("FIN")
252                .with_personas(vec![
253                    UserPersona::JuniorAccountant,
254                    UserPersona::SeniorAccountant,
255                ])
256                .with_processes(vec![BusinessProcess::P2P])
257                .with_headcount(DepartmentHeadcount {
258                    junior_accountant: 5,
259                    senior_accountant: 2,
260                    controller: 0,
261                    manager: 1,
262                    executive: 0,
263                    automated_system: 5,
264                }),
265        );
266
267        // Accounts Receivable
268        org.add_department(
269            Department::new("AR", "Accounts Receivable", "1200")
270                .with_parent("FIN")
271                .with_personas(vec![
272                    UserPersona::JuniorAccountant,
273                    UserPersona::SeniorAccountant,
274                ])
275                .with_processes(vec![BusinessProcess::O2C])
276                .with_headcount(DepartmentHeadcount {
277                    junior_accountant: 4,
278                    senior_accountant: 2,
279                    controller: 0,
280                    manager: 1,
281                    executive: 0,
282                    automated_system: 5,
283                }),
284        );
285
286        // General Ledger
287        org.add_department(
288            Department::new("GL", "General Ledger", "1300")
289                .with_parent("FIN")
290                .with_personas(vec![UserPersona::SeniorAccountant, UserPersona::Controller])
291                .with_processes(vec![BusinessProcess::R2R])
292                .with_headcount(DepartmentHeadcount {
293                    junior_accountant: 2,
294                    senior_accountant: 3,
295                    controller: 1,
296                    manager: 0,
297                    executive: 0,
298                    automated_system: 3,
299                }),
300        );
301
302        // Payroll / HR Accounting
303        org.add_department(
304            Department::new("HR", "Human Resources", "2000")
305                .with_personas(vec![
306                    UserPersona::JuniorAccountant,
307                    UserPersona::SeniorAccountant,
308                ])
309                .with_processes(vec![BusinessProcess::H2R])
310                .with_headcount(DepartmentHeadcount {
311                    junior_accountant: 2,
312                    senior_accountant: 1,
313                    controller: 0,
314                    manager: 1,
315                    executive: 0,
316                    automated_system: 2,
317                }),
318        );
319
320        // Fixed Assets
321        org.add_department(
322            Department::new("FA", "Fixed Assets", "1400")
323                .with_parent("FIN")
324                .with_personas(vec![
325                    UserPersona::JuniorAccountant,
326                    UserPersona::SeniorAccountant,
327                ])
328                .with_processes(vec![BusinessProcess::A2R])
329                .with_headcount(DepartmentHeadcount {
330                    junior_accountant: 1,
331                    senior_accountant: 1,
332                    controller: 0,
333                    manager: 0,
334                    executive: 0,
335                    automated_system: 2,
336                }),
337        );
338
339        // Treasury
340        org.add_department(
341            Department::new("TRE", "Treasury", "1500")
342                .with_parent("FIN")
343                .with_personas(vec![
344                    UserPersona::SeniorAccountant,
345                    UserPersona::Controller,
346                    UserPersona::Manager,
347                ])
348                .with_processes(vec![BusinessProcess::Treasury])
349                .with_headcount(DepartmentHeadcount {
350                    junior_accountant: 0,
351                    senior_accountant: 2,
352                    controller: 1,
353                    manager: 1,
354                    executive: 0,
355                    automated_system: 2,
356                }),
357        );
358
359        // Tax
360        org.add_department(
361            Department::new("TAX", "Tax", "1600")
362                .with_parent("FIN")
363                .with_personas(vec![UserPersona::SeniorAccountant, UserPersona::Controller])
364                .with_processes(vec![BusinessProcess::Tax])
365                .with_headcount(DepartmentHeadcount {
366                    junior_accountant: 1,
367                    senior_accountant: 2,
368                    controller: 1,
369                    manager: 0,
370                    executive: 0,
371                    automated_system: 1,
372                }),
373        );
374
375        // Procurement
376        org.add_department(
377            Department::new("PROC", "Procurement", "3000")
378                .with_personas(vec![UserPersona::SeniorAccountant, UserPersona::Manager])
379                .with_processes(vec![BusinessProcess::P2P])
380                .with_headcount(DepartmentHeadcount {
381                    junior_accountant: 2,
382                    senior_accountant: 2,
383                    controller: 0,
384                    manager: 1,
385                    executive: 0,
386                    automated_system: 3,
387                }),
388        );
389
390        // IT (batch jobs)
391        org.add_department(
392            Department::new("IT", "Information Technology", "4000")
393                .with_personas(vec![UserPersona::AutomatedSystem])
394                .with_processes(vec![BusinessProcess::R2R])
395                .with_headcount(DepartmentHeadcount {
396                    junior_accountant: 0,
397                    senior_accountant: 0,
398                    controller: 0,
399                    manager: 0,
400                    executive: 0,
401                    automated_system: 10,
402                }),
403        );
404
405        org
406    }
407
408    /// Generate a minimal organization structure for small companies.
409    pub fn minimal(company_code: &str) -> Self {
410        let mut org = Self::new(company_code);
411
412        org.add_department(
413            Department::new("FIN", "Finance", "1000")
414                .with_personas(vec![
415                    UserPersona::JuniorAccountant,
416                    UserPersona::SeniorAccountant,
417                    UserPersona::Controller,
418                    UserPersona::Manager,
419                ])
420                .with_processes(vec![
421                    BusinessProcess::O2C,
422                    BusinessProcess::P2P,
423                    BusinessProcess::R2R,
424                    BusinessProcess::H2R,
425                    BusinessProcess::A2R,
426                ])
427                .with_headcount(DepartmentHeadcount {
428                    junior_accountant: 3,
429                    senior_accountant: 2,
430                    controller: 1,
431                    manager: 1,
432                    executive: 0,
433                    automated_system: 5,
434                }),
435        );
436
437        org
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444
445    #[test]
446    fn test_department_creation() {
447        let dept = Department::new("FIN", "Finance", "1000")
448            .with_personas(vec![UserPersona::Controller])
449            .with_processes(vec![BusinessProcess::R2R]);
450
451        assert_eq!(dept.code, "FIN");
452        assert_eq!(dept.name, "Finance");
453        assert!(dept.handles_process(BusinessProcess::R2R));
454        assert!(!dept.handles_process(BusinessProcess::P2P));
455        assert!(dept.is_typical_persona(UserPersona::Controller));
456    }
457
458    #[test]
459    fn test_standard_organization() {
460        let org = OrganizationStructure::standard("1000");
461
462        assert!(!org.departments.is_empty());
463        assert!(org.get_department("FIN").is_some());
464        assert!(org.get_department("AP").is_some());
465        assert!(org.get_department("AR").is_some());
466
467        // Check process mapping
468        let p2p_depts = org.get_departments_for_process(BusinessProcess::P2P);
469        assert!(!p2p_depts.is_empty());
470
471        // Check total headcount
472        assert!(org.total_headcount() > 0);
473    }
474
475    #[test]
476    fn test_headcount_scaling() {
477        let headcount = DepartmentHeadcount {
478            junior_accountant: 10,
479            senior_accountant: 5,
480            controller: 2,
481            manager: 1,
482            executive: 0,
483            automated_system: 3,
484        };
485
486        let scaled = headcount.scaled(0.5);
487        assert_eq!(scaled.junior_accountant, 5);
488        assert_eq!(scaled.senior_accountant, 3); // 2.5 rounds to 3
489        assert_eq!(scaled.controller, 1);
490    }
491
492    #[test]
493    fn test_minimal_organization() {
494        let org = OrganizationStructure::minimal("1000");
495
496        assert_eq!(org.departments.len(), 1);
497        let fin = org.get_department("FIN").unwrap();
498        assert!(fin.handles_process(BusinessProcess::O2C));
499        assert!(fin.handles_process(BusinessProcess::P2P));
500    }
501}