feat(asset_detail): modular fundamentals sections, executive salaries and logo resolution
This commit is contained in:
@@ -1,103 +1,124 @@
|
||||
import 'package:finlytic_app/core/network/api_client.dart';
|
||||
|
||||
import 'package:finlytic_app/features/asset_detail/models/fundamental_data_model.dart';
|
||||
import 'package:finlytic_app/features/asset_detail/models/technical_analysis_model.dart';
|
||||
import 'package:finlytic_app/features/asset_detail/models/manual_analysis_request_dto.dart';
|
||||
import 'package:finlytic_app/features/asset_detail/models/manual_analysis_response_dto.dart';
|
||||
import 'package:finlytic_app/features/trades/models/trade_model.dart';
|
||||
import 'package:finlytic_app/features/trades/models/trade_acceptance_dto.dart';
|
||||
import 'package:finlytic_app/features/trades/models/close_trade_request_dto.dart';
|
||||
import 'package:finlytic_app/features/trades/repositories/trade_repository.dart';
|
||||
|
||||
class AssetRepository {
|
||||
final ApiClient apiClient;
|
||||
final TradeRepository _tradeRepository;
|
||||
|
||||
AssetRepository({required this.apiClient});
|
||||
// In-memory request deduplication & cache
|
||||
final Map<String, Future<FundamentalDataModel?>> _pendingFundamentals = {};
|
||||
final Map<String, FundamentalDataModel> _fundamentalsCache = {};
|
||||
|
||||
final Map<String, Future<TechnicalAnalysisModel?>> _pendingTechnicals = {};
|
||||
final Map<String, TechnicalAnalysisModel> _technicalsCache = {};
|
||||
|
||||
AssetRepository({required this.apiClient, TradeRepository? tradeRepository})
|
||||
: _tradeRepository = tradeRepository ?? TradeRepository(apiClient: apiClient);
|
||||
|
||||
String _buildCacheKey(String isin, String? ticker) => '${isin.toUpperCase()}_${(ticker ?? '').toUpperCase()}';
|
||||
|
||||
Future<FundamentalDataModel?> getAssetFundamentals(String isin, bool forceRefresh, {String? ticker}) async {
|
||||
final key = _buildCacheKey(isin, ticker);
|
||||
|
||||
if (!forceRefresh && _fundamentalsCache.containsKey(key)) {
|
||||
return _fundamentalsCache[key];
|
||||
}
|
||||
|
||||
if (_pendingFundamentals.containsKey(key)) {
|
||||
return await _pendingFundamentals[key];
|
||||
}
|
||||
|
||||
final future = _fetchFundamentals(isin, forceRefresh, ticker: ticker);
|
||||
_pendingFundamentals[key] = future;
|
||||
|
||||
try {
|
||||
final result = await future;
|
||||
if (result != null) {
|
||||
_fundamentalsCache[key] = result;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
_pendingFundamentals.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
Future<FundamentalDataModel?> _fetchFundamentals(String isin, bool forceRefresh, {String? ticker}) async {
|
||||
try {
|
||||
String url = '/api/v1/assets/$isin/fundamentals?forceRefresh=$forceRefresh';
|
||||
if (ticker != null && ticker.isNotEmpty) {
|
||||
url += '&ticker=$ticker';
|
||||
}
|
||||
final res = await apiClient.get(url);
|
||||
if (res.statusCode == 200 && res.data != null) {
|
||||
if (res.statusCode == 200 && res.data != null && res.data is Map<String, dynamic>) {
|
||||
return FundamentalDataModel.fromJson(res.data);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching fundamentals for $isin: $e');
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<TechnicalAnalysisModel?> getAssetTechnical(String isin, bool forceRefresh, {String? ticker}) async {
|
||||
final key = _buildCacheKey(isin, ticker);
|
||||
|
||||
if (!forceRefresh && _technicalsCache.containsKey(key)) {
|
||||
return _technicalsCache[key];
|
||||
}
|
||||
|
||||
if (_pendingTechnicals.containsKey(key)) {
|
||||
return await _pendingTechnicals[key];
|
||||
}
|
||||
|
||||
final future = _fetchTechnicals(isin, forceRefresh, ticker: ticker);
|
||||
_pendingTechnicals[key] = future;
|
||||
|
||||
try {
|
||||
final result = await future;
|
||||
if (result != null) {
|
||||
_technicalsCache[key] = result;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
_pendingTechnicals.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
Future<TechnicalAnalysisModel?> _fetchTechnicals(String isin, bool forceRefresh, {String? ticker}) async {
|
||||
try {
|
||||
String url = '/api/v1/assets/$isin/technicals?forceRefresh=$forceRefresh';
|
||||
if (ticker != null && ticker.isNotEmpty) {
|
||||
url += '&ticker=$ticker';
|
||||
}
|
||||
final res = await apiClient.get(url);
|
||||
if (res.statusCode == 200 && res.data != null) {
|
||||
if (res.statusCode == 200 && res.data != null && res.data is Map<String, dynamic>) {
|
||||
return TechnicalAnalysisModel.fromJson(res.data);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching TA for $isin: $e');
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<TradeModel>> getAssetTrades(String isin, String? status) async {
|
||||
try {
|
||||
String url = '/api/v1/user/trades?isin=$isin';
|
||||
if (status != null) url += '&status=$status';
|
||||
final res = await apiClient.get(url);
|
||||
if (res.statusCode == 200 && res.data != null) {
|
||||
final List<dynamic> list = res.data;
|
||||
return list.map((json) => TradeModel.fromJson(json)).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching trades for $isin: $e');
|
||||
}
|
||||
return [];
|
||||
return _tradeRepository.fetchTrades(isin: isin, status: status);
|
||||
}
|
||||
|
||||
Future<ManualAnalysisResponseDto?> triggerManualAnalysis(String isin, {ManualAnalysisRequestDto? payload}) async {
|
||||
try {
|
||||
final body = payload != null ? payload.toJson() : {'isin': isin};
|
||||
final res = await apiClient.post('/api/v1/analyze/manual', data: body);
|
||||
if (res.statusCode == 200 && res.data != null && res.data is Map<String, dynamic>) {
|
||||
return ManualAnalysisResponseDto.fromJson(res.data);
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Error triggering manual analysis for $isin: $e');
|
||||
rethrow;
|
||||
final body = payload != null ? payload.toJson() : {'isin': isin};
|
||||
final res = await apiClient.post('/api/v1/analyze/manual', data: body);
|
||||
if (res.statusCode == 200 && res.data != null && res.data is Map<String, dynamic>) {
|
||||
return ManualAnalysisResponseDto.fromJson(res.data);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> rejectTrade(String tradeId) async {
|
||||
try {
|
||||
await apiClient.post('/api/v1/user/trades/$tradeId/reject');
|
||||
} catch (e) {
|
||||
print('Error rejecting trade $tradeId: $e');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Future<void> rejectTrade(String tradeId) async => _tradeRepository.rejectTrade(tradeId);
|
||||
|
||||
Future<void> acceptTrade(TradeAcceptanceDto tradeAcceptanceDto) async {
|
||||
try {
|
||||
final payload = tradeAcceptanceDto.toJson();
|
||||
await apiClient.post('/api/v1/user/trades/accept', data: payload);
|
||||
} catch (e) {
|
||||
print('Error accepting trade: $e');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Future<void> acceptTrade(TradeAcceptanceDto tradeAcceptanceDto) async => _tradeRepository.acceptTrade(tradeAcceptanceDto);
|
||||
|
||||
Future<void> closeTrade(String tradeId, double exitPrice) async {
|
||||
try {
|
||||
await apiClient.post('/api/v1/user/trades/$tradeId/close', data: {'userExitPrice': exitPrice});
|
||||
} catch (e) {
|
||||
print('Error closing trade $tradeId: $e');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Future<void> closeTrade(String tradeId, double exitPrice) async =>
|
||||
_tradeRepository.closeTrade(tradeId, dto: CloseTradeRequestDto(userExitPrice: exitPrice));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user