Files
Finlytic/FinlyticApp/lib/features/trades/repositories/trade_repository.dart
T

150 lines
5.9 KiB
Dart

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<List<TradeModel>> fetchTrades({String? isin, String? status}) async {
try {
final queryParams = <String, dynamic>{
'_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<dynamic> 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<TradeProposalDto>` — 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<List<TradeProposalModel>> 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<dynamic> data = response.data;
return data.map((json) => TradeProposalModel.fromJson(Map<String, dynamic>.from(json))).toList();
}
return [];
} catch (e) {
throw Exception('Vorschläge konnten nicht geladen werden: $e');
}
}
Future<List<DerivativeItemModel>> 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 = <String, dynamic>{
'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<dynamic> data = response.data;
return data.map((json) => DerivativeItemModel.fromJson(json)).toList();
}
return [];
} catch (e) {
throw Exception('Derivate konnten nicht geladen werden: $e');
}
}
Future<void> 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<void> 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<TradeModel> 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');
}
}