Skip to main content

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