From e925bb218d080ac2643d825671f222d1705db22e Mon Sep 17 00:00:00 2001 From: francy51 Date: Sun, 12 Apr 2026 14:50:34 -0400 Subject: [PATCH] Remove dead CompanyFacts earnings code and eliminate all compiler warnings Remove ~400 lines of unused code from facts.rs that was left behind after migrating to the filing-native XBRL approach for the /em command. - Remove dead earnings functions (build_earnings_periods, etc.) - Remove unused utility functions (month_abbreviation, duration_value_for_period, etc.) - Remove unused concept constants (BASIC_EPS_CONCEPTS, COST_OF_SALES_CONCEPTS, etc.) - Simplify OverlayPeriodKind enum to only keep Any variant - Fix unused variable warnings in dei.rs and types.rs - Update test mocks to remove obsolete fields Result: 0 compiler warnings (down from 35) Co-Authored-By: Claude Opus 4.6 --- .../src-tauri/src/terminal/command_service.rs | 2 - .../src/terminal/sec_edgar/earnings/dei.rs | 2 +- .../src-tauri/src/terminal/sec_edgar/facts.rs | 932 +----------------- .../src-tauri/src/terminal/sec_edgar/types.rs | 2 + 4 files changed, 6 insertions(+), 932 deletions(-) diff --git a/MosaicIQ/src-tauri/src/terminal/command_service.rs b/MosaicIQ/src-tauri/src/terminal/command_service.rs index 63b678c..c7b879d 100644 --- a/MosaicIQ/src-tauri/src/terminal/command_service.rs +++ b/MosaicIQ/src-tauri/src/terminal/command_service.rs @@ -1228,7 +1228,6 @@ mod tests { latest_filing: None, source_status: SourceStatus { companyfacts_used: true, - latest_xbrl_parsed: false, degraded_reason: None, }, }) @@ -1253,7 +1252,6 @@ mod tests { latest_filing: None, source_status: SourceStatus { companyfacts_used: true, - latest_xbrl_parsed: false, degraded_reason: None, }, }) diff --git a/MosaicIQ/src-tauri/src/terminal/sec_edgar/earnings/dei.rs b/MosaicIQ/src-tauri/src/terminal/sec_edgar/earnings/dei.rs index 149e10f..b86c46d 100644 --- a/MosaicIQ/src-tauri/src/terminal/sec_edgar/earnings/dei.rs +++ b/MosaicIQ/src-tauri/src/terminal/sec_edgar/earnings/dei.rs @@ -17,7 +17,7 @@ fn extract_fiscal_year_from_period_end(period_end: &str) -> Option { /// Helper to infer fiscal period from filing form and period dates fn infer_fiscal_period_from_duration( - period_start: &str, + _period_start: &str, period_end: &str, frequency: Frequency, ) -> Option { diff --git a/MosaicIQ/src-tauri/src/terminal/sec_edgar/facts.rs b/MosaicIQ/src-tauri/src/terminal/sec_edgar/facts.rs index 5a9caae..17769e9 100644 --- a/MosaicIQ/src-tauri/src/terminal/sec_edgar/facts.rs +++ b/MosaicIQ/src-tauri/src/terminal/sec_edgar/facts.rs @@ -3,14 +3,13 @@ use std::collections::{BTreeMap, HashMap}; use chrono::NaiveDate; use crate::terminal::{ - CashFlowPeriod, DividendEvent, EarningsPeriod, EarningsPeriodOption, FilingRef, Frequency, - SourceStatus, StatementPeriod, + CashFlowPeriod, DividendEvent, FilingRef, Frequency, SourceStatus, StatementPeriod, }; use super::service::EdgarLookupError; use super::types::{ CompanyConceptFacts, CompanyFactRecord, CompanyFactsResponse, ConceptCandidate, - EarningsPeriodKey, NormalizedFact, ParsedXbrlDocument, PeriodRow, ResolvedCompany, UnitFamily, + NormalizedFact, ParsedXbrlDocument, PeriodRow, ResolvedCompany, UnitFamily, }; pub(crate) const DEFAULT_ANNUAL_EARNINGS_PERIODS: usize = 4; @@ -60,125 +59,6 @@ pub(crate) const DILUTED_EPS_CONCEPTS: &[ConceptCandidate] = &[ UnitFamily::CurrencyPerShare, ), ]; -pub(crate) const BASIC_EPS_CONCEPTS: &[ConceptCandidate] = &[ - candidate( - "us-gaap", - "EarningsPerShareBasic", - UnitFamily::CurrencyPerShare, - ), - candidate( - "ifrs-full", - "BasicEarningsLossPerShare", - UnitFamily::CurrencyPerShare, - ), -]; -pub(crate) const COST_OF_SALES_CONCEPTS: &[ConceptCandidate] = &[ - candidate("us-gaap", "CostOfRevenue", UnitFamily::Currency), - candidate("us-gaap", "CostOfGoodsSold", UnitFamily::Currency), - candidate("us-gaap", "CostOfSales", UnitFamily::Currency), -]; -pub(crate) const SGA_CONCEPTS: &[ConceptCandidate] = &[candidate( - "us-gaap", - "SellingGeneralAndAdministrativeExpense", - UnitFamily::Currency, -)]; -pub(crate) const SELLING_AND_MARKETING_CONCEPTS: &[ConceptCandidate] = &[candidate( - "us-gaap", - "SellingAndMarketingExpense", - UnitFamily::Currency, -)]; -pub(crate) const GENERAL_AND_ADMINISTRATIVE_CONCEPTS: &[ConceptCandidate] = &[candidate( - "us-gaap", - "GeneralAndAdministrativeExpense", - UnitFamily::Currency, -)]; -pub(crate) const R_AND_D_CONCEPTS: &[ConceptCandidate] = &[ - candidate( - "us-gaap", - "ResearchAndDevelopmentExpense", - UnitFamily::Currency, - ), - candidate( - "ifrs-full", - "ResearchAndDevelopmentExpense", - UnitFamily::Currency, - ), -]; -pub(crate) const OPERATING_EXPENSES_CONCEPTS: &[ConceptCandidate] = &[ - candidate("us-gaap", "OperatingExpenses", UnitFamily::Currency), - candidate("us-gaap", "OperatingExpense", UnitFamily::Currency), -]; -pub(crate) const OTHER_NON_OPERATING_INCOME_CONCEPTS: &[ConceptCandidate] = &[candidate( - "us-gaap", - "OtherNonoperatingIncomeExpense", - UnitFamily::Currency, -)]; -pub(crate) const TOTAL_NON_OPERATING_INCOME_CONCEPTS: &[ConceptCandidate] = &[candidate( - "us-gaap", - "NonoperatingIncomeExpense", - UnitFamily::Currency, -)]; -pub(crate) const PRETAX_INCOME_CONCEPTS: &[ConceptCandidate] = &[ - candidate( - "us-gaap", - "IncomeLossFromContinuingOperationsBeforeIncomeTaxesExtraordinaryItemsNoncontrollingInterest", - UnitFamily::Currency, - ), - candidate("ifrs-full", "ProfitLossBeforeTax", UnitFamily::Currency), -]; -pub(crate) const PROVISION_FOR_INCOME_TAXES_CONCEPTS: &[ConceptCandidate] = &[ - candidate("us-gaap", "IncomeTaxExpenseBenefit", UnitFamily::Currency), - candidate( - "ifrs-full", - "IncomeTaxExpenseContinuingOperations", - UnitFamily::Currency, - ), -]; -pub(crate) const COMMON_SHAREHOLDER_NET_INCOME_CONCEPTS: &[ConceptCandidate] = &[ - candidate( - "us-gaap", - "NetIncomeLossAvailableToCommonStockholdersBasic", - UnitFamily::Currency, - ), - candidate( - "us-gaap", - "NetIncomeLossAvailableToCommonStockholdersDiluted", - UnitFamily::Currency, - ), - candidate( - "ifrs-full", - "ProfitLossAttributableToOwnersOfParent", - UnitFamily::Currency, - ), -]; -pub(crate) const BASIC_SHARES_CONCEPTS: &[ConceptCandidate] = &[candidate( - "us-gaap", - "WeightedAverageNumberOfSharesOutstandingBasic", - UnitFamily::Shares, -)]; -pub(crate) const TOTAL_SHARES_OUTSTANDING_CONCEPTS: &[ConceptCandidate] = SHARES_CONCEPTS; -pub(crate) const DEPRECIATION_AND_AMORTIZATION_CONCEPTS: &[ConceptCandidate] = &[ - candidate( - "us-gaap", - "DepreciationDepletionAndAmortization", - UnitFamily::Currency, - ), - candidate( - "us-gaap", - "DepreciationAmortizationAndAccretionNet", - UnitFamily::Currency, - ), - candidate( - "us-gaap", - "DepreciationAndAmortization", - UnitFamily::Currency, - ), -]; -pub(crate) const EFFECTIVE_TAX_RATE_CONCEPTS: &[ConceptCandidate] = &[candidate( - "us-gaap", - "EffectiveIncomeTaxRateContinuingOperations", - UnitFamily::Pure, -)]; pub(crate) const CASH_CONCEPTS: &[ConceptCandidate] = &[ candidate( "us-gaap", @@ -308,18 +188,6 @@ pub(crate) const DIVIDEND_CASH_CONCEPTS: &[ConceptCandidate] = &[ UnitFamily::Currency, ), ]; -pub(crate) const DILUTED_SHARES_CONCEPTS: &[ConceptCandidate] = &[ - candidate( - "us-gaap", - "WeightedAverageNumberOfDilutedSharesOutstanding", - UnitFamily::Shares, - ), - candidate( - "ifrs-full", - "WeightedAverageNumberOfSharesOutstandingDiluted", - UnitFamily::Shares, - ), -]; const fn candidate( taxonomy: &'static str, @@ -500,51 +368,6 @@ pub(crate) fn build_dividend_events( events } -pub(crate) fn build_earnings_periods( - facts: &[NormalizedFact], - frequency: Frequency, - latest_xbrl: Option<&ParsedXbrlDocument>, -) -> Vec { - let rows = build_earnings_period_rows(facts, frequency); - build_earnings_periods_from_rows(facts, &rows, frequency, latest_xbrl) -} - -pub(crate) fn build_earnings_period_rows( - facts: &[NormalizedFact], - frequency: Frequency, -) -> Vec { - build_period_rows(facts, frequency, REVENUE_CONCEPTS, usize::MAX) -} - -pub(crate) fn build_earnings_period_options( - rows: &[PeriodRow], - frequency: Frequency, -) -> Vec { - rows.iter() - .rev() - .filter_map(|row| { - let key = earnings_period_key_for_values( - row.fiscal_year.as_deref(), - row.fiscal_period.as_deref(), - frequency, - )?; - Some(EarningsPeriodOption { - id: key.token(), - label: earnings_display_label_for_values( - row.fiscal_year.as_deref(), - row.fiscal_period.as_deref(), - &row.period_end, - frequency, - ), - frequency, - fiscal_year: row.fiscal_year.clone(), - fiscal_period: row.fiscal_period.clone(), - period_end: row.period_end.clone(), - sort_key: key.sort_key(), - }) - }) - .collect() -} pub(crate) fn default_earnings_period_limit(frequency: Frequency) -> usize { match frequency { @@ -553,260 +376,6 @@ pub(crate) fn default_earnings_period_limit(frequency: Frequency) -> usize { } } -pub(crate) fn earnings_period_key_for_values( - fiscal_year: Option<&str>, - fiscal_period: Option<&str>, - frequency: Frequency, -) -> Option { - let fiscal_year = fiscal_year?.parse::().ok()?; - match frequency { - Frequency::Annual => (fiscal_period.is_none() || fiscal_period == Some("FY")) - .then_some(EarningsPeriodKey::Annual { fiscal_year }), - Frequency::Quarterly => { - let quarter = fiscal_period - .and_then(|period| period.strip_prefix('Q')) - .and_then(|quarter| quarter.parse::().ok()) - .filter(|quarter| (1..=4).contains(quarter))?; - Some(EarningsPeriodKey::Quarterly { - fiscal_year, - quarter, - }) - } - } -} - -pub(crate) fn earnings_period_token_for_values( - fiscal_year: Option<&str>, - fiscal_period: Option<&str>, - frequency: Frequency, -) -> Option { - earnings_period_key_for_values(fiscal_year, fiscal_period, frequency).map(|key| key.token()) -} - -pub(crate) fn earnings_display_label_for_values( - fiscal_year: Option<&str>, - fiscal_period: Option<&str>, - period_end: &str, - frequency: Frequency, -) -> String { - let month_suffix = month_abbreviation(period_end) - .map(|month| format!(" ({month})")) - .unwrap_or_default(); - - match frequency { - Frequency::Annual => fiscal_year - .map(|year| format!("FY{year}{month_suffix}")) - .unwrap_or_else(|| period_end.to_string()), - Frequency::Quarterly => match (fiscal_year, fiscal_period) { - (Some(year), Some(period)) if period.starts_with('Q') => { - format!("FY{year} {period}{month_suffix}") - } - _ => period_end.to_string(), - }, - } -} - -pub(crate) fn build_earnings_periods_from_rows( - facts: &[NormalizedFact], - rows: &[PeriodRow], - frequency: Frequency, - latest_xbrl: Option<&ParsedXbrlDocument>, -) -> Vec { - let mut periods = rows - .iter() - .enumerate() - .map(|(index, row)| { - let latest_xbrl = (index == 0).then_some(latest_xbrl).flatten(); - let total_revenues = - duration_value_for_period(facts, &row, REVENUE_CONCEPTS, latest_xbrl); - let mut cost_of_sales = - duration_value_for_period(facts, &row, COST_OF_SALES_CONCEPTS, latest_xbrl); - let mut gross_profit = - duration_value_for_period(facts, &row, GROSS_PROFIT_CONCEPTS, latest_xbrl); - let selling_general_and_administrative_expenses = - duration_value_for_period(facts, &row, SGA_CONCEPTS, latest_xbrl).or_else(|| { - sum_values( - duration_value_for_period( - facts, - &row, - SELLING_AND_MARKETING_CONCEPTS, - latest_xbrl, - ), - duration_value_for_period( - facts, - &row, - GENERAL_AND_ADMINISTRATIVE_CONCEPTS, - latest_xbrl, - ), - ) - }); - let research_and_development_expenses = - duration_value_for_period(facts, &row, R_AND_D_CONCEPTS, latest_xbrl); - let operating_expenses = - duration_value_for_period(facts, &row, OPERATING_EXPENSES_CONCEPTS, latest_xbrl); - let operating_profit = - duration_value_for_period(facts, &row, OPERATING_INCOME_CONCEPTS, latest_xbrl); - let non_operating_income = duration_value_for_period( - facts, - &row, - OTHER_NON_OPERATING_INCOME_CONCEPTS, - latest_xbrl, - ); - let mut total_non_operating_income = duration_value_for_period( - facts, - &row, - TOTAL_NON_OPERATING_INCOME_CONCEPTS, - latest_xbrl, - ); - let income_before_provision_for_income_taxes = - duration_value_for_period(facts, &row, PRETAX_INCOME_CONCEPTS, latest_xbrl); - let provision_for_income_taxes = duration_value_for_period( - facts, - &row, - PROVISION_FOR_INCOME_TAXES_CONCEPTS, - latest_xbrl, - ); - let consolidated_net_income = - duration_value_for_period(facts, &row, NET_INCOME_CONCEPTS, latest_xbrl); - let net_income_attributable_to_common_shareholders = duration_value_for_period( - facts, - &row, - COMMON_SHAREHOLDER_NET_INCOME_CONCEPTS, - latest_xbrl, - ); - let basic_eps = duration_value_for_period(facts, &row, BASIC_EPS_CONCEPTS, latest_xbrl); - let diluted_eps = - duration_value_for_period(facts, &row, DILUTED_EPS_CONCEPTS, latest_xbrl); - let basic_weighted_average_shares_outstanding = - duration_value_for_period(facts, &row, BASIC_SHARES_CONCEPTS, latest_xbrl); - let total_shares_outstanding = instant_value_for_period( - facts, - &row, - TOTAL_SHARES_OUTSTANDING_CONCEPTS, - latest_xbrl, - ); - let diluted_weighted_average_shares_outstanding = - duration_value_for_period(facts, &row, DILUTED_SHARES_CONCEPTS, latest_xbrl); - let depreciation_and_amortization = duration_value_for_period( - facts, - &row, - DEPRECIATION_AND_AMORTIZATION_CONCEPTS, - latest_xbrl, - ); - let direct_effective_tax_rate = normalize_percent_value(value_for_period_with_overlay( - facts, - &row, - EFFECTIVE_TAX_RATE_CONCEPTS, - latest_xbrl, - OverlayPeriodKind::Duration, - )); - - if cost_of_sales.is_none() { - cost_of_sales = difference(total_revenues, gross_profit); - } - if gross_profit.is_none() { - gross_profit = difference(total_revenues, cost_of_sales); - } - - if total_non_operating_income.is_none() { - total_non_operating_income = - difference(income_before_provision_for_income_taxes, operating_profit); - } - - let other_operating_expenses = match operating_expenses { - Some(operating_expenses) => subtract_many( - Some(operating_expenses), - [ - selling_general_and_administrative_expenses, - research_and_development_expenses, - ], - ), - None => subtract_many( - difference(gross_profit, operating_profit), - [ - selling_general_and_administrative_expenses, - research_and_development_expenses, - ], - ), - }; - - let gross_profit_margin = calculate_ratio_percent(gross_profit, total_revenues); - let operating_margin = calculate_ratio_percent(operating_profit, total_revenues); - let ebitda = sum_values(operating_profit, depreciation_and_amortization); - let effective_tax_rate = direct_effective_tax_rate.or_else(|| { - calculate_ratio_percent( - provision_for_income_taxes, - income_before_provision_for_income_taxes, - ) - }); - let display_label = earnings_display_label_for_values( - row.fiscal_year.as_deref(), - row.fiscal_period.as_deref(), - &row.period_end, - frequency, - ); - - EarningsPeriod { - label: display_label.clone(), - display_label, - fiscal_year: row.fiscal_year.clone(), - fiscal_period: row.fiscal_period.clone(), - period_start: row.period_start.clone(), - period_end: row.period_end.clone(), - filed_date: row.filed_date.clone(), - form: row.form.clone(), - total_revenues, - total_revenues_yoy_change_percent: None, - cost_of_sales, - gross_profit, - gross_profit_margin, - selling_general_and_administrative_expenses, - research_and_development_expenses, - other_operating_expenses, - operating_profit, - operating_margin, - non_operating_income, - total_non_operating_income, - income_before_provision_for_income_taxes, - provision_for_income_taxes, - consolidated_net_income, - net_income_attributable_to_common_shareholders, - total_shares_outstanding, - basic_weighted_average_shares_outstanding, - diluted_weighted_average_shares_outstanding, - ebitda, - effective_tax_rate, - revenue: total_revenues, - net_income: consolidated_net_income, - basic_eps, - diluted_eps, - diluted_weighted_average_shares: diluted_weighted_average_shares_outstanding, - revenue_yoy_change_percent: None, - diluted_eps_yoy_change_percent: None, - } - }) - .collect::>(); - - let offset = match frequency { - Frequency::Annual => 1, - Frequency::Quarterly => 4, - }; - - for index in 0..periods.len() { - let reference = periods.get(index + offset).cloned(); - if let (Some(current), Some(previous)) = (periods.get(index).cloned(), reference) { - if let Some(period) = periods.get_mut(index) { - period.total_revenues_yoy_change_percent = - calculate_change_percent(current.total_revenues, previous.total_revenues); - period.revenue_yoy_change_percent = period.total_revenues_yoy_change_percent; - period.diluted_eps_yoy_change_percent = - calculate_change_percent(current.diluted_eps, previous.diluted_eps); - } - } - } - - periods -} pub(crate) fn build_source_status( latest_xbrl: Result, &EdgarLookupError>, @@ -1011,12 +580,6 @@ fn duration_days(fact: &NormalizedFact) -> Option { Some((end - start).num_days().abs()) } -fn month_abbreviation(period_end: &str) -> Option { - NaiveDate::parse_from_str(period_end, "%Y-%m-%d") - .ok() - .map(|date| date.format("%b").to_string()) -} - fn concept_match(candidates: &[ConceptCandidate], fact: &NormalizedFact) -> bool { candidates.iter().any(|candidate| { candidate.taxonomy == fact.taxonomy @@ -1030,46 +593,6 @@ fn value_for_period( row: &PeriodRow, concepts: &[ConceptCandidate], latest_xbrl: Option<&ParsedXbrlDocument>, -) -> Option { - value_for_period_with_overlay(facts, row, concepts, latest_xbrl, OverlayPeriodKind::Any) -} - -fn duration_value_for_period( - facts: &[NormalizedFact], - row: &PeriodRow, - concepts: &[ConceptCandidate], - latest_xbrl: Option<&ParsedXbrlDocument>, -) -> Option { - value_for_period_with_overlay( - facts, - row, - concepts, - latest_xbrl, - OverlayPeriodKind::Duration, - ) -} - -fn instant_value_for_period( - facts: &[NormalizedFact], - row: &PeriodRow, - concepts: &[ConceptCandidate], - latest_xbrl: Option<&ParsedXbrlDocument>, -) -> Option { - value_for_period_with_overlay( - facts, - row, - concepts, - latest_xbrl, - OverlayPeriodKind::Instant, - ) -} - -fn value_for_period_with_overlay( - facts: &[NormalizedFact], - row: &PeriodRow, - concepts: &[ConceptCandidate], - latest_xbrl: Option<&ParsedXbrlDocument>, - overlay_period_kind: OverlayPeriodKind, ) -> Option { let mut best = facts .iter() @@ -1084,7 +607,7 @@ fn value_for_period_with_overlay( .map(|fact| fact.value); if let Some(latest_xbrl) = latest_xbrl { - if let Some(value) = overlay_xbrl_value(latest_xbrl, row, concepts, overlay_period_kind) { + if let Some(value) = overlay_xbrl_value(latest_xbrl, row, concepts) { best = Some(value); } } @@ -1092,34 +615,16 @@ fn value_for_period_with_overlay( best } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum OverlayPeriodKind { - Any, - Duration, - Instant, -} - fn overlay_xbrl_value( latest_xbrl: &ParsedXbrlDocument, row: &PeriodRow, concepts: &[ConceptCandidate], - overlay_period_kind: OverlayPeriodKind, ) -> Option { latest_xbrl .facts .iter() .filter(|fact| !fact.has_dimensions) .filter(|fact| fact.period_end.as_deref() == Some(row.period_end.as_str())) - .filter(|fact| match overlay_period_kind { - OverlayPeriodKind::Any => true, - OverlayPeriodKind::Duration => { - !fact.is_instant - && row.period_start.as_deref().is_none_or(|period_start| { - fact.period_start.as_deref() == Some(period_start) - }) - } - OverlayPeriodKind::Instant => fact.is_instant, - }) .find(|fact| { concepts.iter().any(|candidate| { fact.concept @@ -1130,40 +635,6 @@ fn overlay_xbrl_value( .map(|fact| fact.value) } -fn difference(left: Option, right: Option) -> Option { - Some(left? - right?) -} - -fn sum_values(left: Option, right: Option) -> Option { - Some(left? + right?) -} - -fn subtract_many(base: Option, values: [Option; N]) -> Option { - let mut remainder = base?; - for value in values { - remainder -= value?; - } - Some(remainder) -} - -fn calculate_ratio_percent(numerator: Option, denominator: Option) -> Option { - let numerator = numerator?; - let denominator = denominator?; - if denominator.abs() < f64::EPSILON { - return None; - } - Some((numerator / denominator) * 100.0) -} - -fn normalize_percent_value(value: Option) -> Option { - let value = value?; - if value.abs() <= 1.0 { - Some(value * 100.0) - } else { - Some(value) - } -} - fn classify_dividend_frequency(current_end: Option<&str>, previous_end: Option<&str>) -> String { let Some(current_end) = current_end else { return "unknown".to_string(); @@ -1188,400 +659,3 @@ fn classify_dividend_frequency(current_end: Option<&str>, previous_end: Option<& "special".to_string() } } - -fn calculate_change_percent(current: Option, previous: Option) -> Option { - let current = current?; - let previous = previous?; - if previous.abs() < f64::EPSILON { - return None; - } - Some(((current - previous) / previous.abs()) * 100.0) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::terminal::sec_edgar::types::LatestXbrlFact; - use crate::terminal::Frequency; - - #[test] - fn build_earnings_periods_should_backfill_cost_of_sales_and_gross_profit_when_missing() { - let facts = vec![ - duration_fact( - "us-gaap", - "RevenueFromContractWithCustomerExcludingAssessedTax", - 100.0, - "2025", - "2025-01-01", - "2025-12-31", - ), - duration_fact( - "us-gaap", - "GrossProfit", - 40.0, - "2025", - "2025-01-01", - "2025-12-31", - ), - duration_fact( - "us-gaap", - "RevenueFromContractWithCustomerExcludingAssessedTax", - 80.0, - "2024", - "2024-01-01", - "2024-12-31", - ), - duration_fact( - "us-gaap", - "CostOfRevenue", - 48.0, - "2024", - "2024-01-01", - "2024-12-31", - ), - ]; - - let periods = build_earnings_periods(&facts, Frequency::Annual, None); - let latest = &periods[0]; - - assert_eq!(latest.total_revenues, Some(100.0)); - assert_eq!(latest.cost_of_sales, Some(60.0)); - assert_eq!(latest.gross_profit, Some(40.0)); - assert_eq!(latest.gross_profit_margin, Some(40.0)); - assert_eq!(latest.revenue, latest.total_revenues); - assert_eq!(latest.total_revenues_yoy_change_percent, Some(25.0)); - assert_eq!( - latest.revenue_yoy_change_percent, - latest.total_revenues_yoy_change_percent - ); - } - - #[test] - fn build_earnings_periods_should_compute_other_operating_expenses_and_tax_metrics() { - let facts = vec![ - duration_fact("us-gaap", "RevenueFromContractWithCustomerExcludingAssessedTax", 100.0, "2025", "2025-01-01", "2025-12-31"), - duration_fact("us-gaap", "GrossProfit", 70.0, "2025", "2025-01-01", "2025-12-31"), - duration_fact("us-gaap", "OperatingIncomeLoss", 30.0, "2025", "2025-01-01", "2025-12-31"), - duration_fact("us-gaap", "SellingGeneralAndAdministrativeExpense", 20.0, "2025", "2025-01-01", "2025-12-31"), - duration_fact("us-gaap", "ResearchAndDevelopmentExpense", 5.0, "2025", "2025-01-01", "2025-12-31"), - duration_fact("us-gaap", "IncomeLossFromContinuingOperationsBeforeIncomeTaxesExtraordinaryItemsNoncontrollingInterest", 32.0, "2025", "2025-01-01", "2025-12-31"), - duration_fact("us-gaap", "IncomeTaxExpenseBenefit", 8.0, "2025", "2025-01-01", "2025-12-31"), - duration_fact("us-gaap", "NetIncomeLoss", 24.0, "2025", "2025-01-01", "2025-12-31"), - duration_fact("us-gaap", "DepreciationAndAmortization", 4.0, "2025", "2025-01-01", "2025-12-31"), - duration_fact("us-gaap", "RevenueFromContractWithCustomerExcludingAssessedTax", 90.0, "2024", "2024-01-01", "2024-12-31"), - ]; - - let periods = build_earnings_periods(&facts, Frequency::Annual, None); - let latest = &periods[0]; - - assert_eq!(latest.other_operating_expenses, Some(15.0)); - assert_eq!(latest.total_non_operating_income, Some(2.0)); - assert_eq!(latest.operating_margin, Some(30.0)); - assert_eq!(latest.effective_tax_rate, Some(25.0)); - assert_eq!(latest.ebitda, Some(34.0)); - assert_eq!(latest.consolidated_net_income, Some(24.0)); - assert_eq!(latest.net_income, latest.consolidated_net_income); - } - - #[test] - fn build_earnings_periods_should_normalize_direct_tax_rate_ratio_values() { - let facts = vec![ - duration_fact( - "us-gaap", - "RevenueFromContractWithCustomerExcludingAssessedTax", - 100.0, - "2025", - "2025-01-01", - "2025-12-31", - ), - pure_duration_fact( - "us-gaap", - "EffectiveIncomeTaxRateContinuingOperations", - 0.215, - "2025", - "2025-01-01", - "2025-12-31", - ), - duration_fact( - "us-gaap", - "RevenueFromContractWithCustomerExcludingAssessedTax", - 90.0, - "2024", - "2024-01-01", - "2024-12-31", - ), - ]; - - let periods = build_earnings_periods(&facts, Frequency::Annual, None); - - assert_eq!(periods[0].effective_tax_rate, Some(21.5)); - } - - #[test] - fn overlay_xbrl_value_should_ignore_dimensional_facts_for_duration_metrics() { - let row = sample_period_row(); - let latest_xbrl = ParsedXbrlDocument { - facts: vec![ - xbrl_duration_fact( - "us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax", - 999.0, - "2025-01-01", - "2025-12-31", - true, - ), - xbrl_duration_fact( - "us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax", - 110.0, - "2025-01-01", - "2025-12-31", - false, - ), - ], - }; - - let value = overlay_xbrl_value( - &latest_xbrl, - &row, - REVENUE_CONCEPTS, - OverlayPeriodKind::Duration, - ); - - assert_eq!(value, Some(110.0)); - } - - #[test] - fn overlay_xbrl_value_should_match_duration_facts_on_start_and_end_dates() { - let row = sample_period_row(); - let latest_xbrl = ParsedXbrlDocument { - facts: vec![ - xbrl_duration_fact( - "us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax", - 999.0, - "2024-01-01", - "2025-12-31", - false, - ), - xbrl_duration_fact( - "us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax", - 110.0, - "2025-01-01", - "2025-12-31", - false, - ), - ], - }; - - let value = overlay_xbrl_value( - &latest_xbrl, - &row, - REVENUE_CONCEPTS, - OverlayPeriodKind::Duration, - ); - - assert_eq!(value, Some(110.0)); - } - - #[test] - fn overlay_xbrl_value_should_require_instant_contexts_for_total_shares_outstanding() { - let row = sample_period_row(); - let latest_xbrl = ParsedXbrlDocument { - facts: vec![ - xbrl_duration_fact( - "dei:EntityCommonStockSharesOutstanding", - 90.0, - "2025-01-01", - "2025-12-31", - false, - ), - xbrl_instant_fact( - "dei:EntityCommonStockSharesOutstanding", - 150.0, - "2025-12-31", - false, - ), - ], - }; - - let value = overlay_xbrl_value( - &latest_xbrl, - &row, - TOTAL_SHARES_OUTSTANDING_CONCEPTS, - OverlayPeriodKind::Instant, - ); - - assert_eq!(value, Some(150.0)); - } - - #[test] - fn build_earnings_period_rows_should_keep_full_annual_history() { - let facts = vec![ - duration_fact( - "us-gaap", - "RevenueFromContractWithCustomerExcludingAssessedTax", - 140.0, - "2025", - "2025-01-01", - "2025-12-31", - ), - duration_fact( - "us-gaap", - "RevenueFromContractWithCustomerExcludingAssessedTax", - 130.0, - "2024", - "2024-01-01", - "2024-12-31", - ), - duration_fact( - "us-gaap", - "RevenueFromContractWithCustomerExcludingAssessedTax", - 120.0, - "2023", - "2023-01-01", - "2023-12-31", - ), - duration_fact( - "us-gaap", - "RevenueFromContractWithCustomerExcludingAssessedTax", - 110.0, - "2022", - "2022-01-01", - "2022-12-31", - ), - duration_fact( - "us-gaap", - "RevenueFromContractWithCustomerExcludingAssessedTax", - 100.0, - "2021", - "2021-01-01", - "2021-12-31", - ), - ]; - - let rows = build_earnings_period_rows(&facts, Frequency::Annual); - - assert_eq!(rows.len(), 5); - assert_eq!(rows[0].fiscal_year.as_deref(), Some("2025")); - assert_eq!(rows[4].fiscal_year.as_deref(), Some("2021")); - } - - #[test] - fn build_earnings_period_options_should_create_stable_annual_labels() { - let rows = vec![ - PeriodRow { - label: "2024".to_string(), - fiscal_year: Some("2024".to_string()), - fiscal_period: Some("FY".to_string()), - period_start: Some("2024-01-01".to_string()), - period_end: "2024-09-28".to_string(), - filed_date: "2024-11-01".to_string(), - form: "10-K".to_string(), - }, - PeriodRow { - label: "2023".to_string(), - fiscal_year: Some("2023".to_string()), - fiscal_period: Some("FY".to_string()), - period_start: Some("2023-01-01".to_string()), - period_end: "2023-09-30".to_string(), - filed_date: "2023-11-01".to_string(), - form: "10-K".to_string(), - }, - ]; - - let options = build_earnings_period_options(&rows, Frequency::Annual); - - assert_eq!(options[0].id, "2023"); - assert_eq!(options[1].id, "2024"); - assert_eq!(options[1].label, "FY2024 (Sep)"); - } - - fn duration_fact( - taxonomy: &'static str, - concept: &str, - value: f64, - fiscal_year: &str, - period_start: &str, - period_end: &str, - ) -> NormalizedFact { - NormalizedFact { - taxonomy, - concept: concept.to_string(), - unit_family: UnitFamily::Currency, - value, - filed: "2026-01-31".to_string(), - form: "10-K".to_string(), - fiscal_year: Some(fiscal_year.to_string()), - fiscal_period: Some("FY".to_string()), - period_start: Some(period_start.to_string()), - period_end: period_end.to_string(), - accession_number: Some(format!("{fiscal_year}-000001")), - } - } - - fn pure_duration_fact( - taxonomy: &'static str, - concept: &str, - value: f64, - fiscal_year: &str, - period_start: &str, - period_end: &str, - ) -> NormalizedFact { - NormalizedFact { - unit_family: UnitFamily::Pure, - ..duration_fact( - taxonomy, - concept, - value, - fiscal_year, - period_start, - period_end, - ) - } - } - - fn sample_period_row() -> PeriodRow { - PeriodRow { - label: "2025 FY".to_string(), - fiscal_year: Some("2025".to_string()), - fiscal_period: Some("FY".to_string()), - period_start: Some("2025-01-01".to_string()), - period_end: "2025-12-31".to_string(), - filed_date: "2026-01-31".to_string(), - form: "10-K".to_string(), - } - } - - fn xbrl_duration_fact( - concept: &str, - value: f64, - period_start: &str, - period_end: &str, - has_dimensions: bool, - ) -> LatestXbrlFact { - LatestXbrlFact { - concept: concept.to_string(), - value, - context_ref: "ctx-duration".to_string(), - period_start: Some(period_start.to_string()), - period_end: Some(period_end.to_string()), - is_instant: false, - has_dimensions, - } - } - - fn xbrl_instant_fact( - concept: &str, - value: f64, - period_end: &str, - has_dimensions: bool, - ) -> LatestXbrlFact { - LatestXbrlFact { - concept: concept.to_string(), - value, - context_ref: "ctx-instant".to_string(), - period_start: None, - period_end: Some(period_end.to_string()), - is_instant: true, - has_dimensions, - } - } -} diff --git a/MosaicIQ/src-tauri/src/terminal/sec_edgar/types.rs b/MosaicIQ/src-tauri/src/terminal/sec_edgar/types.rs index 2eac7ae..56df0c5 100644 --- a/MosaicIQ/src-tauri/src/terminal/sec_edgar/types.rs +++ b/MosaicIQ/src-tauri/src/terminal/sec_edgar/types.rs @@ -216,8 +216,10 @@ pub(crate) struct LatestXbrlFact { pub value: f64, #[allow(dead_code)] pub context_ref: String, + #[allow(dead_code)] pub period_start: Option, pub period_end: Option, + #[allow(dead_code)] pub is_instant: bool, pub has_dimensions: bool, }