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}