import 'dart:async'; import 'package:finlytic_app/core/network/api_client.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/models/derivative_item_model.dart'; class TradeRepository { final ApiClient apiClient; const TradeRepository({required this.apiClient}); Future> fetchTrades({String? isin, String? status}) async { try { final queryParams = { '_t': DateTime.now().millisecondsSinceEpoch, }; if (isin != null && isin.isNotEmpty) queryParams['isin'] = isin; if (status != null && status.isNotEmpty) queryParams['status'] = status; final response = await apiClient.get('/api/v1/user/trades', queryParameters: queryParams); if (response.statusCode == 200 && response.data != null) { final List data = response.data; return data.map((json) => TradeModel.fromJson(json)).toList(); } return []; } catch (e) { throw Exception('Trades konnten nicht geladen werden: $e'); } } /// Fetches system-wide, currently active trade proposals /// (`GET /api/v1/user/trades?status=Proposed`). /// /// Unlike [fetchTrades], the server does **not** return `ActiveTradeDto` /// (`TradeModel`) for this query: `UserTradesController.GetUserTrades` /// branches on `status == "Proposed"` and instead calls `engine_GetProposals`, /// which returns `List` — a structurally different shape /// (`proposalId` instead of `id`, no `userId`/PnL fields, since proposals are /// system-wide opportunities not owned by any user). Parsing that response /// as `TradeModel` would silently produce garbage/empty fields, so this is a /// dedicated method that parses `TradeProposalModel` instead of overloading /// [fetchTrades] for two incompatible server-side contracts. Future> fetchProposals() async { try { final response = await apiClient.get('/api/v1/user/trades', queryParameters: { 'status': 'Proposed', '_t': DateTime.now().millisecondsSinceEpoch, }); if (response.statusCode == 200 && response.data != null) { final List data = response.data; return data.map((json) => TradeProposalModel.fromJson(Map.from(json))).toList(); } return []; } catch (e) { throw Exception('Vorschläge konnten nicht geladen werden: $e'); } } Future> fetchDerivatives( String isin, { String optionType = 'long', double? targetLeverage, double? minLeverage, double? maxLeverage, String? search, String? after, int? page, bool forceRefresh = false, }) async { try { final queryParams = { 'optionType': optionType, if (targetLeverage != null && targetLeverage > 0) 'targetLeverage': targetLeverage, if (minLeverage != null) 'minLeverage': minLeverage, if (maxLeverage != null) 'maxLeverage': maxLeverage, if (search != null && search.isNotEmpty) 'search': search, if (after != null && after.isNotEmpty) 'after': after, if (page != null) 'page': page, if (forceRefresh) 'forceRefresh': 'true', '_t': DateTime.now().millisecondsSinceEpoch, }; final response = await apiClient.get('/api/v1/assets/$isin/derivatives', queryParameters: queryParams); if (response.statusCode == 200 && response.data != null) { final List data = response.data; return data.map((json) => DerivativeItemModel.fromJson(json)).toList(); } return []; } catch (e) { throw Exception('Derivate konnten nicht geladen werden: $e'); } } Future acceptTrade(TradeAcceptanceDto dto) async { final response = await apiClient.post('/api/v1/user/trades/accept', data: dto.toJson()); if (response.statusCode != 200) { throw Exception('Trade konnte nicht akzeptiert werden'); } } // NOTE: there is intentionally no `rejectTrade` here anymore. A trade // proposal is a system-wide opportunity that many users may accept // independently; "rejecting" it server-side would have no meaning and // the corresponding endpoint (`POST /api/v1/user/trades/{id}/reject`) has // been removed. Dismissing a proposal is now a purely local UI action // (see `AssetTradesBloc`'s `DismissTradeEvent`) — the proposal keeps // existing server-side until it naturally expires (24h TTL). Future closeTrade(String id, {CloseTradeRequestDto? dto}) async { final response = await apiClient.post('/api/v1/user/trades/$id/close', data: dto?.toJson()); if (response.statusCode != 200) { throw Exception('Trade konnte nicht geschlossen werden'); } } /// Records an additional/corrective fill against an already-active trade /// (`EngineController.AddTradeFill` -> `engine_AddFill`). This is the /// correct server-side counterpart for the "review/edit execution /// numbers" path on an active trade — unlike `acceptTrade`, which targets /// a proposal, not an existing trade. `userId`/`tradeId` are always /// overwritten server-side from the JWT claim/route, never trusted from /// this payload. Future addTradeFill( String tradeId, { required double executedPrice, required double quantity, double fee = 0, String? note, }) async { final response = await apiClient.post( '/api/v1/engine/trades/$tradeId/fills', data: { 'executedPrice': executedPrice, 'quantity': quantity, 'fee': fee, if (note != null) 'note': note, }, ); if (response.statusCode == 200 && response.data != null) { return TradeModel.fromJson(response.data); } throw Exception('Ausführung konnte nicht gespeichert werden'); } }