datasynth_core/models/balance/
opening_balance.rs

1//! Opening balance specification models.
2//!
3//! Provides structures for defining and generating coherent opening balance sheets
4//! with industry-specific compositions and configurable financial ratios.
5
6use chrono::NaiveDate;
7use rust_decimal::Decimal;
8use rust_decimal_macros::dec;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use super::account_balance::AccountType;
13use super::trial_balance::AccountCategory;
14
15/// Specification for generating opening balances.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct OpeningBalanceSpec {
18    /// Company code.
19    pub company_code: String,
20    /// Opening balance date (typically start of fiscal year).
21    pub as_of_date: NaiveDate,
22    /// Fiscal year.
23    pub fiscal_year: i32,
24    /// Currency.
25    pub currency: String,
26    /// Total assets target.
27    pub total_assets: Decimal,
28    /// Industry sector for composition.
29    pub industry: IndustryType,
30    /// Asset composition specification.
31    pub asset_composition: AssetComposition,
32    /// Liability and equity specification.
33    pub capital_structure: CapitalStructure,
34    /// Target financial ratios.
35    pub target_ratios: TargetRatios,
36    /// Individual account specifications (overrides).
37    pub account_overrides: HashMap<String, AccountSpec>,
38}
39
40impl OpeningBalanceSpec {
41    /// Create a new opening balance specification.
42    pub fn new(
43        company_code: String,
44        as_of_date: NaiveDate,
45        fiscal_year: i32,
46        currency: String,
47        total_assets: Decimal,
48        industry: IndustryType,
49    ) -> Self {
50        Self {
51            company_code,
52            as_of_date,
53            fiscal_year,
54            currency,
55            total_assets,
56            industry,
57            asset_composition: AssetComposition::for_industry(industry),
58            capital_structure: CapitalStructure::default(),
59            target_ratios: TargetRatios::for_industry(industry),
60            account_overrides: HashMap::new(),
61        }
62    }
63
64    /// Create a specification for a given industry with default parameters.
65    /// This is a convenience method for creating industry-specific opening balances.
66    pub fn for_industry(total_assets: Decimal, industry: IndustryType) -> Self {
67        Self {
68            company_code: String::new(),
69            as_of_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
70            fiscal_year: 2024,
71            currency: "USD".to_string(),
72            total_assets,
73            industry,
74            asset_composition: AssetComposition::for_industry(industry),
75            capital_structure: CapitalStructure::for_industry(industry),
76            target_ratios: TargetRatios::for_industry(industry),
77            account_overrides: HashMap::new(),
78        }
79    }
80
81    /// Validate that the specification is coherent (A = L + E).
82    pub fn validate(&self) -> Result<(), Vec<String>> {
83        let mut errors = Vec::new();
84
85        // Check asset composition sums to 100%
86        let asset_total = self.asset_composition.total_percentage();
87        if (asset_total - dec!(100)).abs() > dec!(0.01) {
88            errors.push(format!(
89                "Asset composition should sum to 100%, got {}%",
90                asset_total
91            ));
92        }
93
94        // Check capital structure sums to 100%
95        let capital_total =
96            self.capital_structure.debt_percent + self.capital_structure.equity_percent;
97        if (capital_total - dec!(100)).abs() > dec!(0.01) {
98            errors.push(format!(
99                "Capital structure should sum to 100%, got {}%",
100                capital_total
101            ));
102        }
103
104        // Check current ratio feasibility
105        if self.target_ratios.current_ratio < dec!(0.5) {
106            errors.push("Current ratio below 0.5 indicates severe liquidity problems".to_string());
107        }
108
109        if errors.is_empty() {
110            Ok(())
111        } else {
112            Err(errors)
113        }
114    }
115
116    /// Calculate total liabilities based on capital structure.
117    pub fn calculate_total_liabilities(&self) -> Decimal {
118        self.total_assets * self.capital_structure.debt_percent / dec!(100)
119    }
120
121    /// Calculate total equity based on capital structure.
122    pub fn calculate_total_equity(&self) -> Decimal {
123        self.total_assets * self.capital_structure.equity_percent / dec!(100)
124    }
125
126    /// Calculate current assets based on composition.
127    pub fn calculate_current_assets(&self) -> Decimal {
128        self.total_assets * self.asset_composition.current_assets_percent / dec!(100)
129    }
130
131    /// Calculate non-current assets based on composition.
132    pub fn calculate_non_current_assets(&self) -> Decimal {
133        self.total_assets * (dec!(100) - self.asset_composition.current_assets_percent) / dec!(100)
134    }
135
136    /// Calculate current liabilities to achieve target current ratio.
137    pub fn calculate_current_liabilities(&self) -> Decimal {
138        let current_assets = self.calculate_current_assets();
139        if self.target_ratios.current_ratio > Decimal::ZERO {
140            current_assets / self.target_ratios.current_ratio
141        } else {
142            Decimal::ZERO
143        }
144    }
145}
146
147/// Industry type for composition defaults.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
149#[serde(rename_all = "snake_case")]
150pub enum IndustryType {
151    /// Manufacturing company.
152    #[default]
153    Manufacturing,
154    /// Retail/wholesale trade.
155    Retail,
156    /// Service company.
157    Services,
158    /// Technology company.
159    Technology,
160    /// Financial services.
161    Financial,
162    /// Healthcare.
163    Healthcare,
164    /// Utilities.
165    Utilities,
166    /// Real estate.
167    RealEstate,
168}
169
170/// Asset composition specification.
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct AssetComposition {
173    /// Current assets as percentage of total.
174    pub current_assets_percent: Decimal,
175    /// Cash and equivalents as % of current assets.
176    pub cash_percent: Decimal,
177    /// Accounts receivable as % of current assets.
178    pub ar_percent: Decimal,
179    /// Inventory as % of current assets.
180    pub inventory_percent: Decimal,
181    /// Prepaid expenses as % of current assets.
182    pub prepaid_percent: Decimal,
183    /// Other current assets as % of current assets.
184    pub other_current_percent: Decimal,
185    /// Property, plant, equipment as % of non-current assets.
186    pub ppe_percent: Decimal,
187    /// Intangible assets as % of non-current assets.
188    pub intangibles_percent: Decimal,
189    /// Investments as % of non-current assets.
190    pub investments_percent: Decimal,
191    /// Other non-current assets as % of non-current assets.
192    pub other_noncurrent_percent: Decimal,
193}
194
195impl AssetComposition {
196    /// Get composition for a specific industry.
197    pub fn for_industry(industry: IndustryType) -> Self {
198        match industry {
199            IndustryType::Manufacturing => Self {
200                current_assets_percent: dec!(40),
201                cash_percent: dec!(15),
202                ar_percent: dec!(30),
203                inventory_percent: dec!(45),
204                prepaid_percent: dec!(5),
205                other_current_percent: dec!(5),
206                ppe_percent: dec!(70),
207                intangibles_percent: dec!(10),
208                investments_percent: dec!(10),
209                other_noncurrent_percent: dec!(10),
210            },
211            IndustryType::Retail => Self {
212                current_assets_percent: dec!(55),
213                cash_percent: dec!(10),
214                ar_percent: dec!(15),
215                inventory_percent: dec!(65),
216                prepaid_percent: dec!(5),
217                other_current_percent: dec!(5),
218                ppe_percent: dec!(60),
219                intangibles_percent: dec!(20),
220                investments_percent: dec!(10),
221                other_noncurrent_percent: dec!(10),
222            },
223            IndustryType::Services => Self {
224                current_assets_percent: dec!(50),
225                cash_percent: dec!(25),
226                ar_percent: dec!(50),
227                inventory_percent: dec!(5),
228                prepaid_percent: dec!(10),
229                other_current_percent: dec!(10),
230                ppe_percent: dec!(40),
231                intangibles_percent: dec!(30),
232                investments_percent: dec!(15),
233                other_noncurrent_percent: dec!(15),
234            },
235            IndustryType::Technology => Self {
236                current_assets_percent: dec!(60),
237                cash_percent: dec!(40),
238                ar_percent: dec!(35),
239                inventory_percent: dec!(5),
240                prepaid_percent: dec!(10),
241                other_current_percent: dec!(10),
242                ppe_percent: dec!(25),
243                intangibles_percent: dec!(50),
244                investments_percent: dec!(15),
245                other_noncurrent_percent: dec!(10),
246            },
247            IndustryType::Financial => Self {
248                current_assets_percent: dec!(70),
249                cash_percent: dec!(30),
250                ar_percent: dec!(40),
251                inventory_percent: dec!(0),
252                prepaid_percent: dec!(5),
253                other_current_percent: dec!(25),
254                ppe_percent: dec!(20),
255                intangibles_percent: dec!(30),
256                investments_percent: dec!(40),
257                other_noncurrent_percent: dec!(10),
258            },
259            IndustryType::Healthcare => Self {
260                current_assets_percent: dec!(35),
261                cash_percent: dec!(20),
262                ar_percent: dec!(50),
263                inventory_percent: dec!(15),
264                prepaid_percent: dec!(10),
265                other_current_percent: dec!(5),
266                ppe_percent: dec!(60),
267                intangibles_percent: dec!(20),
268                investments_percent: dec!(10),
269                other_noncurrent_percent: dec!(10),
270            },
271            IndustryType::Utilities => Self {
272                current_assets_percent: dec!(15),
273                cash_percent: dec!(20),
274                ar_percent: dec!(50),
275                inventory_percent: dec!(15),
276                prepaid_percent: dec!(10),
277                other_current_percent: dec!(5),
278                ppe_percent: dec!(85),
279                intangibles_percent: dec!(5),
280                investments_percent: dec!(5),
281                other_noncurrent_percent: dec!(5),
282            },
283            IndustryType::RealEstate => Self {
284                current_assets_percent: dec!(10),
285                cash_percent: dec!(30),
286                ar_percent: dec!(40),
287                inventory_percent: dec!(10),
288                prepaid_percent: dec!(10),
289                other_current_percent: dec!(10),
290                ppe_percent: dec!(90),
291                intangibles_percent: dec!(3),
292                investments_percent: dec!(5),
293                other_noncurrent_percent: dec!(2),
294            },
295        }
296    }
297
298    /// Get total percentage (should be 100%).
299    pub fn total_percentage(&self) -> Decimal {
300        // Current assets composition should sum to 100%
301        let current = self.cash_percent
302            + self.ar_percent
303            + self.inventory_percent
304            + self.prepaid_percent
305            + self.other_current_percent;
306
307        // Non-current assets composition should sum to 100%
308        let noncurrent = self.ppe_percent
309            + self.intangibles_percent
310            + self.investments_percent
311            + self.other_noncurrent_percent;
312
313        // Both should be approximately 100%
314        if (current - dec!(100)).abs() > dec!(1) || (noncurrent - dec!(100)).abs() > dec!(1) {
315            // Return a value that will fail validation
316            current
317        } else {
318            dec!(100)
319        }
320    }
321}
322
323impl Default for AssetComposition {
324    fn default() -> Self {
325        Self::for_industry(IndustryType::Manufacturing)
326    }
327}
328
329/// Capital structure specification.
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct CapitalStructure {
332    /// Total debt as percentage of total assets.
333    pub debt_percent: Decimal,
334    /// Total equity as percentage of total assets.
335    pub equity_percent: Decimal,
336    /// Current liabilities as % of total liabilities.
337    pub current_liabilities_percent: Decimal,
338    /// Long-term debt as % of total liabilities.
339    pub long_term_debt_percent: Decimal,
340    /// Other liabilities as % of total liabilities.
341    pub other_liabilities_percent: Decimal,
342    /// Common stock as % of equity.
343    pub common_stock_percent: Decimal,
344    /// Additional paid-in capital as % of equity.
345    pub apic_percent: Decimal,
346    /// Retained earnings as % of equity.
347    pub retained_earnings_percent: Decimal,
348    /// Other equity as % of equity.
349    pub other_equity_percent: Decimal,
350}
351
352impl Default for CapitalStructure {
353    fn default() -> Self {
354        Self {
355            debt_percent: dec!(40),
356            equity_percent: dec!(60),
357            current_liabilities_percent: dec!(50),
358            long_term_debt_percent: dec!(40),
359            other_liabilities_percent: dec!(10),
360            common_stock_percent: dec!(15),
361            apic_percent: dec!(25),
362            retained_earnings_percent: dec!(55),
363            other_equity_percent: dec!(5),
364        }
365    }
366}
367
368impl CapitalStructure {
369    /// Get capital structure for a specific industry.
370    pub fn for_industry(industry: IndustryType) -> Self {
371        match industry {
372            IndustryType::Manufacturing => Self {
373                debt_percent: dec!(40),
374                equity_percent: dec!(60),
375                current_liabilities_percent: dec!(50),
376                long_term_debt_percent: dec!(40),
377                other_liabilities_percent: dec!(10),
378                common_stock_percent: dec!(15),
379                apic_percent: dec!(25),
380                retained_earnings_percent: dec!(55),
381                other_equity_percent: dec!(5),
382            },
383            IndustryType::Retail => Self {
384                debt_percent: dec!(45),
385                equity_percent: dec!(55),
386                current_liabilities_percent: dec!(60),
387                long_term_debt_percent: dec!(30),
388                other_liabilities_percent: dec!(10),
389                common_stock_percent: dec!(20),
390                apic_percent: dec!(20),
391                retained_earnings_percent: dec!(55),
392                other_equity_percent: dec!(5),
393            },
394            IndustryType::Services => Self {
395                debt_percent: dec!(30),
396                equity_percent: dec!(70),
397                current_liabilities_percent: dec!(55),
398                long_term_debt_percent: dec!(35),
399                other_liabilities_percent: dec!(10),
400                common_stock_percent: dec!(15),
401                apic_percent: dec!(30),
402                retained_earnings_percent: dec!(50),
403                other_equity_percent: dec!(5),
404            },
405            IndustryType::Technology => Self {
406                debt_percent: dec!(25),
407                equity_percent: dec!(75),
408                current_liabilities_percent: dec!(60),
409                long_term_debt_percent: dec!(30),
410                other_liabilities_percent: dec!(10),
411                common_stock_percent: dec!(10),
412                apic_percent: dec!(40),
413                retained_earnings_percent: dec!(45),
414                other_equity_percent: dec!(5),
415            },
416            IndustryType::Financial => Self {
417                debt_percent: dec!(70),
418                equity_percent: dec!(30),
419                current_liabilities_percent: dec!(70),
420                long_term_debt_percent: dec!(20),
421                other_liabilities_percent: dec!(10),
422                common_stock_percent: dec!(25),
423                apic_percent: dec!(35),
424                retained_earnings_percent: dec!(35),
425                other_equity_percent: dec!(5),
426            },
427            IndustryType::Healthcare => Self {
428                debt_percent: dec!(35),
429                equity_percent: dec!(65),
430                current_liabilities_percent: dec!(50),
431                long_term_debt_percent: dec!(40),
432                other_liabilities_percent: dec!(10),
433                common_stock_percent: dec!(15),
434                apic_percent: dec!(30),
435                retained_earnings_percent: dec!(50),
436                other_equity_percent: dec!(5),
437            },
438            IndustryType::Utilities => Self {
439                debt_percent: dec!(55),
440                equity_percent: dec!(45),
441                current_liabilities_percent: dec!(35),
442                long_term_debt_percent: dec!(55),
443                other_liabilities_percent: dec!(10),
444                common_stock_percent: dec!(20),
445                apic_percent: dec!(25),
446                retained_earnings_percent: dec!(50),
447                other_equity_percent: dec!(5),
448            },
449            IndustryType::RealEstate => Self {
450                debt_percent: dec!(60),
451                equity_percent: dec!(40),
452                current_liabilities_percent: dec!(30),
453                long_term_debt_percent: dec!(60),
454                other_liabilities_percent: dec!(10),
455                common_stock_percent: dec!(25),
456                apic_percent: dec!(30),
457                retained_earnings_percent: dec!(40),
458                other_equity_percent: dec!(5),
459            },
460        }
461    }
462
463    /// Create capital structure with specific debt-to-equity ratio.
464    pub fn with_debt_equity_ratio(ratio: Decimal) -> Self {
465        // D/E = debt_percent / equity_percent
466        // debt_percent + equity_percent = 100
467        // debt_percent = ratio * equity_percent
468        // ratio * equity_percent + equity_percent = 100
469        // equity_percent = 100 / (1 + ratio)
470        let equity_percent = dec!(100) / (Decimal::ONE + ratio);
471        let debt_percent = dec!(100) - equity_percent;
472
473        Self {
474            debt_percent,
475            equity_percent,
476            ..Default::default()
477        }
478    }
479
480    /// Get debt-to-equity ratio.
481    pub fn debt_equity_ratio(&self) -> Decimal {
482        if self.equity_percent > Decimal::ZERO {
483            self.debt_percent / self.equity_percent
484        } else {
485            Decimal::MAX
486        }
487    }
488}
489
490/// Target financial ratios for opening balance.
491#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct TargetRatios {
493    /// Current ratio (Current Assets / Current Liabilities).
494    pub current_ratio: Decimal,
495    /// Quick ratio ((Current Assets - Inventory) / Current Liabilities).
496    pub quick_ratio: Decimal,
497    /// Debt-to-equity ratio (Total Liabilities / Total Equity).
498    pub debt_to_equity: Decimal,
499    /// Asset turnover (Revenue / Total Assets) - for planning.
500    pub asset_turnover: Decimal,
501    /// Days Sales Outstanding (AR / Revenue * 365).
502    pub target_dso_days: u32,
503    /// Days Payable Outstanding (AP / COGS * 365).
504    pub target_dpo_days: u32,
505    /// Days Inventory Outstanding (Inventory / COGS * 365).
506    pub target_dio_days: u32,
507    /// Gross margin ((Revenue - COGS) / Revenue).
508    pub gross_margin: Decimal,
509    /// Operating margin (Operating Income / Revenue).
510    pub operating_margin: Decimal,
511}
512
513impl TargetRatios {
514    /// Get target ratios for a specific industry.
515    pub fn for_industry(industry: IndustryType) -> Self {
516        match industry {
517            IndustryType::Manufacturing => Self {
518                current_ratio: dec!(1.5),
519                quick_ratio: dec!(0.8),
520                debt_to_equity: dec!(0.6),
521                asset_turnover: dec!(1.2),
522                target_dso_days: 45,
523                target_dpo_days: 35,
524                target_dio_days: 60,
525                gross_margin: dec!(0.35),
526                operating_margin: dec!(0.12),
527            },
528            IndustryType::Retail => Self {
529                current_ratio: dec!(1.2),
530                quick_ratio: dec!(0.4),
531                debt_to_equity: dec!(0.8),
532                asset_turnover: dec!(2.5),
533                target_dso_days: 15,
534                target_dpo_days: 30,
535                target_dio_days: 45,
536                gross_margin: dec!(0.30),
537                operating_margin: dec!(0.08),
538            },
539            IndustryType::Services => Self {
540                current_ratio: dec!(1.8),
541                quick_ratio: dec!(1.6),
542                debt_to_equity: dec!(0.4),
543                asset_turnover: dec!(1.5),
544                target_dso_days: 60,
545                target_dpo_days: 25,
546                target_dio_days: 0,
547                gross_margin: dec!(0.45),
548                operating_margin: dec!(0.18),
549            },
550            IndustryType::Technology => Self {
551                current_ratio: dec!(2.5),
552                quick_ratio: dec!(2.3),
553                debt_to_equity: dec!(0.3),
554                asset_turnover: dec!(0.8),
555                target_dso_days: 55,
556                target_dpo_days: 40,
557                target_dio_days: 15,
558                gross_margin: dec!(0.65),
559                operating_margin: dec!(0.25),
560            },
561            IndustryType::Financial => Self {
562                current_ratio: dec!(1.1),
563                quick_ratio: dec!(1.1),
564                debt_to_equity: dec!(2.0),
565                asset_turnover: dec!(0.3),
566                target_dso_days: 30,
567                target_dpo_days: 20,
568                target_dio_days: 0,
569                gross_margin: dec!(0.80),
570                operating_margin: dec!(0.30),
571            },
572            IndustryType::Healthcare => Self {
573                current_ratio: dec!(1.4),
574                quick_ratio: dec!(1.1),
575                debt_to_equity: dec!(0.5),
576                asset_turnover: dec!(1.0),
577                target_dso_days: 50,
578                target_dpo_days: 30,
579                target_dio_days: 30,
580                gross_margin: dec!(0.40),
581                operating_margin: dec!(0.15),
582            },
583            IndustryType::Utilities => Self {
584                current_ratio: dec!(0.9),
585                quick_ratio: dec!(0.7),
586                debt_to_equity: dec!(1.2),
587                asset_turnover: dec!(0.4),
588                target_dso_days: 40,
589                target_dpo_days: 45,
590                target_dio_days: 20,
591                gross_margin: dec!(0.35),
592                operating_margin: dec!(0.20),
593            },
594            IndustryType::RealEstate => Self {
595                current_ratio: dec!(1.0),
596                quick_ratio: dec!(0.8),
597                debt_to_equity: dec!(1.5),
598                asset_turnover: dec!(0.2),
599                target_dso_days: 30,
600                target_dpo_days: 25,
601                target_dio_days: 0,
602                gross_margin: dec!(0.50),
603                operating_margin: dec!(0.35),
604            },
605        }
606    }
607
608    /// Calculate target AR balance from revenue.
609    pub fn calculate_target_ar(&self, annual_revenue: Decimal) -> Decimal {
610        annual_revenue * Decimal::from(self.target_dso_days) / dec!(365)
611    }
612
613    /// Calculate target AP balance from COGS.
614    pub fn calculate_target_ap(&self, annual_cogs: Decimal) -> Decimal {
615        annual_cogs * Decimal::from(self.target_dpo_days) / dec!(365)
616    }
617
618    /// Calculate target inventory balance from COGS.
619    pub fn calculate_target_inventory(&self, annual_cogs: Decimal) -> Decimal {
620        annual_cogs * Decimal::from(self.target_dio_days) / dec!(365)
621    }
622}
623
624impl Default for TargetRatios {
625    fn default() -> Self {
626        Self::for_industry(IndustryType::Manufacturing)
627    }
628}
629
630/// Individual account specification override.
631#[derive(Debug, Clone, Serialize, Deserialize)]
632pub struct AccountSpec {
633    /// Account code.
634    pub account_code: String,
635    /// Account description.
636    pub description: String,
637    /// Account type.
638    pub account_type: AccountType,
639    /// Category.
640    pub category: AccountCategory,
641    /// Fixed balance amount (overrides calculated).
642    pub fixed_balance: Option<Decimal>,
643    /// Percentage of category total.
644    pub category_percent: Option<Decimal>,
645    /// Percentage of total assets.
646    pub total_assets_percent: Option<Decimal>,
647}
648
649/// Generated opening balance result.
650#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct GeneratedOpeningBalance {
652    /// Company code.
653    pub company_code: String,
654    /// As-of date.
655    pub as_of_date: NaiveDate,
656    /// Individual account balances.
657    pub balances: HashMap<String, Decimal>,
658    /// Total assets.
659    pub total_assets: Decimal,
660    /// Total liabilities.
661    pub total_liabilities: Decimal,
662    /// Total equity.
663    pub total_equity: Decimal,
664    /// Is balanced (A = L + E)?
665    pub is_balanced: bool,
666    /// Calculated ratios.
667    pub calculated_ratios: CalculatedRatios,
668}
669
670/// Calculated ratios from generated balances.
671#[derive(Debug, Clone, Serialize, Deserialize)]
672pub struct CalculatedRatios {
673    /// Current ratio.
674    pub current_ratio: Option<Decimal>,
675    /// Quick ratio.
676    pub quick_ratio: Option<Decimal>,
677    /// Debt-to-equity ratio.
678    pub debt_to_equity: Option<Decimal>,
679    /// Working capital.
680    pub working_capital: Decimal,
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    #[test]
688    fn test_opening_balance_spec_creation() {
689        let spec = OpeningBalanceSpec::new(
690            "1000".to_string(),
691            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
692            2022,
693            "USD".to_string(),
694            dec!(1000000),
695            IndustryType::Manufacturing,
696        );
697
698        assert!(spec.validate().is_ok());
699        assert_eq!(spec.calculate_total_liabilities(), dec!(400000)); // 40%
700        assert_eq!(spec.calculate_total_equity(), dec!(600000)); // 60%
701    }
702
703    #[test]
704    fn test_capital_structure_debt_equity() {
705        let structure = CapitalStructure::with_debt_equity_ratio(dec!(0.5));
706
707        // D/E = 0.5, so D = 0.5E, D + E = 100
708        // 0.5E + E = 100 -> 1.5E = 100 -> E = 66.67
709        assert!((structure.equity_percent - dec!(66.67)).abs() < dec!(0.01));
710        assert!((structure.debt_percent - dec!(33.33)).abs() < dec!(0.01));
711        assert!((structure.debt_equity_ratio() - dec!(0.5)).abs() < dec!(0.01));
712    }
713
714    #[test]
715    fn test_asset_composition_for_industries() {
716        let manufacturing = AssetComposition::for_industry(IndustryType::Manufacturing);
717        assert_eq!(manufacturing.current_assets_percent, dec!(40));
718
719        let retail = AssetComposition::for_industry(IndustryType::Retail);
720        assert_eq!(retail.current_assets_percent, dec!(55));
721        assert!(retail.inventory_percent > manufacturing.inventory_percent);
722
723        let technology = AssetComposition::for_industry(IndustryType::Technology);
724        assert!(technology.intangibles_percent > manufacturing.intangibles_percent);
725    }
726
727    #[test]
728    fn test_target_ratios_calculations() {
729        let ratios = TargetRatios::for_industry(IndustryType::Manufacturing);
730
731        let annual_revenue = dec!(1000000);
732        let annual_cogs = dec!(650000); // 35% gross margin
733
734        let target_ar = ratios.calculate_target_ar(annual_revenue);
735        // 1,000,000 * 45 / 365 ≈ 123,288
736        assert!(target_ar > dec!(120000) && target_ar < dec!(130000));
737
738        let target_inventory = ratios.calculate_target_inventory(annual_cogs);
739        // 650,000 * 60 / 365 ≈ 106,849
740        assert!(target_inventory > dec!(100000) && target_inventory < dec!(115000));
741    }
742
743    #[test]
744    fn test_opening_balance_validation() {
745        let mut spec = OpeningBalanceSpec::new(
746            "1000".to_string(),
747            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
748            2022,
749            "USD".to_string(),
750            dec!(1000000),
751            IndustryType::Manufacturing,
752        );
753
754        // Valid spec
755        assert!(spec.validate().is_ok());
756
757        // Invalid capital structure
758        spec.capital_structure.debt_percent = dec!(80);
759        spec.capital_structure.equity_percent = dec!(30); // Sums to 110%
760        assert!(spec.validate().is_err());
761    }
762}