payment quotation v2 + payment orchestration v2 draft

This commit is contained in:
Stephan D
2026-02-24 13:01:35 +01:00
parent 0646f55189
commit 6444813f38
289 changed files with 17005 additions and 16065 deletions

View File

@@ -6,7 +6,7 @@ import 'package:pshared/api/requests/login_data.dart';
import 'package:pshared/api/responses/account.dart';
import 'package:pshared/api/responses/login.dart';
import 'package:pshared/api/responses/login_pending.dart';
import 'package:pshared/config/web.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/data/mapper/account/account.dart';
import 'package:pshared/models/account/account.dart';
import 'package:pshared/models/auth/login_outcome.dart';
@@ -31,21 +31,33 @@ class AuthorizationService {
final deviceId = await DeviceIdManager.getDeviceId();
final response = await httpr.getPOSTResponse(
service,
'/login',
LoginRequest(login: login, deviceId: deviceId, clientId: Constants.clientId).toJson(),
'/login',
LoginRequest(
login: login,
deviceId: deviceId,
clientId: Constants.clientId,
).toJson(),
);
if (response.containsKey('refreshToken')) {
return LoginOutcome.completed((await completeLogin(response)).account.toDomain());
return LoginOutcome.completed(
(await completeLogin(response)).account.toDomain(),
);
}
if (response.containsKey('pendingToken')) {
final pending = PendingLogin.fromResponse(
PendingLoginResponse.fromJson(response),
session: SessionIdentifier(clientId: Constants.clientId, deviceId: deviceId),
session: SessionIdentifier(
clientId: Constants.clientId,
deviceId: deviceId,
),
);
return LoginOutcome.pending(pending);
}
throw AuthenticationFailedException('Unexpected login response', Exception(response.toString()));
throw AuthenticationFailedException(
'Unexpected login response',
Exception(response.toString()),
);
}
static Future<void> _updateAccessToken(AccountResponse response) async {
@@ -57,13 +69,16 @@ class AuthorizationService {
return AuthorizationStorage.updateRefreshToken(response.refreshToken);
}
static Future<LoginResponse> _completeLogin(Map<String, dynamic> response) async {
static Future<LoginResponse> _completeLogin(
Map<String, dynamic> response,
) async {
final LoginResponse lr = LoginResponse.fromJson(response);
await _updateTokens(lr);
return lr;
}
static Future<LoginResponse> completeLogin(Map<String, dynamic> response) => _completeLogin(response);
static Future<LoginResponse> completeLogin(Map<String, dynamic> response) =>
_completeLogin(response);
static Future<Account> restore() async {
return (await TokenService.refreshAccessToken()).account.toDomain();
@@ -74,82 +89,123 @@ class AuthorizationService {
}
// Original AuthorizationService methods - keeping the interface unchanged
static Future<Map<String, dynamic>> getGETResponse(String service, String url) async {
static Future<Map<String, dynamic>> getGETResponse(
String service,
String url,
) async {
final token = await TokenService.getAccessTokenSafe();
return httpr.getGETResponse(service, url, authToken: token);
}
static Future<httpr.BinaryResponse> getGETBinaryResponse(String service, String url) async {
static Future<httpr.BinaryResponse> getGETBinaryResponse(
String service,
String url,
) async {
final token = await TokenService.getAccessTokenSafe();
return httpr.getBinaryGETResponse(service, url, authToken: token);
}
static Future<Map<String, dynamic>> getPOSTResponse(String service, String url, Map<String, dynamic> body) async {
static Future<Map<String, dynamic>> getPOSTResponse(
String service,
String url,
Map<String, dynamic> body,
) async {
final token = await TokenService.getAccessTokenSafe();
return httpr.getPOSTResponse(service, url, body, authToken: token);
}
static Future<Map<String, dynamic>> getPUTResponse(String service, String url, Map<String, dynamic> body) async {
static Future<Map<String, dynamic>> getPUTResponse(
String service,
String url,
Map<String, dynamic> body,
) async {
final token = await TokenService.getAccessTokenSafe();
return httpr.getPUTResponse(service, url, body, authToken: token);
}
static Future<Map<String, dynamic>> getPATCHResponse(String service, String url, Map<String, dynamic> body) async {
static Future<Map<String, dynamic>> getPATCHResponse(
String service,
String url,
Map<String, dynamic> body,
) async {
final token = await TokenService.getAccessTokenSafe();
return httpr.getPATCHResponse(service, url, body, authToken: token);
}
static Future<Map<String, dynamic>> getDELETEResponse(String service, String url, Map<String, dynamic> body) async {
static Future<Map<String, dynamic>> getDELETEResponse(
String service,
String url,
Map<String, dynamic> body,
) async {
final token = await TokenService.getAccessTokenSafe();
return httpr.getDELETEResponse(service, url, body, authToken: token);
}
static Future<String> getFileUploadResponseAuth(String service, String url, String fileName, String fileType, String mediaType, List<int> bytes) async {
static Future<String> getFileUploadResponseAuth(
String service,
String url,
String fileName,
String fileType,
String mediaType,
List<int> bytes,
) async {
final token = await TokenService.getAccessTokenSafe();
final res = await httpr.getFileUploadResponse(service, url, fileName, fileType, mediaType, bytes, authToken: token);
final res = await httpr.getFileUploadResponse(
service,
url,
fileName,
fileType,
mediaType,
bytes,
authToken: token,
);
if (res == null) {
throw Exception('Upload failed');
}
return res.url;
}
static Future<bool> isAuthorizationStored() async => AuthorizationStorage.isAuthorizationStored();
static Future<bool> isAuthorizationStored() async =>
AuthorizationStorage.isAuthorizationStored();
/// Execute an operation with automatic token management and retry logic
static Future<T> executeWithAuth<T>(
Future<T> Function() operation,
String description, {
int? maxRetries,
}) async => AuthCircuitBreaker.execute(() async => RetryHelper.withExponentialBackoff(
operation,
maxRetries: maxRetries ?? 3,
initialDelay: Duration(milliseconds: 100),
maxDelay: Duration(seconds: 5),
shouldRetry: (error) => RetryHelper.isRetryableError(error),
));
}) async => AuthCircuitBreaker.execute(
() async => RetryHelper.withExponentialBackoff(
operation,
maxRetries: maxRetries ?? 3,
initialDelay: Duration(milliseconds: 100),
maxDelay: Duration(seconds: 5),
shouldRetry: (error) => RetryHelper.isRetryableError(error),
),
);
/// Handle 401 unauthorized errors with automatic token recovery
static Future<T> handleUnauthorized<T>(
Future<T> Function() operation,
String description,
) async {
_logger.warning('Handling unauthorized error with token recovery: $description');
return executeWithAuth(
() async {
try {
// Attempt token recovery first
await TokenService.handleUnauthorized();
// Retry the original operation
return await operation();
} catch (e) {
_logger.severe('Token recovery failed', e);
throw AuthenticationFailedException('Token recovery failed', toException(e));
}
},
'unauthorized recovery: $description',
_logger.warning(
'Handling unauthorized error with token recovery: $description',
);
return executeWithAuth(() async {
try {
// Attempt token recovery first
await TokenService.handleUnauthorized();
// Retry the original operation
return await operation();
} catch (e) {
_logger.severe('Token recovery failed', e);
throw AuthenticationFailedException(
'Token recovery failed',
toException(e),
);
}
}, 'unauthorized recovery: $description');
}
}

View File

@@ -2,10 +2,9 @@ import 'package:uuid/uuid.dart';
import 'package:logging/logging.dart';
import 'package:pshared/config/web.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/service/secure_storage.dart';
class DeviceIdManager {
static final _logger = Logger('service.device_id');
@@ -15,7 +14,7 @@ class DeviceIdManager {
if (deviceId == null) {
_logger.fine('Device id is not set, generating new');
deviceId = (const Uuid()).v4();
deviceId = (const Uuid()).v4();
await SecureStorageService.set(_key, deviceId);
}

View File

@@ -12,7 +12,6 @@ import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/services.dart';
import 'package:pshared/utils/http/params.dart';
class PaymentService {
static final _logger = Logger('service.payment');
static const String _objectType = Services.payments;
@@ -21,17 +20,21 @@ class PaymentService {
String organizationRef, {
int? limit,
String? cursor,
String? sourceRef,
String? destinationRef,
String? quotationRef,
DateTime? createdFrom,
DateTime? createdTo,
List<String>? states,
}) async {
_logger.fine('Listing payments for organization $organizationRef');
final queryParams = <String, String>{};
if (sourceRef != null && sourceRef.isNotEmpty) {
queryParams['source_ref'] = sourceRef;
if (quotationRef != null && quotationRef.isNotEmpty) {
queryParams['quotation_ref'] = quotationRef;
}
if (destinationRef != null && destinationRef.isNotEmpty) {
queryParams['destination_ref'] = destinationRef;
if (createdFrom != null) {
queryParams['created_from'] = createdFrom.toUtc().toIso8601String();
}
if (createdTo != null) {
queryParams['created_to'] = createdTo.toUtc().toIso8601String();
}
if (states != null && states.isNotEmpty) {
queryParams['state'] = states.join(',');
@@ -43,9 +46,14 @@ class PaymentService {
cursor: cursor,
queryParams: queryParams,
);
final response = await AuthorizationService.getGETResponse(_objectType, url);
final response = await AuthorizationService.getGETResponse(
_objectType,
url,
);
final parsed = PaymentsResponse.fromJson(response);
final payments = parsed.payments.map((payment) => payment.toDomain()).toList();
final payments = parsed.payments
.map((payment) => payment.toDomain())
.toList();
return PaymentPage(items: payments, nextCursor: parsed.nextCursor);
}
@@ -53,16 +61,18 @@ class PaymentService {
String organizationRef, {
int? limit,
String? cursor,
String? sourceRef,
String? destinationRef,
String? quotationRef,
DateTime? createdFrom,
DateTime? createdTo,
List<String>? states,
}) async {
final page = await listPage(
organizationRef,
limit: limit,
cursor: cursor,
sourceRef: sourceRef,
destinationRef: destinationRef,
quotationRef: quotationRef,
createdFrom: createdFrom,
createdTo: createdTo,
states: states,
);
return page.items;
@@ -74,7 +84,9 @@ class PaymentService {
String? idempotencyKey,
Map<String, String>? metadata,
}) async {
_logger.fine('Executing payment for quotation $quotationRef in $organizationRef');
_logger.fine(
'Executing payment for quotation $quotationRef in $organizationRef',
);
final request = InitiatePaymentRequest(
idempotencyKey: idempotencyKey ?? Uuid().v4(),
quoteRef: quotationRef,
@@ -87,5 +99,4 @@ class PaymentService {
);
return PaymentResponse.fromJson(response).payment.toDomain();
}
}