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/client_subscription.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 _getAuthOptions() async { final accessToken = await _securityService.getAccessToken(); return Options(headers: {"Authorization": "Bearer $accessToken"}); } Future 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?> 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?> 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 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> getPaymentCards() async { final response = await _dio.get( "$baseUrl/client/me", options: await _getAuthOptions(), ); if (response.statusCode == 200) { final List 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 updateProfile(UserProfile profile) async { const url = "$baseUrl/client/me"; final accessToken = await _securityService.getAccessToken(); final Map 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 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 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> uploadScooterPhotos(List 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 list = response.data['data'] ?? []; return list.map((item) => item['id'] as int).toList(); } catch (e) { rethrow; } } Future getScooters({ required List 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 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 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 getZones({ required List 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 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> getClientSubscriptions() async { const url = "$baseUrl/scootersubscription/client"; try { final response = await _dio.get(url, options: await _getAuthOptions()); if (response.statusCode == 200) { final Map responseData = response.data; final List items = responseData['data'] ?? []; return items.map((item) { return ClientSubscription.fromJson(item); }).toList(); } return []; } catch (e) { print("APISERVICE (getClientSubscriptions) Error: $e"); return []; } } Future 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 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 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 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; } 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 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 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 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) { 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 start zone")) { throw WrongZoneException( message: "Некорректная зона для начала поездки.", ); } } if (data is Map && data['message'] is String) {} } else { _handleDioError(e); } rethrow; } } Future 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) { 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 start zone")) { throw WrongZoneException( message: "Некорректная зона для начала поездки.", ); } } if (data is Map && data['message'] is String) {} } else { _handleDioError(e); } rethrow; } } Future 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 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 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 finishRide({ required int orderId, required List 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 start zone")) { throw WrongZoneException( message: "Некорректная зона для завершения поездки.", ); } } if (data is Map && data['message'] is String) {} } else { _handleDioError(e); } rethrow; } } Future 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; } } on DioException catch (e) { _handleDioError(e); return; } } Future> 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 && 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 updateScooterOrderData({ required int orderId, Map? 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 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 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; } } on DioException catch (e) { _handleDioError(e); } } Future> 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> 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 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> getNews() async { try { final response = await _dio.get( "$baseUrl/news", options: await _getAuthOptions(), ); return response.data; } on DioException catch (e) { _handleDioError(e); return {}; } } Future?> 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?> 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> getNotificationsStream() { final url = "$baseUrl/notification/client/stream"; final controller = StreamController>(); _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 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>> getNotifications() async { final url = Uri.parse('$baseUrl/notification/client'); final accessToken = await _securityService.getAccessToken(); if (accessToken == null) { print("APISERVICE Error: Access token is null."); throw UnauthorizedException(); } print("GET NOTIFICATIONS REQUEST:"); print("URL: $url"); final response = await http.get( url, headers: { "Content-Type": "application/json", "Authorization": "Bearer $accessToken", }, ); print("GET NOTIFICATIONS RESPONSE:"); print("STATUS: ${response.statusCode}"); print("BODY: ${response.body}"); if (response.statusCode == 200) { final data = jsonDecode(utf8.decode(response.bodyBytes)); // ✅ Проверяем, является ли ответ массивом или объектом с data[] if (data is List) { return data.cast>(); } else if (data is Map) { final list = data['data']; if (list is List) { return list.cast>(); } else { throw Exception( 'Expected a List under "data" but got ${list.runtimeType}', ); } } else { throw Exception('Expected a List or Map but got ${data.runtimeType}'); } } else if (response.statusCode == 401) { throw UnauthorizedException(); } else if (response.statusCode == 403) { throw AuthBlockException(); } throw Exception('Ошибка сервера: ${response.statusCode}'); } Future> 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; return dataList.map((json) => Certificate.fromJson(json)).toList(); } return []; } on DioException catch (e) { _handleDioError(e); return []; } } Future?> 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) { final messages = data['message'] as List?; if (messages != null && messages.isNotEmpty) { final firstError = messages.first as Map?; return firstError?['message'] as String?; } return data['message'] as String?; } return null; } Future> applyPromoCode(String code) async { final token = await _securityService.getAccessToken(); if (token == null) throw Exception('No access token'); final response = await _dio.post( '$baseUrl/promocode/apply', data: {'code': code}, options: Options( headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ), ); if (response.statusCode == 201 || response.statusCode == 200) { return response.data; } else if (response.statusCode == 404) { throw PromoCodeNotFoundException(); } else { throw Exception('API Error: ${response.statusCode}'); } } } // ✅ Добавь исключение (можно в отдельный файл) class PromoCodeNotFoundException implements Exception {}