150 lines
5.9 KiB
Dart
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');
|
|
}
|
|
}
|
|
|