Skip to main content

datasynth_core/models/
profit_center.rs

1//! Profit centre hierarchy model for management accounting / segment reporting.
2//!
3//! Profit centres represent business units, product lines, or geographic
4//! regions whose contribution to the consolidated P&L is reported
5//! independently — they map to SAP CEPC (`PRCTR`) and to the IFRS 8 /
6//! ASC 280 operating-segment dimension.  Unlike cost centres (which are
7//! always cost-only), profit centres carry both revenue and cost
8//! attribution and are typically organised by business segment, region,
9//! or product line.
10
11use serde::{Deserialize, Serialize};
12
13/// Categorisation of a profit centre — drives default account mapping
14/// and segment-reporting roll-up.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
16#[serde(rename_all = "snake_case")]
17pub enum ProfitCenterCategory {
18    /// A product-line profit centre — owns revenue + cost for one offering.
19    #[default]
20    ProductLine,
21    /// A regional / geographic profit centre — Americas, EMEA, APAC, etc.
22    Region,
23    /// A business-segment profit centre — IFRS 8 reportable operating segment.
24    Segment,
25    /// A service / shared-service profit centre that allocates internally.
26    Service,
27    /// A corporate / holding profit centre (often used for consolidation
28    /// adjustments and unallocated items in segment reconciliations).
29    Corporate,
30}
31
32impl std::fmt::Display for ProfitCenterCategory {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Self::ProductLine => write!(f, "Product Line"),
36            Self::Region => write!(f, "Region"),
37            Self::Segment => write!(f, "Segment"),
38            Self::Service => write!(f, "Service"),
39            Self::Corporate => write!(f, "Corporate"),
40        }
41    }
42}
43
44/// A profit centre node in the organisational profit hierarchy.
45///
46/// Profit centres are arranged in a two-level tree:
47/// - **Level 1** (parent): represents a business segment, geographic region,
48///   or major product-line group.  These have `parent_id == None`.
49/// - **Level 2** (child): represents a sub-segment, sub-region, or
50///   individual product line.  These have `parent_id == Some(...)`.
51///
52/// Mapping to SAP CEPC: `id` → `PRCTR`, `name` → `KTEXT`,
53/// `responsible_person` → `VERAK_USER`, `company_code` →
54/// (resolved through controlling area `KOKRS`), `is_active == false`
55/// → `LOKKZ` (locked) flag.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ProfitCenter {
58    /// Unique profit centre identifier (e.g., "PC-C001-EMEA-DACH")
59    pub id: String,
60
61    /// Human-readable name (e.g., "EMEA / DACH region")
62    pub name: String,
63
64    /// Parent profit centre ID for level-2 nodes; `None` for level-1
65    /// segment / region nodes.
66    pub parent_id: Option<String>,
67
68    /// Company code this profit centre belongs to.
69    pub company_code: String,
70
71    /// Employee ID of the manager responsible for this profit centre.
72    pub responsible_person: Option<String>,
73
74    /// Functional category of this profit centre.
75    pub category: ProfitCenterCategory,
76
77    /// Reporting segment code — used by IFRS 8 / ASC 280 segment
78    /// reconciliation.  Multiple level-2 profit centres can roll up to
79    /// the same segment.  `None` means the profit centre is not
80    /// individually reported (e.g., it sits within a larger segment).
81    pub segment_code: Option<String>,
82
83    /// Hierarchy level (1 = top-level segment/region, 2 = sub-unit).
84    pub level: u8,
85
86    /// Whether this profit centre is currently active.
87    pub is_active: bool,
88}
89
90impl ProfitCenter {
91    /// Create a new level-1 (top-level segment / region / product group)
92    /// profit centre.
93    pub fn top_level(
94        id: impl Into<String>,
95        name: impl Into<String>,
96        company_code: impl Into<String>,
97        category: ProfitCenterCategory,
98    ) -> Self {
99        Self {
100            id: id.into(),
101            name: name.into(),
102            parent_id: None,
103            company_code: company_code.into(),
104            responsible_person: None,
105            category,
106            segment_code: None,
107            level: 1,
108            is_active: true,
109        }
110    }
111
112    /// Create a new level-2 (sub-unit) profit centre.
113    pub fn sub_unit(
114        id: impl Into<String>,
115        name: impl Into<String>,
116        parent_id: impl Into<String>,
117        company_code: impl Into<String>,
118        category: ProfitCenterCategory,
119    ) -> Self {
120        Self {
121            id: id.into(),
122            name: name.into(),
123            parent_id: Some(parent_id.into()),
124            company_code: company_code.into(),
125            responsible_person: None,
126            category,
127            segment_code: None,
128            level: 2,
129            is_active: true,
130        }
131    }
132
133    /// Attach a segment code (IFRS 8 / ASC 280 reportable segment).
134    pub fn with_segment(mut self, segment_code: impl Into<String>) -> Self {
135        self.segment_code = Some(segment_code.into());
136        self
137    }
138
139    /// Attach a responsible person.
140    pub fn with_responsible_person(mut self, person_id: impl Into<String>) -> Self {
141        self.responsible_person = Some(person_id.into());
142        self
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_top_level_constructor() {
152        let pc = ProfitCenter::top_level("PC-EMEA", "EMEA", "C001", ProfitCenterCategory::Region);
153        assert!(pc.parent_id.is_none());
154        assert_eq!(pc.level, 1);
155        assert!(pc.is_active);
156        assert_eq!(pc.category, ProfitCenterCategory::Region);
157    }
158
159    #[test]
160    fn test_sub_unit_constructor() {
161        let pc = ProfitCenter::sub_unit(
162            "PC-EMEA-DACH",
163            "DACH",
164            "PC-EMEA",
165            "C001",
166            ProfitCenterCategory::Region,
167        );
168        assert_eq!(pc.parent_id.as_deref(), Some("PC-EMEA"));
169        assert_eq!(pc.level, 2);
170    }
171
172    #[test]
173    fn test_builder_chain() {
174        let pc = ProfitCenter::top_level(
175            "PC-CONSUMER",
176            "Consumer",
177            "C001",
178            ProfitCenterCategory::Segment,
179        )
180        .with_segment("SEG-CONSUMER")
181        .with_responsible_person("EMP-001");
182        assert_eq!(pc.segment_code.as_deref(), Some("SEG-CONSUMER"));
183        assert_eq!(pc.responsible_person.as_deref(), Some("EMP-001"));
184    }
185}