import 'package:equatable/equatable.dart'; class FundamentalDataModel extends Equatable { final String isin; final String primaryTicker; final String ticker; final String companyName; final String? exchange; final String? tradingCurrency; final String? businessSummary; final String? sector; final String? industry; final String? country; final int? employees; final double currentPrice; final double dayChangeAbsolute; final double dayChangePercent; final double fiftyTwoWeekHigh; final double fiftyTwoWeekLow; final double marketCapitalization; final double enterpriseValue; final double? peRatioTrailing; final double? peRatioForward; final double? pegRatio; final double? pbRatio; final double? psRatio; final double? evToEbitda; final double? evToRevenue; final double? totalRevenue; final double? revenueGrowthYoY; final double? grossProfit; final double? ebitda; final double? dilutedEps; final double? totalCash; final double? totalDebt; final double? operatingCashFlow; final double? freeCashFlow; final double? grossMargin; final double? operatingMargin; final double? netProfitMargin; final double? returnOnEquity; final double? returnOnAssets; final double? returnOnInvestedCapital; final double? debtToEquity; final double? currentRatio; final double? quickRatio; final double? interestCoverage; final double? dividendYield; final double? payoutRatio; final String? exDividendDate; final String? nextEarningsDate; final double? percentHeldByInstitutions; final double? percentHeldByInsiders; final double? shortRatio; final double? shortPercentOfFloat; final String? consensusRating; final double? priceTargetLow; final double? priceTargetHigh; final double? priceTargetMedian; final double? priceTargetMean; final List executives; final List financialStatements; final List estimates; final List availableTickers; const FundamentalDataModel({ required this.isin, required this.primaryTicker, required this.ticker, required this.companyName, this.exchange, this.tradingCurrency, this.businessSummary, this.sector, this.industry, this.country, this.employees, required this.currentPrice, required this.dayChangeAbsolute, required this.dayChangePercent, required this.fiftyTwoWeekHigh, required this.fiftyTwoWeekLow, required this.marketCapitalization, required this.enterpriseValue, this.peRatioTrailing, this.peRatioForward, this.pegRatio, this.pbRatio, this.psRatio, this.evToEbitda, this.evToRevenue, this.totalRevenue, this.revenueGrowthYoY, this.grossProfit, this.ebitda, this.dilutedEps, this.totalCash, this.totalDebt, this.operatingCashFlow, this.freeCashFlow, this.grossMargin, this.operatingMargin, this.netProfitMargin, this.returnOnEquity, this.returnOnAssets, this.returnOnInvestedCapital, this.debtToEquity, this.currentRatio, this.quickRatio, this.interestCoverage, this.dividendYield, this.payoutRatio, this.exDividendDate, this.nextEarningsDate, this.percentHeldByInstitutions, this.percentHeldByInsiders, this.shortRatio, this.shortPercentOfFloat, this.consensusRating, this.priceTargetLow, this.priceTargetHigh, this.priceTargetMedian, this.priceTargetMean, required this.executives, required this.financialStatements, required this.estimates, this.availableTickers = const [], }); factory FundamentalDataModel.fromJson(Map json) { double parseDouble(dynamic val) { if (val == null) return 0.0; if (val is num) return val.toDouble(); return double.tryParse(val.toString()) ?? 0.0; } double? parseNullableDouble(dynamic val) { if (val == null) return null; if (val is num) return val.toDouble(); return double.tryParse(val.toString()); } final assetMap = json['asset'] is Map ? json['asset'] as Map : null; final fundMap = json['fundamentals'] is Map ? json['fundamentals'] as Map : null; String extractTickerStr(dynamic val) { if (val == null) return ''; if (val is Map) { return val['ticker']?.toString() ?? ''; } return val.toString(); } String? extractExchangeStr(dynamic val) { if (val == null) return null; if (val is Map) { return val['exchange']?.toString(); } return null; } final isinVal = assetMap?['isin']?.toString() ?? json['isin']?.toString() ?? ''; final primaryTickerVal = extractTickerStr(assetMap?['primaryTicker'] ?? json['primaryTicker']); final tickerVal = extractTickerStr(fundMap?['ticker'] ?? json['ticker']).isNotEmpty ? extractTickerStr(fundMap?['ticker'] ?? json['ticker']) : primaryTickerVal; final companyNameVal = assetMap?['name']?.toString() ?? json['companyName']?.toString() ?? json['name']?.toString() ?? tickerVal; final businessSummaryVal = assetMap?['description']?.toString() ?? json['businessSummary']?.toString() ?? json['description']?.toString(); final exchangeVal = extractExchangeStr(fundMap?['ticker']) ?? extractExchangeStr(assetMap?['primaryTicker']) ?? json['exchange']?.toString(); final rawTickers = assetMap?['availableTickers'] ?? json['availableTickers']; List availableTickersList = []; if (rawTickers is List) { availableTickersList = rawTickers.map((t) { if (t is Map) { return TickerModel.fromJson(t); } else { return TickerModel(ticker: t.toString()); } }).toList(); } // Revenue & Margins Derivation final totalRev = parseNullableDouble(fundMap?['totalRevenue'] ?? json['totalRevenue']); final grossProf = parseNullableDouble(fundMap?['grossProfit'] ?? json['grossProfit']); double? grossMarginVal = parseNullableDouble(fundMap?['grossMargin'] ?? json['grossMargin']); if (grossMarginVal == null && grossProf != null) { if (grossProf <= 1.0 && grossProf >= 0.0) { grossMarginVal = grossProf; } else if (totalRev != null && totalRev > 0) { grossMarginVal = grossProf / totalRev; } } // Enterprise Value to Revenue final evVal = parseNullableDouble(fundMap?['enterpriseValue'] ?? json['enterpriseValue']); double? evToRevVal = parseNullableDouble(fundMap?['evToRevenue'] ?? fundMap?['enterpriseValueToRevenue'] ?? json['evToRevenue']); if (evToRevVal == null && evVal != null && totalRev != null && totalRev > 0) { evToRevVal = evVal / totalRev; } // Event Dates (Ex-Dividend & Next Earnings) String? exDividendDateVal = json['exDividendDate']?.toString() ?? fundMap?['exDividendDate']?.toString(); String? nextEarningsDateVal = json['nextEarningsDate']?.toString() ?? fundMap?['nextEarningsDate']?.toString(); final rawEvents = json['events']; if (rawEvents is List && rawEvents.isNotEmpty) { final now = DateTime.now(); final parsedEvents = >[]; for (final ev in rawEvents) { if (ev is Map) { final dtStr = ev['date']?.toString(); final dt = dtStr != null ? DateTime.tryParse(dtStr) : null; if (dt != null) { parsedEvents.add({ 'type': ev['type']?.toString().toUpperCase() ?? '', 'date': dt, 'dateStr': dtStr, }); } } } if (exDividendDateVal == null) { final dividendEvents = parsedEvents.where((e) => e['type'] == 'DIVIDEND').toList() ..sort((a, b) => (a['date'] as DateTime).compareTo(b['date'] as DateTime)); final futureDividends = dividendEvents.where((e) => (e['date'] as DateTime).isAfter(now)).toList(); if (futureDividends.isNotEmpty) { exDividendDateVal = futureDividends.first['dateStr'] as String; } else if (dividendEvents.isNotEmpty) { exDividendDateVal = dividendEvents.last['dateStr'] as String; } } if (nextEarningsDateVal == null) { final earningsEvents = parsedEvents.where((e) => e['type'] == 'EARNINGS_RELEASE' || e['type'] == 'EARNINGS_CALL').toList() ..sort((a, b) => (a['date'] as DateTime).compareTo(b['date'] as DateTime)); final futureEarnings = earningsEvents.where((e) => (e['date'] as DateTime).isAfter(now)).toList(); if (futureEarnings.isNotEmpty) { nextEarningsDateVal = futureEarnings.first['dateStr'] as String; } else if (earningsEvents.isNotEmpty) { nextEarningsDateVal = earningsEvents.last['dateStr'] as String; } } } return FundamentalDataModel( isin: isinVal, primaryTicker: primaryTickerVal, ticker: tickerVal, companyName: companyNameVal, exchange: exchangeVal, tradingCurrency: json['tradingCurrency']?.toString(), businessSummary: businessSummaryVal, sector: json['sector']?.toString(), industry: json['industry']?.toString(), country: json['country']?.toString(), employees: json['employees'] != null ? int.tryParse(json['employees'].toString()) : null, currentPrice: parseDouble(json['currentPrice']), dayChangeAbsolute: parseDouble(json['dayChangeAbsolute']), dayChangePercent: parseDouble(json['dayChangePercent']), fiftyTwoWeekHigh: parseDouble(fundMap?['fiftyTwoWeekHigh'] ?? json['fiftyTwoWeekHigh']), fiftyTwoWeekLow: parseDouble(fundMap?['fiftyTwoWeekLow'] ?? json['fiftyTwoWeekLow']), marketCapitalization: parseDouble(fundMap?['marketCap'] ?? json['marketCapitalization'] ?? json['marketCap']), enterpriseValue: parseDouble(fundMap?['enterpriseValue'] ?? json['enterpriseValue']), peRatioTrailing: parseNullableDouble(fundMap?['trailingPe'] ?? json['peRatioTrailing'] ?? json['peRatio']), peRatioForward: parseNullableDouble(fundMap?['forwardPe'] ?? json['peRatioForward']), pegRatio: parseNullableDouble(fundMap?['pegRatio'] ?? json['pegRatio']), pbRatio: parseNullableDouble(fundMap?['priceToBook'] ?? json['pbRatio']), psRatio: parseNullableDouble(fundMap?['priceToSales'] ?? json['psRatio']), evToEbitda: parseNullableDouble(fundMap?['evToEbitda'] ?? json['evToEbitda']), evToRevenue: evToRevVal, totalRevenue: totalRev, revenueGrowthYoY: parseNullableDouble(fundMap?['revenueGrowthYoY'] ?? json['revenueGrowthYoY']), grossProfit: grossProf, ebitda: parseNullableDouble(fundMap?['ebitda'] ?? json['ebitda']), dilutedEps: parseNullableDouble(fundMap?['dilutedEps'] ?? json['dilutedEps']), totalCash: parseNullableDouble(fundMap?['totalCash'] ?? json['totalCash']), totalDebt: parseNullableDouble(fundMap?['totalDebt'] ?? json['totalDebt']), operatingCashFlow: parseNullableDouble(fundMap?['operatingCashFlow'] ?? json['operatingCashFlow']), freeCashFlow: parseNullableDouble(fundMap?['freeCashFlow'] ?? json['freeCashFlow']), grossMargin: grossMarginVal, operatingMargin: parseNullableDouble(fundMap?['operatingMargin'] ?? fundMap?['operatingIncome'] ?? json['operatingMargin']), netProfitMargin: parseNullableDouble(fundMap?['netProfitMargin'] ?? fundMap?['netIncome'] ?? json['netProfitMargin']), returnOnEquity: parseNullableDouble(fundMap?['returnOnEquity'] ?? json['returnOnEquity']), returnOnAssets: parseNullableDouble(fundMap?['returnOnAssets'] ?? json['returnOnAssets']), returnOnInvestedCapital: parseNullableDouble(fundMap?['returnOnInvestedCapital'] ?? json['returnOnInvestedCapital']), debtToEquity: parseNullableDouble(fundMap?['debtToEquity'] ?? json['debtToEquity']), currentRatio: parseNullableDouble(fundMap?['currentRatio'] ?? json['currentRatio']), quickRatio: parseNullableDouble(fundMap?['quickRatio'] ?? json['quickRatio']), interestCoverage: parseNullableDouble(fundMap?['interestCoverage'] ?? json['interestCoverage']), dividendYield: parseNullableDouble(fundMap?['forwardDividendYield'] ?? json['dividendYield']), payoutRatio: parseNullableDouble(fundMap?['payoutRatio'] ?? json['payoutRatio']), exDividendDate: exDividendDateVal, nextEarningsDate: nextEarningsDateVal, percentHeldByInstitutions: parseNullableDouble(fundMap?['percentHeldByInstitutions'] ?? json['percentHeldByInstitutions']), percentHeldByInsiders: parseNullableDouble(fundMap?['percentHeldByInsiders'] ?? json['percentHeldByInsiders']), shortRatio: parseNullableDouble(fundMap?['shortRatio'] ?? json['shortRatio']), shortPercentOfFloat: parseNullableDouble(fundMap?['shortPercentOfFloat'] ?? json['shortPercentOfFloat']), consensusRating: (fundMap?['consensusRating'] ?? json['consensusRating'])?.toString(), priceTargetLow: parseNullableDouble(fundMap?['priceTargetLow'] ?? json['priceTargetLow']), priceTargetHigh: parseNullableDouble(fundMap?['priceTargetHigh'] ?? json['priceTargetHigh']), priceTargetMedian: parseNullableDouble(fundMap?['priceTargetMedian'] ?? json['priceTargetMedian']), priceTargetMean: parseNullableDouble(fundMap?['priceTargetMean'] ?? json['priceTargetMean']), executives: (json['executives'] as List?) ?.map((e) => CompanyExecutiveModel.fromJson(e is Map ? e : {})) .toList() ?? [], financialStatements: (json['financialStatements'] as List?) ?.map((e) => FinancialStatementModel.fromJson(e is Map ? e : {})) .toList() ?? [], estimates: (json['estimates'] as List?) ?.map((e) => ForwardEstimateModel.fromJson(e is Map ? e : {})) .toList() ?? [], availableTickers: availableTickersList, ); } Map toJson() { return { 'isin': isin, 'primaryTicker': primaryTicker, 'ticker': ticker, 'companyName': companyName, 'exchange': exchange, 'tradingCurrency': tradingCurrency, 'businessSummary': businessSummary, 'sector': sector, 'industry': industry, 'country': country, 'employees': employees, 'currentPrice': currentPrice, 'dayChangeAbsolute': dayChangeAbsolute, 'dayChangePercent': dayChangePercent, 'fiftyTwoWeekHigh': fiftyTwoWeekHigh, 'fiftyTwoWeekLow': fiftyTwoWeekLow, 'marketCapitalization': marketCapitalization, 'enterpriseValue': enterpriseValue, 'peRatioTrailing': peRatioTrailing, 'peRatioForward': peRatioForward, 'pegRatio': pegRatio, 'pbRatio': pbRatio, 'psRatio': psRatio, 'evToEbitda': evToEbitda, 'evToRevenue': evToRevenue, 'grossMargin': grossMargin, 'operatingMargin': operatingMargin, 'netProfitMargin': netProfitMargin, 'returnOnEquity': returnOnEquity, 'returnOnAssets': returnOnAssets, 'returnOnInvestedCapital': returnOnInvestedCapital, 'debtToEquity': debtToEquity, 'currentRatio': currentRatio, 'quickRatio': quickRatio, 'dividendYield': dividendYield, 'payoutRatio': payoutRatio, 'exDividendDate': exDividendDate, 'nextEarningsDate': nextEarningsDate, 'percentHeldByInstitutions': percentHeldByInstitutions, 'percentHeldByInsiders': percentHeldByInsiders, 'shortRatio': shortRatio, 'shortPercentOfFloat': shortPercentOfFloat, 'consensusRating': consensusRating, 'priceTargetLow': priceTargetLow, 'priceTargetHigh': priceTargetHigh, 'priceTargetMedian': priceTargetMedian, 'priceTargetMean': priceTargetMean, 'executives': executives.map((e) => e.toJson()).toList(), 'financialStatements': financialStatements.map((e) => e.toJson()).toList(), 'estimates': estimates.map((e) => e.toJson()).toList(), 'availableTickers': availableTickers.map((e) => e.toJson()).toList(), }; } @override List get props => [ isin, primaryTicker, ticker, companyName, exchange, tradingCurrency, businessSummary, sector, industry, country, employees, currentPrice, dayChangeAbsolute, dayChangePercent, fiftyTwoWeekHigh, fiftyTwoWeekLow, marketCapitalization, enterpriseValue, peRatioTrailing, peRatioForward, pegRatio, pbRatio, psRatio, evToEbitda, evToRevenue, grossMargin, operatingMargin, netProfitMargin, returnOnEquity, returnOnAssets, returnOnInvestedCapital, debtToEquity, currentRatio, quickRatio, dividendYield, payoutRatio, exDividendDate, nextEarningsDate, percentHeldByInstitutions, percentHeldByInsiders, shortRatio, shortPercentOfFloat, consensusRating, priceTargetLow, priceTargetHigh, priceTargetMedian, priceTargetMean, executives, financialStatements, estimates, availableTickers, ]; } class CompanyExecutiveModel extends Equatable { final String name; final String title; final int? age; final double? compensation; const CompanyExecutiveModel({ required this.name, required this.title, this.age, this.compensation, }); factory CompanyExecutiveModel.fromJson(Map json) { double? compVal; if (json['compensation'] != null) { compVal = double.tryParse(json['compensation'].toString()); } else if (json['payment'] != null) { final pStr = json['payment'].toString().trim().toUpperCase().replaceAll('\$', '').replaceAll('€', '').replaceAll('£', '').replaceAll(',', '').replaceAll(' ', ''); if (pStr.endsWith('M')) { final numPart = double.tryParse(pStr.substring(0, pStr.length - 1)); if (numPart != null) compVal = numPart * 1e6; } else if (pStr.endsWith('K')) { final numPart = double.tryParse(pStr.substring(0, pStr.length - 1)); if (numPart != null) compVal = numPart * 1e3; } else if (pStr.endsWith('B')) { final numPart = double.tryParse(pStr.substring(0, pStr.length - 1)); if (numPart != null) compVal = numPart * 1e9; } else { compVal = double.tryParse(pStr); } } return CompanyExecutiveModel( name: json['name']?.toString() ?? '', title: json['title']?.toString() ?? '', age: json['age'] != null ? int.tryParse(json['age'].toString()) : null, compensation: compVal, ); } Map toJson() { return { 'name': name, 'title': title, 'age': age, 'compensation': compensation, }; } @override List get props => [name, title, age, compensation]; } class FinancialStatementModel extends Equatable { final String periodType; final String endDate; // Income Statement final double? totalRevenue; final double? costOfRevenue; final double? grossProfit; final double? operatingExpenses; final double? operatingIncome; final double? ebitda; final double? netIncome; final double? epsBasic; final double? epsDiluted; // Balance Sheet final double? cashAndCashEquivalents; final double? accountsReceivable; final double? inventory; final double? totalCurrentAssets; final double? totalNonCurrentAssets; final double? currentLiabilities; final double? longTermDebt; final double? totalLiabilities; final double? totalStockholdersEquity; // Cash Flow final double? operatingCashFlow; final double? investingCashFlow; final double? capitalExpenditures; final double? financingCashFlow; final double? freeCashFlow; const FinancialStatementModel({ required this.periodType, required this.endDate, this.totalRevenue, this.costOfRevenue, this.grossProfit, this.operatingExpenses, this.operatingIncome, this.ebitda, this.netIncome, this.epsBasic, this.epsDiluted, this.cashAndCashEquivalents, this.accountsReceivable, this.inventory, this.totalCurrentAssets, this.totalNonCurrentAssets, this.currentLiabilities, this.longTermDebt, this.totalLiabilities, this.totalStockholdersEquity, this.operatingCashFlow, this.investingCashFlow, this.capitalExpenditures, this.financingCashFlow, this.freeCashFlow, }); factory FinancialStatementModel.fromJson(Map json) { double? parseD(dynamic val) { if (val == null) return null; if (val is num) return val.toDouble(); return double.tryParse(val.toString()); } return FinancialStatementModel( periodType: json['periodType']?.toString() ?? '', endDate: json['endDate']?.toString() ?? '', totalRevenue: parseD(json['totalRevenue']), costOfRevenue: parseD(json['costOfRevenue']), grossProfit: parseD(json['grossProfit']), operatingExpenses: parseD(json['operatingExpenses']), operatingIncome: parseD(json['operatingIncome']), ebitda: parseD(json['ebitda']), netIncome: parseD(json['netIncome']), epsBasic: parseD(json['epsBasic']), epsDiluted: parseD(json['epsDiluted']), cashAndCashEquivalents: parseD(json['cashAndCashEquivalents']), accountsReceivable: parseD(json['accountsReceivable']), inventory: parseD(json['inventory']), totalCurrentAssets: parseD(json['totalCurrentAssets']), totalNonCurrentAssets: parseD(json['totalNonCurrentAssets']), currentLiabilities: parseD(json['currentLiabilities']), longTermDebt: parseD(json['longTermDebt']), totalLiabilities: parseD(json['totalLiabilities']), totalStockholdersEquity: parseD(json['totalStockholdersEquity']), operatingCashFlow: parseD(json['operatingCashFlow']), investingCashFlow: parseD(json['investingCashFlow']), capitalExpenditures: parseD(json['capitalExpenditures']), financingCashFlow: parseD(json['financingCashFlow']), freeCashFlow: parseD(json['freeCashFlow']), ); } Map toJson() { return { 'periodType': periodType, 'endDate': endDate, 'totalRevenue': totalRevenue, 'costOfRevenue': costOfRevenue, 'grossProfit': grossProfit, 'operatingExpenses': operatingExpenses, 'operatingIncome': operatingIncome, 'ebitda': ebitda, 'netIncome': netIncome, 'epsBasic': epsBasic, 'epsDiluted': epsDiluted, 'cashAndCashEquivalents': cashAndCashEquivalents, 'accountsReceivable': accountsReceivable, 'inventory': inventory, 'totalCurrentAssets': totalCurrentAssets, 'totalNonCurrentAssets': totalNonCurrentAssets, 'currentLiabilities': currentLiabilities, 'longTermDebt': longTermDebt, 'totalLiabilities': totalLiabilities, 'totalStockholdersEquity': totalStockholdersEquity, 'operatingCashFlow': operatingCashFlow, 'investingCashFlow': investingCashFlow, 'capitalExpenditures': capitalExpenditures, 'financingCashFlow': financingCashFlow, 'freeCashFlow': freeCashFlow, }; } @override List get props => [ periodType, endDate, totalRevenue, costOfRevenue, grossProfit, operatingExpenses, operatingIncome, ebitda, netIncome, epsBasic, epsDiluted, cashAndCashEquivalents, accountsReceivable, inventory, totalCurrentAssets, totalNonCurrentAssets, currentLiabilities, longTermDebt, totalLiabilities, totalStockholdersEquity, operatingCashFlow, investingCashFlow, capitalExpenditures, financingCashFlow, freeCashFlow, ]; } class ForwardEstimateModel extends Equatable { final String period; final double? expectedRevenue; final double? expectedEps; final double? expectedGrowthRate; const ForwardEstimateModel({ required this.period, this.expectedRevenue, this.expectedEps, this.expectedGrowthRate, }); factory ForwardEstimateModel.fromJson(Map json) { return ForwardEstimateModel( period: json['period']?.toString() ?? '', expectedRevenue: json['expectedRevenue'] != null ? double.tryParse(json['expectedRevenue'].toString()) : null, expectedEps: json['expectedEps'] != null ? double.tryParse(json['expectedEps'].toString()) : null, expectedGrowthRate: json['expectedGrowthRate'] != null ? double.tryParse(json['expectedGrowthRate'].toString()) : null, ); } Map toJson() { return { 'period': period, 'expectedRevenue': expectedRevenue, 'expectedEps': expectedEps, 'expectedGrowthRate': expectedGrowthRate, }; } @override List get props => [period, expectedRevenue, expectedEps, expectedGrowthRate]; } class TickerModel extends Equatable { final String ticker; final String? exchange; final String? tradingCurrency; final double currentPrice; const TickerModel({ required this.ticker, this.exchange, this.tradingCurrency, this.currentPrice = 0.0, }); factory TickerModel.fromJson(Map json) { return TickerModel( ticker: json['ticker']?.toString() ?? '', exchange: json['exchange']?.toString(), tradingCurrency: json['tradingCurrency']?.toString(), currentPrice: json['currentPrice'] != null ? double.tryParse(json['currentPrice'].toString()) ?? 0.0 : 0.0, ); } Map toJson() { return { 'ticker': ticker, 'exchange': exchange, 'tradingCurrency': tradingCurrency, 'currentPrice': currentPrice, }; } @override List get props => [ticker, exchange, tradingCurrency, currentPrice]; }