1use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8
9use crate::models::intercompany::ConsolidationMethod;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct FiscalYearVariant {
14 pub code: String,
16 pub description: String,
18 pub periods: u8,
20 pub special_periods: u8,
22 pub first_month: u8,
24}
25
26impl FiscalYearVariant {
27 pub fn calendar_year() -> Self {
29 Self {
30 code: "K4".to_string(),
31 description: "Calendar Year".to_string(),
32 periods: 12,
33 special_periods: 4,
34 first_month: 1,
35 }
36 }
37
38 pub fn us_federal() -> Self {
40 Self {
41 code: "V3".to_string(),
42 description: "US Federal Fiscal Year".to_string(),
43 periods: 12,
44 special_periods: 4,
45 first_month: 10,
46 }
47 }
48
49 pub fn april_year() -> Self {
51 Self {
52 code: "K1".to_string(),
53 description: "April Fiscal Year".to_string(),
54 periods: 12,
55 special_periods: 4,
56 first_month: 4,
57 }
58 }
59}
60
61impl Default for FiscalYearVariant {
62 fn default() -> Self {
63 Self::calendar_year()
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct CompanyCode {
74 pub code: String,
76
77 pub name: String,
79
80 pub legal_name: String,
82
83 pub currency: String,
85
86 pub country: String,
88
89 pub city: Option<String>,
91
92 pub fiscal_year_variant: FiscalYearVariant,
94
95 pub coa_id: String,
97
98 pub parent_company: Option<String>,
100
101 pub is_group_parent: bool,
103
104 pub controlling_area: Option<String>,
106
107 pub credit_control_area: Option<String>,
109
110 pub time_zone: String,
112
113 pub vat_number: Option<String>,
115
116 pub tax_jurisdiction: Option<String>,
118}
119
120impl CompanyCode {
121 pub fn new(code: String, name: String, currency: String, country: String) -> Self {
123 Self {
124 code: code.clone(),
125 name: name.clone(),
126 legal_name: name,
127 currency,
128 country,
129 city: None,
130 fiscal_year_variant: FiscalYearVariant::default(),
131 coa_id: "OPER".to_string(),
132 parent_company: None,
133 is_group_parent: false,
134 controlling_area: Some(code.clone()),
135 credit_control_area: Some(code),
136 time_zone: "UTC".to_string(),
137 vat_number: None,
138 tax_jurisdiction: None,
139 }
140 }
141
142 pub fn us(code: &str, name: &str) -> Self {
144 Self::new(
145 code.to_string(),
146 name.to_string(),
147 "USD".to_string(),
148 "US".to_string(),
149 )
150 }
151
152 pub fn de(code: &str, name: &str) -> Self {
154 Self::new(
155 code.to_string(),
156 name.to_string(),
157 "EUR".to_string(),
158 "DE".to_string(),
159 )
160 }
161
162 pub fn gb(code: &str, name: &str) -> Self {
164 Self::new(
165 code.to_string(),
166 name.to_string(),
167 "GBP".to_string(),
168 "GB".to_string(),
169 )
170 }
171
172 pub fn ch(code: &str, name: &str) -> Self {
174 Self::new(
175 code.to_string(),
176 name.to_string(),
177 "CHF".to_string(),
178 "CH".to_string(),
179 )
180 }
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct EnterpriseGroup {
186 pub group_id: String,
188
189 pub name: String,
191
192 pub group_currency: String,
194
195 pub companies: Vec<CompanyCode>,
197
198 pub intercompany_links: Vec<(String, String)>,
200}
201
202impl EnterpriseGroup {
203 pub fn new(group_id: String, name: String, group_currency: String) -> Self {
205 Self {
206 group_id,
207 name,
208 group_currency,
209 companies: Vec::new(),
210 intercompany_links: Vec::new(),
211 }
212 }
213
214 pub fn add_company(&mut self, company: CompanyCode) {
216 self.companies.push(company);
217 }
218
219 pub fn add_intercompany_link(&mut self, from_code: String, to_code: String) {
221 self.intercompany_links.push((from_code, to_code));
222 }
223
224 pub fn get_company(&self, code: &str) -> Option<&CompanyCode> {
226 self.companies.iter().find(|c| c.code == code)
227 }
228
229 pub fn company_codes(&self) -> Vec<&str> {
231 self.companies.iter().map(|c| c.code.as_str()).collect()
232 }
233
234 pub fn get_intercompany_partners(&self, code: &str) -> Vec<&str> {
236 self.intercompany_links
237 .iter()
238 .filter_map(|(from, to)| {
239 if from == code {
240 Some(to.as_str())
241 } else if to == code {
242 Some(from.as_str())
243 } else {
244 None
245 }
246 })
247 .collect()
248 }
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct Company {
257 pub company_code: String,
259
260 pub company_name: String,
262
263 pub country: String,
265
266 pub local_currency: String,
268
269 pub functional_currency: String,
271
272 pub is_parent: bool,
274
275 pub parent_company: Option<String>,
277
278 pub ownership_percentage: Option<Decimal>,
280
281 pub consolidation_method: ConsolidationMethod,
283}
284
285impl Company {
286 pub fn new(
288 company_code: impl Into<String>,
289 company_name: impl Into<String>,
290 country: impl Into<String>,
291 local_currency: impl Into<String>,
292 ) -> Self {
293 let currency = local_currency.into();
294 Self {
295 company_code: company_code.into(),
296 company_name: company_name.into(),
297 country: country.into(),
298 local_currency: currency.clone(),
299 functional_currency: currency,
300 is_parent: false,
301 parent_company: None,
302 ownership_percentage: None,
303 consolidation_method: ConsolidationMethod::Full,
304 }
305 }
306
307 pub fn parent(
309 company_code: impl Into<String>,
310 company_name: impl Into<String>,
311 country: impl Into<String>,
312 currency: impl Into<String>,
313 ) -> Self {
314 let mut company = Self::new(company_code, company_name, country, currency);
315 company.is_parent = true;
316 company
317 }
318
319 pub fn subsidiary(
321 company_code: impl Into<String>,
322 company_name: impl Into<String>,
323 country: impl Into<String>,
324 currency: impl Into<String>,
325 parent_code: impl Into<String>,
326 ownership_pct: Decimal,
327 ) -> Self {
328 let mut company = Self::new(company_code, company_name, country, currency);
329 company.parent_company = Some(parent_code.into());
330 company.ownership_percentage = Some(ownership_pct);
331 company
332 }
333}
334
335impl From<&CompanyCode> for Company {
336 fn from(cc: &CompanyCode) -> Self {
337 Self {
338 company_code: cc.code.clone(),
339 company_name: cc.name.clone(),
340 country: cc.country.clone(),
341 local_currency: cc.currency.clone(),
342 functional_currency: cc.currency.clone(),
343 is_parent: cc.is_group_parent,
344 parent_company: cc.parent_company.clone(),
345 ownership_percentage: None,
346 consolidation_method: ConsolidationMethod::Full,
347 }
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn test_company_creation() {
357 let company = CompanyCode::us("1000", "US Operations");
358 assert_eq!(company.code, "1000");
359 assert_eq!(company.currency, "USD");
360 assert_eq!(company.country, "US");
361 }
362
363 #[test]
364 fn test_enterprise_group() {
365 let mut group = EnterpriseGroup::new(
366 "CORP".to_string(),
367 "Global Corp".to_string(),
368 "USD".to_string(),
369 );
370
371 group.add_company(CompanyCode::us("1000", "US HQ"));
372 group.add_company(CompanyCode::de("2000", "EU Operations"));
373 group.add_intercompany_link("1000".to_string(), "2000".to_string());
374
375 assert_eq!(group.companies.len(), 2);
376 assert_eq!(group.get_intercompany_partners("1000"), vec!["2000"]);
377 }
378}