Skip to main content

datasynth_core/models/
financial_statements.rs

1//! Financial statement models for period-end reporting.
2
3use chrono::NaiveDate;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use uuid::Uuid;
8
9/// Type of financial statement.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum StatementType {
13    /// Balance Sheet (Statement of Financial Position)
14    BalanceSheet,
15    /// Income Statement (Profit & Loss)
16    IncomeStatement,
17    /// Cash Flow Statement
18    CashFlowStatement,
19    /// Statement of Changes in Equity
20    ChangesInEquity,
21}
22
23/// Basis of accounting for the statement.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
25#[serde(rename_all = "snake_case")]
26pub enum StatementBasis {
27    /// US GAAP
28    #[default]
29    UsGaap,
30    /// IFRS
31    Ifrs,
32    /// Statutory/local GAAP
33    Statutory,
34}
35
36/// Cash flow category for cash flow statement items.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum CashFlowCategory {
40    /// Operating activities
41    Operating,
42    /// Investing activities
43    Investing,
44    /// Financing activities
45    Financing,
46}
47
48/// A line item on a financial statement.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct FinancialStatementLineItem {
51    /// Line item code (e.g., "BS-CASH", "IS-REV")
52    pub line_code: String,
53    /// Display label
54    pub label: String,
55    /// Statement section (e.g., "Current Assets", "Revenue")
56    pub section: String,
57    /// Sort order within section
58    pub sort_order: u32,
59    /// Current period amount
60    #[serde(with = "rust_decimal::serde::str")]
61    pub amount: Decimal,
62    /// Prior period amount (for comparison)
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub amount_prior: Option<Decimal>,
65    /// Prior year amount for year-on-year comparative statements
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub prior_year_amount: Option<Decimal>,
68    /// Significant accounting assumptions underlying this line item
69    /// (e.g., goodwill, provisions, fair value measurements)
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub assumptions: Option<String>,
72    /// Indentation level for display hierarchy
73    pub indent_level: u8,
74    /// Whether this is a subtotal/total line
75    pub is_total: bool,
76    /// GL accounts that roll up to this line
77    pub gl_accounts: Vec<String>,
78}
79
80/// A cash flow item (indirect method).
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct CashFlowItem {
83    /// Item code
84    pub item_code: String,
85    /// Display label
86    pub label: String,
87    /// Cash flow category
88    pub category: CashFlowCategory,
89    /// Amount
90    #[serde(with = "rust_decimal::serde::str")]
91    pub amount: Decimal,
92    /// Prior period amount
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub amount_prior: Option<Decimal>,
95    /// Sort order
96    pub sort_order: u32,
97    /// Is this a subtotal line
98    pub is_total: bool,
99}
100
101/// A single line in a consolidation schedule, showing per-entity amounts plus
102/// pre-elimination total, elimination adjustments, and post-elimination total.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct ConsolidationLineItem {
105    /// Account category (e.g. "Revenue", "Cash", "Payables")
106    pub account_category: String,
107    /// Per-entity amounts: entity_code → net balance
108    #[serde(default)]
109    pub entity_amounts: HashMap<String, Decimal>,
110    /// Sum of all entity amounts before eliminations
111    #[serde(with = "rust_decimal::serde::str")]
112    pub pre_elimination_total: Decimal,
113    /// Net elimination adjustment (positive = increases total, negative = decreases)
114    #[serde(with = "rust_decimal::serde::str")]
115    pub elimination_adjustments: Decimal,
116    /// post_elimination_total = pre_elimination_total + elimination_adjustments
117    #[serde(with = "rust_decimal::serde::str")]
118    pub post_elimination_total: Decimal,
119}
120
121/// A consolidation schedule showing how individual entity amounts roll up into
122/// the consolidated group total with elimination entries applied.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ConsolidationSchedule {
125    /// Fiscal period label, e.g. "2024-Q1" or "2024-03"
126    pub period: String,
127    /// One line per account category
128    pub line_items: Vec<ConsolidationLineItem>,
129}
130
131// ============================================================================
132// IFRS 8 / ASC 280 — Operating Segment Reporting
133// ============================================================================
134
135/// Basis for how the entity defines its operating segments.
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum SegmentType {
139    /// Segments defined by geographic region (country / continent)
140    Geographic,
141    /// Segments defined by product or service line
142    ProductLine,
143    /// Segments that correspond to separate legal entities
144    LegalEntity,
145}
146
147/// A single IFRS 8 / ASC 280 reportable operating segment.
148///
149/// All monetary fields are expressed in the entity's reporting currency.
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct OperatingSegment {
152    /// Unique identifier for this segment record (deterministic UUID)
153    pub segment_id: String,
154    /// Human-readable segment name (e.g. "North America", "Software Products")
155    pub name: String,
156    /// Basis on which the segment is identified
157    pub segment_type: SegmentType,
158    /// Revenue from transactions with external customers
159    #[serde(with = "rust_decimal::serde::str")]
160    pub revenue_external: Decimal,
161    /// Revenue from transactions with other operating segments (eliminated on consolidation)
162    #[serde(with = "rust_decimal::serde::str")]
163    pub revenue_intersegment: Decimal,
164    /// Segment operating profit (before corporate overhead and group tax)
165    #[serde(with = "rust_decimal::serde::str")]
166    pub operating_profit: Decimal,
167    /// Total assets allocated to this segment
168    #[serde(with = "rust_decimal::serde::str")]
169    pub total_assets: Decimal,
170    /// Total liabilities allocated to this segment
171    #[serde(with = "rust_decimal::serde::str")]
172    pub total_liabilities: Decimal,
173    /// Capital expenditure (additions to PP&E and intangibles) in the period
174    #[serde(with = "rust_decimal::serde::str")]
175    pub capital_expenditure: Decimal,
176    /// Depreciation and amortisation charged in the period
177    #[serde(with = "rust_decimal::serde::str")]
178    pub depreciation_amortization: Decimal,
179    /// Fiscal period label for which these figures are reported (e.g. "2024-03")
180    pub period: String,
181    /// Company / group these segments belong to
182    pub company_code: String,
183}
184
185/// Reconciliation of segment totals to the consolidated financial statements
186/// as required by IFRS 8 para. 28 and ASC 280-10-50-30.
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct SegmentReconciliation {
189    /// Fiscal period label (e.g. "2024-03")
190    pub period: String,
191    /// Company / group code
192    pub company_code: String,
193    /// Sum of all reportable segment revenues (external + intersegment)
194    #[serde(with = "rust_decimal::serde::str")]
195    pub segment_revenue_total: Decimal,
196    /// Elimination of intersegment revenues (typically negative)
197    #[serde(with = "rust_decimal::serde::str")]
198    pub intersegment_eliminations: Decimal,
199    /// Consolidated external revenue = segment_revenue_total + intersegment_eliminations
200    #[serde(with = "rust_decimal::serde::str")]
201    pub consolidated_revenue: Decimal,
202    /// Sum of all reportable segment operating profits
203    #[serde(with = "rust_decimal::serde::str")]
204    pub segment_profit_total: Decimal,
205    /// Unallocated corporate overhead (negative amount)
206    #[serde(with = "rust_decimal::serde::str")]
207    pub corporate_overhead: Decimal,
208    /// Consolidated operating profit = segment_profit_total + corporate_overhead
209    #[serde(with = "rust_decimal::serde::str")]
210    pub consolidated_profit: Decimal,
211    /// Sum of all reportable segment assets
212    #[serde(with = "rust_decimal::serde::str")]
213    pub segment_assets_total: Decimal,
214    /// Unallocated corporate / group assets (e.g. deferred tax, goodwill)
215    #[serde(with = "rust_decimal::serde::str")]
216    pub unallocated_assets: Decimal,
217    /// Consolidated total assets = segment_assets_total + unallocated_assets
218    #[serde(with = "rust_decimal::serde::str")]
219    pub consolidated_assets: Decimal,
220}
221
222/// A complete financial statement.
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct FinancialStatement {
225    /// Unique statement identifier
226    pub statement_id: String,
227    /// Company code
228    pub company_code: String,
229    /// Statement type
230    pub statement_type: StatementType,
231    /// Accounting basis
232    pub basis: StatementBasis,
233    /// Reporting period start
234    pub period_start: NaiveDate,
235    /// Reporting period end
236    pub period_end: NaiveDate,
237    /// Fiscal year
238    pub fiscal_year: u16,
239    /// Fiscal period
240    pub fiscal_period: u8,
241    /// Line items
242    pub line_items: Vec<FinancialStatementLineItem>,
243    /// Cash flow items (only for CashFlowStatement)
244    pub cash_flow_items: Vec<CashFlowItem>,
245    /// Currency
246    pub currency: String,
247    /// Whether this is a consolidated statement
248    pub is_consolidated: bool,
249    /// Preparer ID
250    pub preparer_id: String,
251}
252
253// ============================================================================
254// Management Report — WI-7
255// ============================================================================
256
257/// A single KPI summary line within a management report.
258///
259/// Captures the actual vs. target comparison and a RAG (Red/Amber/Green) status
260/// for each metric tracked in the period.
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct KpiSummaryLine {
263    /// KPI metric name (e.g., "Revenue Growth Rate", "Gross Margin")
264    pub metric: String,
265    /// Actual value achieved in the period
266    #[serde(with = "rust_decimal::serde::str")]
267    pub actual: Decimal,
268    /// Target value set for the period
269    #[serde(with = "rust_decimal::serde::str")]
270    pub target: Decimal,
271    /// Variance as a percentage of target ((actual - target) / target)
272    pub variance_pct: f64,
273    /// Traffic-light status: "green" (< 5% variance), "amber" (< 10%), "red" (>= 10%)
274    pub rag_status: String,
275}
276
277/// A single budget variance line within a management report.
278///
279/// Shows the planned vs. actual for a GL account category and the resulting
280/// variance expressed both in absolute and percentage terms.
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct BudgetVarianceLine {
283    /// GL account code or category (e.g., "4000", "Revenue")
284    pub account: String,
285    /// Budgeted amount for the period
286    #[serde(with = "rust_decimal::serde::str")]
287    pub budget_amount: Decimal,
288    /// Actual amount recorded for the period
289    #[serde(with = "rust_decimal::serde::str")]
290    pub actual_amount: Decimal,
291    /// Variance = actual − budget
292    #[serde(with = "rust_decimal::serde::str")]
293    pub variance: Decimal,
294    /// Variance as a percentage of budget ((actual − budget) / budget)
295    pub variance_pct: f64,
296}
297
298/// A management report aggregating KPIs and budget variances for a period.
299///
300/// Management packs and board reports are the primary documents that auditors
301/// reference when performing analytical procedures (ISA 520) and understanding
302/// management's assessment of the business for the period.
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct ManagementReport {
305    /// Unique report identifier
306    pub report_id: Uuid,
307    /// Report type: "monthly_pack", "board_report", "forecast", "flash_report"
308    pub report_type: String,
309    /// Fiscal period label (e.g., "2025-Q1", "2025-01")
310    pub period: String,
311    /// Entity this report belongs to
312    pub entity_code: String,
313    /// Employee / role ID of the preparer
314    pub prepared_by: String,
315    /// Date the report was prepared
316    pub prepared_date: NaiveDate,
317    /// KPI summary lines (6–10 metrics)
318    pub kpi_summary: Vec<KpiSummaryLine>,
319    /// Budget variance lines (8–15 accounts)
320    pub budget_variances: Vec<BudgetVarianceLine>,
321    /// Narrative management commentary for the period
322    pub commentary: String,
323}
324
325// ---------------------------------------------------------------------------
326// Tests — WI-8: FinancialStatementLineItem comparative fields
327// ---------------------------------------------------------------------------
328
329#[cfg(test)]
330#[allow(clippy::unwrap_used)]
331mod tests {
332    use super::*;
333    use rust_decimal_macros::dec;
334
335    fn make_line_item(
336        amount: Decimal,
337        prior_year: Option<Decimal>,
338        assumptions: Option<&str>,
339    ) -> FinancialStatementLineItem {
340        FinancialStatementLineItem {
341            line_code: "BS-CASH".to_string(),
342            label: "Cash and Cash Equivalents".to_string(),
343            section: "Current Assets".to_string(),
344            sort_order: 1,
345            amount,
346            amount_prior: None,
347            prior_year_amount: prior_year,
348            assumptions: assumptions.map(|s| s.to_string()),
349            indent_level: 0,
350            is_total: false,
351            gl_accounts: vec![],
352        }
353    }
354
355    #[test]
356    fn test_prior_year_amount_field_present() {
357        let item = make_line_item(dec!(100_000), Some(dec!(95_000)), None);
358        assert_eq!(item.prior_year_amount, Some(dec!(95_000)));
359    }
360
361    #[test]
362    fn test_assumptions_present_for_estimate_heavy_line() {
363        let assumption_text = "Based on discounted cash flow analysis";
364        let item = make_line_item(dec!(500_000), Some(dec!(480_000)), Some(assumption_text));
365        assert!(item.assumptions.is_some());
366        assert_eq!(item.assumptions.as_deref(), Some(assumption_text));
367    }
368
369    #[test]
370    fn test_prior_year_amounts_are_within_30_pct_of_current() {
371        // Verify that representative prior-year amounts are plausible
372        // (within 30% of the current-year amount, per WI-8 spec).
373        let cases: &[(Decimal, Decimal)] = &[
374            (dec!(100_000), dec!(85_000)),  // -15% — within bounds
375            (dec!(200_000), dec!(230_000)), // +15% — within bounds
376            (dec!(50_000), dec!(35_100)),   // -29.8% — within bounds
377        ];
378        for (current, prior) in cases {
379            let ratio = ((prior - current).abs() / current)
380                .to_string()
381                .parse::<f64>()
382                .unwrap_or(1.0);
383            assert!(
384                ratio <= 0.30,
385                "Prior year amount {prior} is more than 30% away from current {current} \
386                 (ratio={ratio:.3})"
387            );
388        }
389    }
390}