payment quotation v2 + payment orchestration v2 draft
This commit is contained in:
@@ -5,7 +5,6 @@ import 'package:pshared/models/payment/kind.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/payment/settlement_mode.dart';
|
||||
|
||||
|
||||
PaymentKind paymentKindFromValue(String? value) {
|
||||
switch (value) {
|
||||
case 'payout':
|
||||
@@ -166,6 +165,7 @@ PaymentType endpointTypeFromValue(String? value) {
|
||||
case 'managedWallet':
|
||||
case 'managed_wallet':
|
||||
return PaymentType.managedWallet;
|
||||
case 'cryptoAddress':
|
||||
case 'externalChain':
|
||||
case 'external_chain':
|
||||
return PaymentType.externalChain;
|
||||
@@ -193,15 +193,15 @@ String endpointTypeToValue(PaymentType type) {
|
||||
case PaymentType.ledger:
|
||||
return 'ledger';
|
||||
case PaymentType.managedWallet:
|
||||
return 'managed_wallet';
|
||||
return 'managedWallet';
|
||||
case PaymentType.externalChain:
|
||||
return 'external_chain';
|
||||
return 'cryptoAddress';
|
||||
case PaymentType.card:
|
||||
return 'card';
|
||||
case PaymentType.cardToken:
|
||||
return 'card';
|
||||
return 'cardToken';
|
||||
case PaymentType.bankAccount:
|
||||
return 'bank_account';
|
||||
return 'bankAccount';
|
||||
case PaymentType.iban:
|
||||
return 'iban';
|
||||
case PaymentType.wallet:
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
import 'package:pshared/data/dto/payment/payment.dart';
|
||||
import 'package:pshared/data/mapper/payment/payment_quote.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
|
||||
import 'package:pshared/models/payment/state.dart';
|
||||
|
||||
extension PaymentDTOMapper on PaymentDTO {
|
||||
Payment toDomain() => Payment(
|
||||
paymentRef: paymentRef,
|
||||
idempotencyKey: idempotencyKey,
|
||||
state: state,
|
||||
failureCode: failureCode,
|
||||
failureReason: failureReason,
|
||||
lastQuote: lastQuote?.toDomain(),
|
||||
metadata: metadata,
|
||||
createdAt: createdAt == null ? null : DateTime.tryParse(createdAt!),
|
||||
);
|
||||
paymentRef: paymentRef,
|
||||
idempotencyKey: idempotencyKey,
|
||||
state: state,
|
||||
orchestrationState: paymentOrchestrationStateFromValue(state),
|
||||
failureCode: failureCode,
|
||||
failureReason: failureReason,
|
||||
lastQuote: lastQuote?.toDomain(),
|
||||
metadata: metadata,
|
||||
createdAt: createdAt == null ? null : DateTime.tryParse(createdAt!),
|
||||
);
|
||||
}
|
||||
|
||||
extension PaymentMapper on Payment {
|
||||
PaymentDTO toDTO() => PaymentDTO(
|
||||
paymentRef: paymentRef,
|
||||
idempotencyKey: idempotencyKey,
|
||||
state: state,
|
||||
failureCode: failureCode,
|
||||
failureReason: failureReason,
|
||||
lastQuote: lastQuote?.toDTO(),
|
||||
metadata: metadata,
|
||||
createdAt: createdAt?.toUtc().toIso8601String(),
|
||||
);
|
||||
paymentRef: paymentRef,
|
||||
idempotencyKey: idempotencyKey,
|
||||
state: state ?? paymentOrchestrationStateToValue(orchestrationState),
|
||||
failureCode: failureCode,
|
||||
failureReason: failureReason,
|
||||
lastQuote: lastQuote?.toDTO(),
|
||||
metadata: metadata,
|
||||
createdAt: createdAt?.toUtc().toIso8601String(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'package:pshared/models/payment/quote/quote.dart';
|
||||
|
||||
import 'package:pshared/models/payment/state.dart';
|
||||
|
||||
class Payment {
|
||||
final String? paymentRef;
|
||||
final String? idempotencyKey;
|
||||
final String? state;
|
||||
final PaymentOrchestrationState orchestrationState;
|
||||
final String? failureCode;
|
||||
final String? failureReason;
|
||||
final PaymentQuote? lastQuote;
|
||||
@@ -15,6 +16,7 @@ class Payment {
|
||||
required this.paymentRef,
|
||||
required this.idempotencyKey,
|
||||
required this.state,
|
||||
required this.orchestrationState,
|
||||
required this.failureCode,
|
||||
required this.failureReason,
|
||||
required this.lastQuote,
|
||||
@@ -22,9 +24,12 @@ class Payment {
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
bool get isPending => orchestrationState.isPending;
|
||||
|
||||
bool get isTerminal => orchestrationState.isTerminal;
|
||||
|
||||
bool get isFailure {
|
||||
if ((failureCode ?? '').trim().isNotEmpty) return true;
|
||||
final normalized = (state ?? '').trim().toLowerCase();
|
||||
return normalized.contains('fail') || normalized.contains('cancel');
|
||||
return orchestrationState == PaymentOrchestrationState.failed;
|
||||
}
|
||||
}
|
||||
|
||||
81
frontend/pshared/lib/models/payment/state.dart
Normal file
81
frontend/pshared/lib/models/payment/state.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
enum PaymentOrchestrationState {
|
||||
created,
|
||||
executing,
|
||||
needsAttention,
|
||||
settled,
|
||||
failed,
|
||||
unspecified,
|
||||
}
|
||||
|
||||
PaymentOrchestrationState paymentOrchestrationStateFromValue(String? value) {
|
||||
final normalized = _normalizePaymentState(value);
|
||||
switch (normalized) {
|
||||
case 'CREATED':
|
||||
case 'ACCEPTED':
|
||||
return PaymentOrchestrationState.created;
|
||||
case 'EXECUTING':
|
||||
case 'PROCESSING':
|
||||
case 'FUNDS_RESERVED':
|
||||
case 'SUBMITTED':
|
||||
return PaymentOrchestrationState.executing;
|
||||
case 'NEEDS_ATTENTION':
|
||||
return PaymentOrchestrationState.needsAttention;
|
||||
case 'SETTLED':
|
||||
case 'SUCCESS':
|
||||
return PaymentOrchestrationState.settled;
|
||||
case 'FAILED':
|
||||
case 'CANCELLED':
|
||||
return PaymentOrchestrationState.failed;
|
||||
default:
|
||||
return PaymentOrchestrationState.unspecified;
|
||||
}
|
||||
}
|
||||
|
||||
String paymentOrchestrationStateToValue(PaymentOrchestrationState state) {
|
||||
switch (state) {
|
||||
case PaymentOrchestrationState.created:
|
||||
return 'orchestration_state_created';
|
||||
case PaymentOrchestrationState.executing:
|
||||
return 'orchestration_state_executing';
|
||||
case PaymentOrchestrationState.needsAttention:
|
||||
return 'orchestration_state_needs_attention';
|
||||
case PaymentOrchestrationState.settled:
|
||||
return 'orchestration_state_settled';
|
||||
case PaymentOrchestrationState.failed:
|
||||
return 'orchestration_state_failed';
|
||||
case PaymentOrchestrationState.unspecified:
|
||||
return 'orchestration_state_unspecified';
|
||||
}
|
||||
}
|
||||
|
||||
extension PaymentOrchestrationStateX on PaymentOrchestrationState {
|
||||
bool get isTerminal {
|
||||
switch (this) {
|
||||
case PaymentOrchestrationState.settled:
|
||||
case PaymentOrchestrationState.failed:
|
||||
return true;
|
||||
case PaymentOrchestrationState.created:
|
||||
case PaymentOrchestrationState.executing:
|
||||
case PaymentOrchestrationState.needsAttention:
|
||||
case PaymentOrchestrationState.unspecified:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isPending => !isTerminal;
|
||||
}
|
||||
|
||||
String _normalizePaymentState(String? value) {
|
||||
final trimmed = (value ?? '').trim().toUpperCase();
|
||||
if (trimmed.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('ORCHESTRATION_STATE_')) {
|
||||
return trimmed.substring('ORCHESTRATION_STATE_'.length);
|
||||
}
|
||||
if (trimmed.startsWith('PAYMENT_STATE_')) {
|
||||
return trimmed.substring('PAYMENT_STATE_'.length);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/config/web.dart';
|
||||
|
||||
import 'package:pshared/config/constants.dart';
|
||||
|
||||
abstract class Storable {
|
||||
String get id;
|
||||
DateTime get createdAt;
|
||||
DateTime get updatedAt;
|
||||
DateTime get updatedAt;
|
||||
}
|
||||
|
||||
@immutable
|
||||
@@ -23,11 +22,11 @@ class _StorableImp implements Storable {
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Storable newStorable({String? id, DateTime? createdAt, DateTime? updatedAt}) => _StorableImp(
|
||||
id: id ?? Constants.nilObjectRef,
|
||||
createdAt: createdAt ?? DateTime.now().toUtc(),
|
||||
updatedAt: updatedAt ?? DateTime.now().toUtc(),
|
||||
);
|
||||
Storable newStorable({String? id, DateTime? createdAt, DateTime? updatedAt}) =>
|
||||
_StorableImp(
|
||||
id: id ?? Constants.nilObjectRef,
|
||||
createdAt: createdAt ?? DateTime.now().toUtc(),
|
||||
updatedAt: updatedAt ?? DateTime.now().toUtc(),
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/service/payment/service.dart';
|
||||
import 'package:pshared/utils/exception.dart';
|
||||
|
||||
|
||||
class PaymentsProvider with ChangeNotifier {
|
||||
static const Duration _pendingRefreshInterval = Duration(seconds: 10);
|
||||
|
||||
@@ -20,8 +19,9 @@ class PaymentsProvider with ChangeNotifier {
|
||||
bool _isLoadingMore = false;
|
||||
String? _nextCursor;
|
||||
int? _limit;
|
||||
String? _sourceRef;
|
||||
String? _destinationRef;
|
||||
String? _quotationRef;
|
||||
DateTime? _createdFrom;
|
||||
DateTime? _createdTo;
|
||||
List<String>? _states;
|
||||
|
||||
int _opSeq = 0;
|
||||
@@ -32,7 +32,8 @@ class PaymentsProvider with ChangeNotifier {
|
||||
List<Payment> get payments => _resource.data ?? [];
|
||||
bool get isLoading => _resource.isLoading;
|
||||
Exception? get error => _resource.error;
|
||||
bool get isReady => _isLoaded && !_resource.isLoading && _resource.error == null;
|
||||
bool get isReady =>
|
||||
_isLoaded && !_resource.isLoading && _resource.error == null;
|
||||
|
||||
bool get isLoadingMore => _isLoadingMore;
|
||||
String? get nextCursor => _nextCursor;
|
||||
@@ -54,14 +55,16 @@ class PaymentsProvider with ChangeNotifier {
|
||||
|
||||
Future<void> refresh({
|
||||
int? limit,
|
||||
String? sourceRef,
|
||||
String? destinationRef,
|
||||
String? quotationRef,
|
||||
DateTime? createdFrom,
|
||||
DateTime? createdTo,
|
||||
List<String>? states,
|
||||
}) async {
|
||||
await _refresh(
|
||||
limit: limit,
|
||||
sourceRef: sourceRef,
|
||||
destinationRef: destinationRef,
|
||||
quotationRef: quotationRef,
|
||||
createdFrom: createdFrom,
|
||||
createdTo: createdTo,
|
||||
states: states,
|
||||
showLoading: true,
|
||||
updateError: true,
|
||||
@@ -70,14 +73,16 @@ class PaymentsProvider with ChangeNotifier {
|
||||
|
||||
Future<void> refreshSilently({
|
||||
int? limit,
|
||||
String? sourceRef,
|
||||
String? destinationRef,
|
||||
String? quotationRef,
|
||||
DateTime? createdFrom,
|
||||
DateTime? createdTo,
|
||||
List<String>? states,
|
||||
}) async {
|
||||
await _refresh(
|
||||
limit: limit,
|
||||
sourceRef: sourceRef,
|
||||
destinationRef: destinationRef,
|
||||
quotationRef: quotationRef,
|
||||
createdFrom: createdFrom,
|
||||
createdTo: createdTo,
|
||||
states: states,
|
||||
showLoading: false,
|
||||
updateError: false,
|
||||
@@ -87,10 +92,7 @@ class PaymentsProvider with ChangeNotifier {
|
||||
void mergePayments(List<Payment> incoming) {
|
||||
if (incoming.isEmpty) return;
|
||||
final existing = List<Payment>.from(_resource.data ?? const []);
|
||||
final combined = <Payment>[
|
||||
...incoming,
|
||||
...existing,
|
||||
];
|
||||
final combined = <Payment>[...incoming, ...existing];
|
||||
final seen = <String>{};
|
||||
final merged = <Payment>[];
|
||||
|
||||
@@ -110,8 +112,9 @@ class PaymentsProvider with ChangeNotifier {
|
||||
|
||||
Future<void> _refresh({
|
||||
int? limit,
|
||||
String? sourceRef,
|
||||
String? destinationRef,
|
||||
String? quotationRef,
|
||||
DateTime? createdFrom,
|
||||
DateTime? createdTo,
|
||||
List<String>? states,
|
||||
required bool showLoading,
|
||||
required bool updateError,
|
||||
@@ -120,8 +123,9 @@ class PaymentsProvider with ChangeNotifier {
|
||||
if (org == null || !org.isOrganizationSet) return;
|
||||
|
||||
_limit = limit;
|
||||
_sourceRef = _normalize(sourceRef);
|
||||
_destinationRef = _normalize(destinationRef);
|
||||
_quotationRef = _normalize(quotationRef);
|
||||
_createdFrom = createdFrom?.toUtc();
|
||||
_createdTo = createdTo?.toUtc();
|
||||
_states = _normalizeStates(states);
|
||||
_nextCursor = null;
|
||||
_isLoadingMore = false;
|
||||
@@ -129,7 +133,10 @@ class PaymentsProvider with ChangeNotifier {
|
||||
final seq = ++_opSeq;
|
||||
|
||||
if (showLoading) {
|
||||
_applyResource(_resource.copyWith(isLoading: true, error: null), notify: true);
|
||||
_applyResource(
|
||||
_resource.copyWith(isLoading: true, error: null),
|
||||
notify: true,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -137,8 +144,9 @@ class PaymentsProvider with ChangeNotifier {
|
||||
org.current.id,
|
||||
limit: _limit,
|
||||
cursor: null,
|
||||
sourceRef: _sourceRef,
|
||||
destinationRef: _destinationRef,
|
||||
quotationRef: _quotationRef,
|
||||
createdFrom: _createdFrom,
|
||||
createdTo: _createdTo,
|
||||
states: _states,
|
||||
);
|
||||
|
||||
@@ -147,11 +155,7 @@ class PaymentsProvider with ChangeNotifier {
|
||||
_isLoaded = true;
|
||||
_nextCursor = _normalize(page.nextCursor);
|
||||
_applyResource(
|
||||
Resource(
|
||||
data: page.items,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
),
|
||||
Resource(data: page.items, isLoading: false, error: null),
|
||||
notify: true,
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -162,10 +166,7 @@ class PaymentsProvider with ChangeNotifier {
|
||||
notify: true,
|
||||
);
|
||||
} else if (showLoading) {
|
||||
_applyResource(
|
||||
_resource.copyWith(isLoading: false),
|
||||
notify: true,
|
||||
);
|
||||
_applyResource(_resource.copyWith(isLoading: false), notify: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,8 +190,9 @@ class PaymentsProvider with ChangeNotifier {
|
||||
org.current.id,
|
||||
limit: _limit,
|
||||
cursor: cursor,
|
||||
sourceRef: _sourceRef,
|
||||
destinationRef: _destinationRef,
|
||||
quotationRef: _quotationRef,
|
||||
createdFrom: _createdFrom,
|
||||
createdTo: _createdTo,
|
||||
states: _states,
|
||||
);
|
||||
|
||||
@@ -206,10 +208,7 @@ class PaymentsProvider with ChangeNotifier {
|
||||
} catch (e) {
|
||||
if (seq != _opSeq) return;
|
||||
|
||||
_applyResource(
|
||||
_resource.copyWith(error: toException(e)),
|
||||
notify: false,
|
||||
);
|
||||
_applyResource(_resource.copyWith(error: toException(e)), notify: false);
|
||||
} finally {
|
||||
if (seq == _opSeq) {
|
||||
_isLoadingMore = false;
|
||||
@@ -224,15 +223,19 @@ class PaymentsProvider with ChangeNotifier {
|
||||
_isLoadingMore = false;
|
||||
_nextCursor = null;
|
||||
_limit = null;
|
||||
_sourceRef = null;
|
||||
_destinationRef = null;
|
||||
_quotationRef = null;
|
||||
_createdFrom = null;
|
||||
_createdTo = null;
|
||||
_states = null;
|
||||
_resource = Resource(data: []);
|
||||
_stopPendingRefreshTimer();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _applyResource(Resource<List<Payment>> newResource, {required bool notify}) {
|
||||
void _applyResource(
|
||||
Resource<List<Payment>> newResource, {
|
||||
required bool notify,
|
||||
}) {
|
||||
_resource = newResource;
|
||||
_syncPendingRefresh();
|
||||
if (notify) notifyListeners();
|
||||
@@ -253,15 +256,15 @@ class PaymentsProvider with ChangeNotifier {
|
||||
List<String>? _normalizeStates(List<String>? states) {
|
||||
if (states == null || states.isEmpty) return null;
|
||||
final normalized = states
|
||||
.map((state) => state.trim())
|
||||
.where((state) => state.isNotEmpty)
|
||||
.toList();
|
||||
.map((state) => state.trim())
|
||||
.where((state) => state.isNotEmpty)
|
||||
.toList();
|
||||
if (normalized.isEmpty) return null;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
void _syncPendingRefresh() {
|
||||
final hasPending = payments.any(_isPending);
|
||||
final hasPending = payments.any((payment) => payment.isPending);
|
||||
if (!hasPending) {
|
||||
_stopPendingRefreshTimer();
|
||||
return;
|
||||
@@ -286,8 +289,9 @@ class PaymentsProvider with ChangeNotifier {
|
||||
try {
|
||||
await refreshSilently(
|
||||
limit: _limit,
|
||||
sourceRef: _sourceRef,
|
||||
destinationRef: _destinationRef,
|
||||
quotationRef: _quotationRef,
|
||||
createdFrom: _createdFrom,
|
||||
createdTo: _createdTo,
|
||||
states: _states,
|
||||
);
|
||||
} finally {
|
||||
@@ -301,29 +305,9 @@ class PaymentsProvider with ChangeNotifier {
|
||||
_isPendingRefreshInFlight = false;
|
||||
}
|
||||
|
||||
bool _isPending(Payment payment) {
|
||||
final raw = payment.state;
|
||||
final trimmed = (raw ?? '').trim().toUpperCase();
|
||||
final normalized = trimmed.startsWith('PAYMENT_STATE_')
|
||||
? trimmed.substring('PAYMENT_STATE_'.length)
|
||||
: trimmed;
|
||||
|
||||
switch (normalized) {
|
||||
case 'SUCCESS':
|
||||
case 'FAILED':
|
||||
case 'CANCELLED':
|
||||
return false;
|
||||
case 'PROCESSING':
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopPendingRefreshTimer();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user