1101 lines
32 KiB
Dart
1101 lines
32 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) {
|
|
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<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) {
|
|
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<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 start zone")) {
|
|
throw WrongZoneException(message: "Некорректная зона для завершения поездки.");
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|