use crate::models::{BusinessProcess, UserPersona};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Department {
pub code: String,
pub name: String,
pub parent_code: Option<String>,
pub cost_center: String,
pub typical_personas: Vec<UserPersona>,
pub primary_processes: Vec<BusinessProcess>,
pub standard_headcount: DepartmentHeadcount,
pub is_active: bool,
}
impl Department {
pub fn new(code: &str, name: &str, cost_center: &str) -> Self {
Self {
code: code.to_string(),
name: name.to_string(),
parent_code: None,
cost_center: cost_center.to_string(),
typical_personas: Vec::new(),
primary_processes: Vec::new(),
standard_headcount: DepartmentHeadcount::default(),
is_active: true,
}
}
pub fn with_parent(mut self, parent_code: &str) -> Self {
self.parent_code = Some(parent_code.to_string());
self
}
pub fn with_personas(mut self, personas: Vec<UserPersona>) -> Self {
self.typical_personas = personas;
self
}
pub fn with_processes(mut self, processes: Vec<BusinessProcess>) -> Self {
self.primary_processes = processes;
self
}
pub fn with_headcount(mut self, headcount: DepartmentHeadcount) -> Self {
self.standard_headcount = headcount;
self
}
pub fn handles_process(&self, process: BusinessProcess) -> bool {
self.primary_processes.contains(&process)
}
pub fn is_typical_persona(&self, persona: UserPersona) -> bool {
self.typical_personas.contains(&persona)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepartmentHeadcount {
pub junior_accountant: usize,
pub senior_accountant: usize,
pub controller: usize,
pub manager: usize,
pub executive: usize,
pub automated_system: usize,
}
impl Default for DepartmentHeadcount {
fn default() -> Self {
Self {
junior_accountant: 2,
senior_accountant: 1,
controller: 0,
manager: 0,
executive: 0,
automated_system: 1,
}
}
}
impl DepartmentHeadcount {
pub fn empty() -> Self {
Self {
junior_accountant: 0,
senior_accountant: 0,
controller: 0,
manager: 0,
executive: 0,
automated_system: 0,
}
}
pub fn total(&self) -> usize {
self.junior_accountant
+ self.senior_accountant
+ self.controller
+ self.manager
+ self.executive
+ self.automated_system
}
pub fn scaled(&self, multiplier: f64) -> Self {
Self {
junior_accountant: (self.junior_accountant as f64 * multiplier).round() as usize,
senior_accountant: (self.senior_accountant as f64 * multiplier).round() as usize,
controller: (self.controller as f64 * multiplier).round() as usize,
manager: (self.manager as f64 * multiplier).round() as usize,
executive: (self.executive as f64 * multiplier).round() as usize,
automated_system: (self.automated_system as f64 * multiplier).round() as usize,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrganizationStructure {
pub company_code: String,
pub departments: Vec<Department>,
#[serde(skip)]
department_index: HashMap<String, usize>,
}
impl OrganizationStructure {
pub fn new(company_code: &str) -> Self {
Self {
company_code: company_code.to_string(),
departments: Vec::new(),
department_index: HashMap::new(),
}
}
pub fn add_department(&mut self, department: Department) {
let idx = self.departments.len();
self.department_index.insert(department.code.clone(), idx);
self.departments.push(department);
}
pub fn get_department(&self, code: &str) -> Option<&Department> {
self.department_index
.get(code)
.map(|&idx| &self.departments[idx])
}
pub fn get_departments_for_process(&self, process: BusinessProcess) -> Vec<&Department> {
self.departments
.iter()
.filter(|d| d.handles_process(process))
.collect()
}
pub fn get_departments_for_persona(&self, persona: UserPersona) -> Vec<&Department> {
self.departments
.iter()
.filter(|d| d.is_typical_persona(persona))
.collect()
}
pub fn rebuild_index(&mut self) {
self.department_index.clear();
for (idx, dept) in self.departments.iter().enumerate() {
self.department_index.insert(dept.code.clone(), idx);
}
}
pub fn total_headcount(&self) -> usize {
self.departments
.iter()
.map(|d| d.standard_headcount.total())
.sum()
}
pub fn standard(company_code: &str) -> Self {
let mut org = Self::new(company_code);
org.add_department(
Department::new("FIN", "Finance", "1000")
.with_personas(vec![
UserPersona::Controller,
UserPersona::SeniorAccountant,
UserPersona::Manager,
UserPersona::Executive,
])
.with_processes(vec![BusinessProcess::R2R])
.with_headcount(DepartmentHeadcount {
junior_accountant: 0,
senior_accountant: 2,
controller: 2,
manager: 1,
executive: 1,
automated_system: 2,
}),
);
org.add_department(
Department::new("AP", "Accounts Payable", "1100")
.with_parent("FIN")
.with_personas(vec![
UserPersona::JuniorAccountant,
UserPersona::SeniorAccountant,
])
.with_processes(vec![BusinessProcess::P2P])
.with_headcount(DepartmentHeadcount {
junior_accountant: 5,
senior_accountant: 2,
controller: 0,
manager: 1,
executive: 0,
automated_system: 5,
}),
);
org.add_department(
Department::new("AR", "Accounts Receivable", "1200")
.with_parent("FIN")
.with_personas(vec![
UserPersona::JuniorAccountant,
UserPersona::SeniorAccountant,
])
.with_processes(vec![BusinessProcess::O2C])
.with_headcount(DepartmentHeadcount {
junior_accountant: 4,
senior_accountant: 2,
controller: 0,
manager: 1,
executive: 0,
automated_system: 5,
}),
);
org.add_department(
Department::new("GL", "General Ledger", "1300")
.with_parent("FIN")
.with_personas(vec![UserPersona::SeniorAccountant, UserPersona::Controller])
.with_processes(vec![BusinessProcess::R2R])
.with_headcount(DepartmentHeadcount {
junior_accountant: 2,
senior_accountant: 3,
controller: 1,
manager: 0,
executive: 0,
automated_system: 3,
}),
);
org.add_department(
Department::new("HR", "Human Resources", "2000")
.with_personas(vec![
UserPersona::JuniorAccountant,
UserPersona::SeniorAccountant,
])
.with_processes(vec![BusinessProcess::H2R])
.with_headcount(DepartmentHeadcount {
junior_accountant: 2,
senior_accountant: 1,
controller: 0,
manager: 1,
executive: 0,
automated_system: 2,
}),
);
org.add_department(
Department::new("FA", "Fixed Assets", "1400")
.with_parent("FIN")
.with_personas(vec![
UserPersona::JuniorAccountant,
UserPersona::SeniorAccountant,
])
.with_processes(vec![BusinessProcess::A2R])
.with_headcount(DepartmentHeadcount {
junior_accountant: 1,
senior_accountant: 1,
controller: 0,
manager: 0,
executive: 0,
automated_system: 2,
}),
);
org.add_department(
Department::new("TRE", "Treasury", "1500")
.with_parent("FIN")
.with_personas(vec![
UserPersona::SeniorAccountant,
UserPersona::Controller,
UserPersona::Manager,
])
.with_processes(vec![BusinessProcess::Treasury])
.with_headcount(DepartmentHeadcount {
junior_accountant: 0,
senior_accountant: 2,
controller: 1,
manager: 1,
executive: 0,
automated_system: 2,
}),
);
org.add_department(
Department::new("TAX", "Tax", "1600")
.with_parent("FIN")
.with_personas(vec![UserPersona::SeniorAccountant, UserPersona::Controller])
.with_processes(vec![BusinessProcess::Tax])
.with_headcount(DepartmentHeadcount {
junior_accountant: 1,
senior_accountant: 2,
controller: 1,
manager: 0,
executive: 0,
automated_system: 1,
}),
);
org.add_department(
Department::new("PROC", "Procurement", "3000")
.with_personas(vec![UserPersona::SeniorAccountant, UserPersona::Manager])
.with_processes(vec![BusinessProcess::P2P])
.with_headcount(DepartmentHeadcount {
junior_accountant: 2,
senior_accountant: 2,
controller: 0,
manager: 1,
executive: 0,
automated_system: 3,
}),
);
org.add_department(
Department::new("IT", "Information Technology", "4000")
.with_personas(vec![UserPersona::AutomatedSystem])
.with_processes(vec![BusinessProcess::R2R])
.with_headcount(DepartmentHeadcount {
junior_accountant: 0,
senior_accountant: 0,
controller: 0,
manager: 0,
executive: 0,
automated_system: 10,
}),
);
org
}
pub fn minimal(company_code: &str) -> Self {
let mut org = Self::new(company_code);
org.add_department(
Department::new("FIN", "Finance", "1000")
.with_personas(vec![
UserPersona::JuniorAccountant,
UserPersona::SeniorAccountant,
UserPersona::Controller,
UserPersona::Manager,
])
.with_processes(vec![
BusinessProcess::O2C,
BusinessProcess::P2P,
BusinessProcess::R2R,
BusinessProcess::H2R,
BusinessProcess::A2R,
])
.with_headcount(DepartmentHeadcount {
junior_accountant: 3,
senior_accountant: 2,
controller: 1,
manager: 1,
executive: 0,
automated_system: 5,
}),
);
org
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_department_creation() {
let dept = Department::new("FIN", "Finance", "1000")
.with_personas(vec![UserPersona::Controller])
.with_processes(vec![BusinessProcess::R2R]);
assert_eq!(dept.code, "FIN");
assert_eq!(dept.name, "Finance");
assert!(dept.handles_process(BusinessProcess::R2R));
assert!(!dept.handles_process(BusinessProcess::P2P));
assert!(dept.is_typical_persona(UserPersona::Controller));
}
#[test]
fn test_standard_organization() {
let org = OrganizationStructure::standard("1000");
assert!(!org.departments.is_empty());
assert!(org.get_department("FIN").is_some());
assert!(org.get_department("AP").is_some());
assert!(org.get_department("AR").is_some());
let p2p_depts = org.get_departments_for_process(BusinessProcess::P2P);
assert!(!p2p_depts.is_empty());
assert!(org.total_headcount() > 0);
}
#[test]
fn test_headcount_scaling() {
let headcount = DepartmentHeadcount {
junior_accountant: 10,
senior_accountant: 5,
controller: 2,
manager: 1,
executive: 0,
automated_system: 3,
};
let scaled = headcount.scaled(0.5);
assert_eq!(scaled.junior_accountant, 5);
assert_eq!(scaled.senior_accountant, 3); assert_eq!(scaled.controller, 1);
}
#[test]
fn test_minimal_organization() {
let org = OrganizationStructure::minimal("1000");
assert_eq!(org.departments.len(), 1);
let fin = org.get_department("FIN").unwrap();
assert!(fin.handles_process(BusinessProcess::O2C));
assert!(fin.handles_process(BusinessProcess::P2P));
}
}