Files
be_happy_public/lib/data/network/api_service.dart
2026-05-12 12:02:40 +03:00

1073 lines
31 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:async/async.dart';
import 'package:be_happy/data/exceptions/auth_block_exception.dart';
import 'package:be_happy/data/exceptions/auth_exception.dart';
import 'package:be_happy/data/exceptions/route_history_not_found_exception.dart';
import 'package:be_happy/data/exceptions/scooter_not_found_exception.dart';
import 'package:be_happy/data/exceptions/unauthorized_exception.dart';
import 'package:be_happy/data/exceptions/wrong_zone_exception.dart';
import 'package:be_happy/domain/entities/active_scooter_order.dart';
import 'package:be_happy/domain/entities/scooter.dart';
import 'package:be_happy/domain/service/security_service.dart';
import 'package:dio/dio.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:intl/intl.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart';
import 'package:flutter_client_sse/constants/sse_request_type_enum.dart';
import 'package:flutter_client_sse/flutter_client_sse.dart';
import '../../domain/entities/point.dart';
import '../../domain/entities/user_profile.dart';
import '../../domain/entities/payment_card.dart';
import '../../domain/entities/scooter_order.dart';
import '../../domain/entities/subscription.dart';
import '../../domain/entities/user_check_flags.dart';
import '../../domain/entities/certificate.dart';
import '../models/scooters_response.dart';
import '../models/tariffs_response.dart';
import '../models/zones_response.dart';
import '../models/subscriptions_response.dart';
import '../models/user_check_response_dto.dart';
class ApiService {
static const String baseUrl = "https://sharing-api.sparkit.by/api/v1";
static const String fileBaseUrl = "https://sharing-api.sparkit.by";
final SecurityService _securityService;
final Dio _dio;
ApiService(this._securityService, this._dio);
Future<Options> _getAuthOptions() async {
final accessToken = await _securityService.getAccessToken();
return Options(headers: {"Authorization": "Bearer $accessToken"});
}
Future<String?> sendPhone(String phone, String model, String systemId) async {
try {
final response = await _dio.post(
"$baseUrl/auth/client/login",
data: {"phone": phone, "model": model, "systemId": systemId},
);
if (response.statusCode == 200 || response.statusCode == 201) {
return response.data["token"];
}
return null;
} catch (e) {
return null;
}
}
Future<Map<String, String>?> verifyCode(String code, String token) async {
try {
final response = await _dio.post(
"$baseUrl/auth/client/verify",
data: {"code": code, "token": token},
);
if (response.statusCode == 200 || response.statusCode == 201) {
return {
"accessToken": response.data["accessToken"],
"refreshToken": response.data["refreshToken"],
};
}
return null;
} on DioException catch (e) {
if (e.response?.statusCode == 400) {
final attempts =
int.tryParse(e.response?.data["message"]?.toString() ?? "0") ?? 0;
if (attempts == 0) throw AuthBlockException();
throw AuthException("Ошибка. Неверный код.", attempts);
} else if (e.response?.statusCode == 403) {
throw AuthBlockException();
}
rethrow;
}
}
Future<Map<String, String>?> refresh() async {
final refreshToken = await _securityService.getRefreshToken();
try {
final response = await _dio.get(
"$baseUrl/auth/client/refresh",
options: Options(headers: {"Authorization": "Bearer $refreshToken"}),
);
if (response.statusCode == 200) {
return {
"accessToken": response.data["accessToken"],
"refreshToken": response.data["refreshToken"],
};
}
return null;
} catch (e) {
return null;
}
}
Future<UserProfile?> getProfile() async {
try {
final response = await _dio.get(
"$baseUrl/client/me",
options: await _getAuthOptions(),
);
if (response.statusCode == 200) {
final data = response.data;
final profileData = data["profile"];
if (profileData == null) return null;
String? parsedDate;
if (profileData["dob"] != null) {
try {
parsedDate = DateFormat(
'dd.MM.yyyy',
).format(DateTime.parse(profileData["dob"]));
} catch (_) {}
}
final int? avatarId = profileData["avatarId"];
String? avatarUrl;
if (avatarId != null && profileData["avatar"] != null) {
final String? avatarPath = profileData["avatar"]["path"];
if (avatarPath != null && avatarPath.isNotEmpty) {
avatarUrl = Uri.parse(fileBaseUrl).resolve(avatarPath).toString(); }
}
dynamic balanceRaw = profileData["balance"];
int? balance;
if (balanceRaw is int) {
balance = balanceRaw;
} else if (balanceRaw is String) {
balance = int.tryParse(balanceRaw);
}
return UserProfile(
name: profileData["name"] ?? "",
birthDate: parsedDate ?? "",
phone: data["phone"] ?? "",
balance: balance,
email: profileData["email"] ?? "",
avatarId: avatarId,
avatarUrl: avatarUrl,
);
}
return null;
} catch (e) {
return null;
}
}
Future<List<PaymentCard>> getPaymentCards() async {
final response = await _dio.get(
"$baseUrl/client/me",
options: await _getAuthOptions(),
);
if (response.statusCode == 200) {
final List<dynamic> cardsJson = response.data["clientCards"] ?? [];
return Future.wait(
cardsJson.map((cardJson) async {
return PaymentCard(
id: cardJson['id'],
clientId: cardJson['clientId'],
expirationMonth: cardJson['expirationMonth'],
expirationYear: cardJson['expirationYear'],
cardHolder: cardJson['cardHolder'],
cardLastNumber: cardJson['cardLastNumber'],
isMain: cardJson['isMain'],
type: cardJson['cardType'],
);
}),
);
}
return [];
}
Future<UserProfile?> updateProfile(UserProfile profile) async {
const url = "$baseUrl/client/me";
final accessToken = await _securityService.getAccessToken();
final Map<String, dynamic> body = {
if (profile.email != null && profile.email!.isNotEmpty)
"email": profile.email,
if (profile.name != null && profile.name!.isNotEmpty)
"name": profile.name,
if (profile.birthDate.isNotEmpty) "dob": profile.birthDate,
if (profile.avatarId != null) "avatarId": profile.avatarId,
};
if (body.isEmpty) return null;
try {
final response = await _dio.patch(
url,
data: body,
options: Options(headers: {"Authorization": "Bearer $accessToken"}),
);
if (response.statusCode == 200) {
final data = response.data;
final profileData = data["profile"];
if (profileData == null) return null;
String? parsedDate;
if (profileData["dob"] != null) {
try {
parsedDate = DateFormat(
'dd.MM.yyyy',
).format(DateTime.parse(profileData["dob"]));
} catch (_) {}
}
final int? avatarId = profileData["avatarId"];
String? avatarUrl;
if (avatarId != null && profileData["avatar"] != null) {
final String? avatarPath = profileData["avatar"]["path"];
if (avatarPath != null && avatarPath.isNotEmpty) {
avatarUrl = "$fileBaseUrl/${avatarPath.replaceFirst('/', '')}";
}
}
dynamic balanceRaw = data["balance"];
int? balance;
if (balanceRaw is int) {
balance = balanceRaw;
} else if (balanceRaw is String) {
balance = int.tryParse(balanceRaw);
}
return UserProfile(
name: profileData["name"] ?? "",
birthDate: parsedDate ?? "",
phone: data["phone"] ?? "",
balance: balance,
email: profileData["email"] ?? "",
avatarId: avatarId,
avatarUrl: avatarUrl,
);
}
return null;
} on DioException catch (e) {
print(
"ERROR: updateProfile failed: ${e.response?.statusCode} - ${e.message}",
);
rethrow;
}
}
Future<UserCheckFlags?> checkUser() async {
const url = "$baseUrl/scooterorder/check";
final accessToken = await _securityService.getAccessToken();
try {
final response = await _dio.post(
url,
options: Options(headers: {"Authorization": "Bearer $accessToken"}),
);
if (response.statusCode == 200 || response.statusCode == 201) {
final dto = UserCheckResponseDto.fromJson(response.data);
return UserCheckFlags(
hasFine: dto.hasFine,
hasUnpaidOrder: dto.hasUnpaidOrder,
hasCard: dto.hasCard,
);
}
return null;
} catch (e) {
print("ERROR: checkUser failed: $e");
return null;
}
}
Future<int?> uploadPhoto(File imageFile) async {
try {
final mimeType = lookupMimeType(imageFile.path) ?? 'image/jpeg';
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
imageFile.path,
filename: basename(imageFile.path),
contentType: MediaType.parse(mimeType),
),
});
final response = await _dio.post(
"$baseUrl/client/upload",
data: formData,
options: await _getAuthOptions(),
);
return response.data['id'];
} catch (e) {
rethrow;
}
}
Future<List<int>> uploadScooterPhotos(List<File> images) async {
try {
final formData = FormData();
for (var file in images) {
final mimeType = lookupMimeType(file.path) ?? 'image/jpeg';
formData.files.add(
MapEntry(
'files',
await MultipartFile.fromFile(
file.path,
filename: basename(file.path),
contentType: MediaType.parse(mimeType),
),
),
);
}
final response = await _dio.post(
"$baseUrl/scooterorder/upload",
data: formData,
options: await _getAuthOptions(),
);
final List<dynamic> list = response.data['data'] ?? [];
return list.map((item) => item['id'] as int).toList();
} catch (e) {
rethrow;
}
}
Future<ScootersResponse?> getScooters({
required List<double> area,
int page = 1,
int pageSize = 500,
}) async {
const path = "$baseUrl/scooter/available";
final queryParams = {
'readAll': 'true',
'area': area.map((coord) => coord.toString()).toList(),
'page': page,
'pageSize': pageSize,
};
final accessToken = await _securityService.getAccessToken();
if (accessToken == null) return null;
try {
final response = await _dio.get(
path,
queryParameters: queryParams,
options: Options(headers: {"Authorization": "Bearer $accessToken"}),
);
if (response.statusCode == 200) {
return ScootersResponse.fromJson(response.data);
}
return null;
} catch (e) {
print("ERROR: getScooters failed: $e");
return null;
}
}
Future<Scooter?> getScooterById({required int id}) async {
try {
final response = await _dio.get(
"$baseUrl/scooter/$id/client",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return Scooter.fromJson(response.data);
}
} on DioException catch (e) {
if (e.response?.statusCode == 401) throw UnauthorizedException();
if (e.response?.statusCode == 403) throw AuthBlockException();
}
return null;
}
Future<Scooter?> getScooterByTitle({required String title}) async {
final url = "$baseUrl/scooter/$title/code";
try {
final response = await _dio.get(
url,
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return Scooter.fromJson(response.data);
}
if (response.statusCode == 404) {
throw ScooterNotFoundException(message: "Самокат не найден");
}
return null;
} on DioException catch (e) {
if (e.response?.statusCode == 401) throw UnauthorizedException();
if (e.response?.statusCode == 403) throw AuthBlockException();
if (e.response?.statusCode == 404) throw ScooterNotFoundException();
return null;
}
}
Future<ZonesResponse?> getZones({
required List<double> area,
int page = 1,
int pageSize = 500,
}) async {
const path = "$baseUrl/zone/available";
final queryParams = {
'readAll': 'true',
'area': area.map((coord) => coord.toString()).toList(),
'page': page,
'pageSize': pageSize,
};
try {
final response = await _dio.get(
path,
queryParameters: queryParams,
options: await _getAuthOptions(),
);
if (response.statusCode == 200) {
return ZonesResponse.fromJson(response.data);
}
return null;
} catch (e) {
print("ERROR: getZones failed: $e");
return null;
}
}
Future<TariffsResponse?> getAvailableTariffs({required int scooterId}) async {
final url = "$baseUrl/scooterplan/$scooterId/available";
try {
final response = await _dio.get(url, options: await _getAuthOptions());
if (response.statusCode == 200) {
return TariffsResponse.fromJson(response.data);
}
return null;
} catch (e) {
print("ERROR: getAvailableTariffs failed: $e");
return null;
}
}
Future<List<Subscription>> getClientSubscriptions() async {
const url = "$baseUrl/scootersubscription/client";
try {
final response = await _dio.get(url, options: await _getAuthOptions());
if (response.statusCode == 200) {
final Map<String, dynamic> responseData = response.data;
final List<dynamic> items = responseData['data'] ?? [];
return items.map((item) {
final Map<String, dynamic> subscriptionMap =
Map<String, dynamic>.from(item['subscription'] ?? {});
if (item['expiredAt'] != null) {
subscriptionMap['activeTo'] = item['expiredAt'];
}
return Subscription.fromJson(subscriptionMap);
}).toList();
}
return [];
} catch (e) {
print("APISERVICE (getClientSubscriptions) Error: $e");
return [];
}
}
Future<SubscriptionsResponse?> getAvailableSubscriptions() async {
const url = "$baseUrl/scootersubscription/available";
try {
final response = await _dio.get(url, options: await _getAuthOptions());
if (response.statusCode == 200) {
return SubscriptionsResponse.fromJson(response.data);
}
return null;
} catch (e) {
print("APISERVICE (getAvailableSubscriptions) Error: $e");
return null;
}
}
Future<Subscription?> getSubscriptionById({required int id}) async {
final url = "$baseUrl/scootersubscription/$id/client";
try {
final response = await _dio.get(url, options: await _getAuthOptions());
if (response.statusCode == 200 || response.statusCode == 201) {
return Subscription.fromJson(response.data);
}
return null;
} on DioException catch (e) {
if (e.response?.statusCode == 401) throw UnauthorizedException();
if (e.response?.statusCode == 403) throw AuthBlockException();
print("APISERVICE (getSubscriptionById) Error: $e");
return null;
}
}
Future<bool> activateSubscription({required int optionId}) async {
const url = "$baseUrl/scootersubscription/client";
try {
final response = await _dio.post(
url,
data: {"optionId": optionId},
options: await _getAuthOptions(),
);
if (response.statusCode == 201) {
return response.data["success"] == true;
}
return false;
} on DioException catch (e) {
if (e.response?.statusCode == 401) throw UnauthorizedException();
if (e.response?.statusCode == 403) throw AuthBlockException();
print("APISERVICE (activateSubscription) Error: $e");
return false;
}
}
Future<int> addPaymentCard({
required String cardNumber,
required String cardHolder,
required int expirationMonth,
required int expirationYear,
required String cvv,
}) async {
const url = "$baseUrl/client/card";
final cleanCardNumber = cardNumber.replaceAll(' ', '');
try {
final response = await _dio.post(
url,
data: {
"cardNumber": cleanCardNumber,
"cardHolder": cardHolder,
"expirationMonth": expirationMonth,
"expirationYear": expirationYear,
"cvv": cvv,
},
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return response.data['id'] as int;
}
throw AuthException('Непредвиденный статус: ${response.statusCode}', 0);
} on DioException catch (e) {
final data = e.response?.data;
final statusCode = e.response?.statusCode;
if (statusCode == 400) {
final message = _parseErrorMessage(data);
throw AuthException(message ?? 'Неверные данные карты', 0);
} else if (statusCode == 401) {
throw UnauthorizedException();
} else if (statusCode == 403) {
throw AuthBlockException();
} else if (statusCode == 404) {
throw AuthException('Пользователь не найден', 0);
}
throw AuthException('Ошибка сервера: $statusCode', 0);
}
}
Future<void> setMainPaymentCard(int cardId) async {
final url = "$baseUrl/client/card/$cardId";
try {
await _dio.put(
url,
data: {"isMain": true},
options: await _getAuthOptions(),
);
} on DioException catch (e) {
final statusCode = e.response?.statusCode;
if (statusCode == 400) {
final message = _parseErrorMessage(e.response?.data);
throw AuthException(
message ?? 'Ошибка при установке основной карты',
0,
);
} else if (statusCode == 401) {
throw UnauthorizedException();
} else if (statusCode == 403) {
throw AuthBlockException();
} else if (statusCode == 404) {
throw AuthException('Карта не найдена', 0);
}
throw AuthException('Ошибка сервера: $statusCode', 0);
}
}
Future<void> removePaymentCard(int cardId) async {
final url = "$baseUrl/client/card/$cardId";
try {
await _dio.delete(url, options: await _getAuthOptions());
} on DioException catch (e) {
final statusCode = e.response?.statusCode;
if (statusCode == 400) {
final message = _parseErrorMessage(e.response?.data);
throw AuthException(message ?? 'Ошибка при удалении карты', 0);
} else if (statusCode == 401) {
throw UnauthorizedException();
} else if (statusCode == 403) {
throw AuthBlockException();
} else if (statusCode == 404) {
throw AuthException('Карта не найдена', 0);
}
throw AuthException('Ошибка сервера: $statusCode', 0);
}
}
Future<ScooterOrder?> bookScooter({
required int scooterId,
required int planId,
int? subscriptionId,
int? cardId,
required bool isBalance,
required bool isInsurance,
}) async {
try {
final response = await _dio.post(
"$baseUrl/scooterorder/booking",
data: {
"scooterId": scooterId,
"planId": planId,
"subscriptionId": subscriptionId,
"cardId": cardId,
"isBalance": isBalance,
"isInsurance": isInsurance,
},
options: await _getAuthOptions(),
);
return ScooterOrder.fromJson(response.data);
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<ScooterOrder?> startRide(int orderId) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/start",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<ScooterOrder?> cancelRide(int orderId) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/cancel",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<ScooterOrder?> pauseRide(int orderId) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/pause",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<ScooterOrder?> resumeRide(int orderId) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/resume",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<ScooterOrder?> finishRide({
required int orderId,
required List<int> filesId,
}) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/finish",
data: {"files": filesId},
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
if (e.response?.statusCode == 400) {
final data = e.response?.data;
if (data is Map && data['message'] is List) {
final firstError = data['message'][0]['message'].toString();
if (firstError.contains("Wrong zone")) {
throw WrongZoneException(message: firstError);
}
}
if (data is Map && data['message'] is String) {}
} else {
_handleDioError(e);
}
rethrow;
}
}
Future<ScooterOrder?> payRide(int orderId) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/pay",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<List<ScooterOrder>> getClientOrders() async {
try {
final response = await _dio.get(
"$baseUrl/scooterorder/active",
options: await _getAuthOptions(),
);
if (response.statusCode == 200) {
final data = response.data;
if (data is Map<String, dynamic> && data['data'] is List) {
final List ordersList = data['data'];
return ordersList.map((json) => ScooterOrder.fromJson(json)).toList();
}
}
return [];
} on DioException catch (e) {
_handleDioError(e);
return [];
}
}
Future<ActiveScooterOrder?> updateScooterOrderData({
required int orderId,
Map<String, dynamic>? data,
}) async {
try {
final response = await _dio.get(
"$baseUrl/scooterorder/$orderId/data",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ActiveScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<ScooterOrder?> getScooterOrderById({required int id}) async {
try {
final response = await _dio.get(
"$baseUrl/scooterorder/$id/client",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<ScooterOrder?> payScooterOrderWithPhotos({
required int orderId,
required int? cardId,
required bool isBalance,
}) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/pay",
data: {"cardId": cardId, "isBalance": isBalance},
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<Map<String, dynamic>> getScooterOrderHistory({
int page = 1,
int pageSize = 20,
}) async {
try {
final response = await _dio.get(
"$baseUrl/scooterorder/history",
queryParameters: {"page": page, "pageSize": pageSize},
options: await _getAuthOptions(),
);
return response.data;
} on DioException catch (e) {
_handleDioError(e);
return {};
}
}
Future<List<Point>> getScooterOrderRouteHistory({required int id}) async {
try {
final response = await _dio.get(
"$baseUrl/scooterorder/$id/routehistory",
options: await _getAuthOptions(),
);
if (response.statusCode == 200) {
final String routeString = response.data['route'] ?? '[]';
final List<dynamic> routeList = json.decode(routeString);
return routeList.map((item) => Point(
(item[1] as num).toDouble(),
(item[0] as num).toDouble(),
)).toList();
}
throw RouteHistoryNotFoundException(message: "История маршрута не найдена");
} on DioException catch (e) {
if (e.response?.statusCode == 401) throw UnauthorizedException();
if (e.response?.statusCode == 403) throw AuthBlockException();
if (e.response?.statusCode == 404) throw RouteHistoryNotFoundException();
rethrow;
}
}
Future<Map<String, dynamic>> getNews() async {
try {
final response = await _dio.get(
"$baseUrl/news",
options: await _getAuthOptions(),
);
return response.data;
} on DioException catch (e) {
_handleDioError(e);
return {};
}
}
Future<Map<String, dynamic>?> getNewsById(int newsId) async {
try {
final response = await _dio.get(
'$baseUrl/news/$newsId',
options: await _getAuthOptions(),
);
return response.data;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<Map<String, dynamic>?> cancelNotification(int id) async {
try {
final response = await _dio.put(
"$baseUrl/notification/client/$id/cancel",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return response.data;
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Stream<Map<String, dynamic>> getNotificationsStream() {
final url = "$baseUrl/notification/client/stream";
final controller = StreamController<Map<String, dynamic>>();
_securityService.getAccessToken().then((accessToken) {
if (accessToken == null) {
controller.addError(UnauthorizedException());
return;
}
print("[SSE] Subscribing via flutter_client_sse...");
SSEClient.subscribeToSSE(
method: SSERequestType.GET,
url: url,
header: {
"Authorization": "Bearer $accessToken",
"Accept": "text/event-stream",
"Cache-Control": "no-cache",
},
).listen(
(event) {
print("Received line [SSE]: id=${event.id}, event=${event.event}");
print("Data [SSE]: ${event.data}");
if (event.data != null && event.data!.isNotEmpty) {
try {
final Map<String, dynamic> jsonData = jsonDecode(event.data!);
print("✅ [SSE] Parsed JSON: $jsonData");
controller.add(jsonData);
} catch (e) {
print("❌ [SSE] JSON Parse Error: $e | Raw: ${event.data}");
}
}
},
onError: (error) {
print(" [SSE] Library Error: $error");
controller.addError(error);
},
onDone: () {
print(" [SSE] Library Stream Closed");
controller.close();
},
);
});
return controller.stream;
}
Future<List<Certificate>> getCertificates() async {
try {
final response = await _dio.get(
"$baseUrl/certificate",
options: await _getAuthOptions(),
);
if (response.statusCode == 200) {
final data = response.data;
final dataList = data['data'] as List<dynamic>;
return dataList.map((json) => Certificate.fromJson(json)).toList();
}
return [];
} on DioException catch (e) {
_handleDioError(e);
return [];
}
}
Future<Map<String, dynamic>?> purchaseCertificate({
required int certificateId,
required int cardId,
}) async {
try {
final response = await _dio.post(
"$baseUrl/certificate/$certificateId",
data: {"cardId": cardId},
options: await _getAuthOptions(),
);
if (response.statusCode == 201) {
return response.data;
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
void _handleDioError(DioException e) {
if (e.response?.statusCode == 401) throw UnauthorizedException();
if (e.response?.statusCode == 403) throw AuthBlockException();
final message = _parseErrorMessage(e.response?.data);
throw AuthException(message ?? 'Ошибка сервера', 0);
}
String? _parseErrorMessage(dynamic data) {
if (data is Map<String, dynamic>) {
final messages = data['message'] as List?;
if (messages != null && messages.isNotEmpty) {
final firstError = messages.first as Map<String, dynamic>?;
return firstError?['message'] as String?;
}
return data['message'] as String?;
}
return null;
}
}