1use crate::models::{BusinessProcess, UserPersona};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Department {
13 pub code: String,
15
16 pub name: String,
18
19 pub parent_code: Option<String>,
21
22 pub cost_center: String,
24
25 pub typical_personas: Vec<UserPersona>,
27
28 pub primary_processes: Vec<BusinessProcess>,
30
31 pub standard_headcount: DepartmentHeadcount,
33
34 pub is_active: bool,
36}
37
38impl Department {
39 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 pub fn with_parent(mut self, parent_code: &str) -> Self {
55 self.parent_code = Some(parent_code.to_string());
56 self
57 }
58
59 pub fn with_personas(mut self, personas: Vec<UserPersona>) -> Self {
61 self.typical_personas = personas;
62 self
63 }
64
65 pub fn with_processes(mut self, processes: Vec<BusinessProcess>) -> Self {
67 self.primary_processes = processes;
68 self
69 }
70
71 pub fn with_headcount(mut self, headcount: DepartmentHeadcount) -> Self {
73 self.standard_headcount = headcount;
74 self
75 }
76
77 pub fn handles_process(&self, process: BusinessProcess) -> bool {
79 self.primary_processes.contains(&process)
80 }
81
82 pub fn is_typical_persona(&self, persona: UserPersona) -> bool {
84 self.typical_personas.contains(&persona)
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct DepartmentHeadcount {
91 pub junior_accountant: usize,
93 pub senior_accountant: usize,
95 pub controller: usize,
97 pub manager: usize,
99 pub executive: usize,
101 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct OrganizationStructure {
157 pub company_code: String,
159
160 pub departments: Vec<Department>,
162
163 #[serde(skip)]
165 department_index: HashMap<String, usize>,
166}
167
168impl OrganizationStructure {
169 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 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 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 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 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 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 pub fn total_headcount(&self) -> usize {
218 self.departments
219 .iter()
220 .map(|d| d.standard_headcount.total())
221 .sum()
222 }
223
224 pub fn standard(company_code: &str) -> Self {
226 let mut org = Self::new(company_code);
227
228 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 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 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 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 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 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 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 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 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 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 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 let p2p_depts = org.get_departments_for_process(BusinessProcess::P2P);
469 assert!(!p2p_depts.is_empty());
470
471 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); 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}