This commit is contained in:
Arseni
2026-03-04 17:43:18 +03:00
parent 80b25a8608
commit aff804ec58
46 changed files with 1090 additions and 345 deletions

View File

@@ -0,0 +1,56 @@
class PaymentOperationDTO {
final String? stepRef;
final String? operationRef;
final String? code;
final String? state;
final String? label;
final String? failureCode;
final String? failureReason;
final String? startedAt;
final String? completedAt;
const PaymentOperationDTO({
this.stepRef,
this.operationRef,
this.code,
this.state,
this.label,
this.failureCode,
this.failureReason,
this.startedAt,
this.completedAt,
});
factory PaymentOperationDTO.fromJson(Map<String, dynamic> json) =>
PaymentOperationDTO(
stepRef: _asString(json['stepRef'] ?? json['step_ref']),
operationRef: _asString(json['operationRef'] ?? json['operation_ref']),
code: _asString(json['code']),
state: _asString(json['state']),
label: _asString(json['label']),
failureCode: _asString(json['failureCode'] ?? json['failure_code']),
failureReason: _asString(
json['failureReason'] ?? json['failure_reason'],
),
startedAt: _asString(json['startedAt'] ?? json['started_at']),
completedAt: _asString(json['completedAt'] ?? json['completed_at']),
);
Map<String, dynamic> toJson() => <String, dynamic>{
'stepRef': stepRef,
'operationRef': operationRef,
'code': code,
'state': state,
'label': label,
'failureCode': failureCode,
'failureReason': failureReason,
'startedAt': startedAt,
'completedAt': completedAt,
};
}
String? _asString(Object? value) {
final text = value?.toString().trim();
if (text == null || text.isEmpty) return null;
return text;
}

View File

@@ -1,5 +1,6 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/payment/operation.dart';
import 'package:pshared/data/dto/payment/payment_quote.dart';
part 'payment.g.dart';
@@ -12,6 +13,8 @@ class PaymentDTO {
final String? state;
final String? failureCode;
final String? failureReason;
@JsonKey(defaultValue: <PaymentOperationDTO>[])
final List<PaymentOperationDTO> operations;
final PaymentQuoteDTO? lastQuote;
final Map<String, String>? metadata;
final String? createdAt;
@@ -22,6 +25,7 @@ class PaymentDTO {
this.state,
this.failureCode,
this.failureReason,
this.operations = const <PaymentOperationDTO>[],
this.lastQuote,
this.metadata,
this.createdAt,

View File

@@ -0,0 +1,37 @@
import 'package:pshared/data/dto/payment/operation.dart';
import 'package:pshared/models/payment/execution_operation.dart';
extension PaymentOperationDTOMapper on PaymentOperationDTO {
PaymentExecutionOperation toDomain() => PaymentExecutionOperation(
stepRef: stepRef,
operationRef: operationRef,
code: code,
state: state,
label: label,
failureCode: failureCode,
failureReason: failureReason,
startedAt: _parseDateTime(startedAt),
completedAt: _parseDateTime(completedAt),
);
}
extension PaymentExecutionOperationMapper on PaymentExecutionOperation {
PaymentOperationDTO toDTO() => PaymentOperationDTO(
stepRef: stepRef,
operationRef: operationRef,
code: code,
state: state,
label: label,
failureCode: failureCode,
failureReason: failureReason,
startedAt: startedAt?.toUtc().toIso8601String(),
completedAt: completedAt?.toUtc().toIso8601String(),
);
}
DateTime? _parseDateTime(String? value) {
final normalized = value?.trim();
if (normalized == null || normalized.isEmpty) return null;
return DateTime.tryParse(normalized);
}

View File

@@ -1,8 +1,10 @@
import 'package:pshared/data/dto/payment/payment.dart';
import 'package:pshared/data/mapper/payment/operation.dart';
import 'package:pshared/data/mapper/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,
@@ -11,6 +13,7 @@ extension PaymentDTOMapper on PaymentDTO {
orchestrationState: paymentOrchestrationStateFromValue(state),
failureCode: failureCode,
failureReason: failureReason,
operations: operations.map((item) => item.toDomain()).toList(),
lastQuote: lastQuote?.toDomain(),
metadata: metadata,
createdAt: createdAt == null ? null : DateTime.tryParse(createdAt!),
@@ -24,6 +27,7 @@ extension PaymentMapper on Payment {
state: state ?? paymentOrchestrationStateToValue(orchestrationState),
failureCode: failureCode,
failureReason: failureReason,
operations: operations.map((item) => item.toDTO()).toList(),
lastQuote: lastQuote?.toDTO(),
metadata: metadata,
createdAt: createdAt?.toUtc().toIso8601String(),

View File

@@ -10,11 +10,31 @@
"@operationStatusProcessing": {
"description": "Label for the “processing” operation status"
},
"operationStatusPending": "Pending",
"@operationStatusPending": {
"description": "Label for the “pending” operation status"
},
"operationStatusRetrying": "Retrying",
"@operationStatusRetrying": {
"description": "Label for the “retrying” operation status"
},
"operationStatusSuccess": "Success",
"@operationStatusSuccess": {
"description": "Label for the “success” operation status"
},
"operationStatusSkipped": "Skipped",
"@operationStatusSkipped": {
"description": "Label for the “skipped” operation status"
},
"operationStatusCancelled": "Cancelled",
"@operationStatusCancelled": {
"description": "Label for the “cancelled” operation status"
},
"operationStatusNeedsAttention": "Needs attention",
"@operationStatusNeedsAttention": {
"description": "Label for the “needs attention” operation status"
},
"operationStatusError": "Error",
"@operationStatusError": {

View File

@@ -10,11 +10,31 @@
"@operationStatusProcessing": {
"description": "Label for the “processing” operation status"
},
"operationStatusPending": "В ожидании",
"@operationStatusPending": {
"description": "Label for the “pending” operation status"
},
"operationStatusRetrying": "Повтор",
"@operationStatusRetrying": {
"description": "Label for the “retrying” operation status"
},
"operationStatusSuccess": "Успех",
"@operationStatusSuccess": {
"description": "Label for the “success” operation status"
},
"operationStatusSkipped": "Пропущен",
"@operationStatusSkipped": {
"description": "Label for the “skipped” operation status"
},
"operationStatusCancelled": "Отменен",
"@operationStatusCancelled": {
"description": "Label for the “cancelled” operation status"
},
"operationStatusNeedsAttention": "Требует внимания",
"@operationStatusNeedsAttention": {
"description": "Label for the “needs attention” operation status"
},
"operationStatusError": "Ошибка",
"@operationStatusError": {

View File

@@ -0,0 +1,23 @@
class PaymentExecutionOperation {
final String? stepRef;
final String? operationRef;
final String? code;
final String? state;
final String? label;
final String? failureCode;
final String? failureReason;
final DateTime? startedAt;
final DateTime? completedAt;
const PaymentExecutionOperation({
required this.stepRef,
required this.operationRef,
required this.code,
required this.state,
required this.label,
required this.failureCode,
required this.failureReason,
required this.startedAt,
required this.completedAt,
});
}

View File

@@ -1,3 +1,4 @@
import 'package:pshared/models/payment/execution_operation.dart';
import 'package:pshared/models/payment/quote/quote.dart';
import 'package:pshared/models/payment/state.dart';
@@ -8,6 +9,7 @@ class Payment {
final PaymentOrchestrationState orchestrationState;
final String? failureCode;
final String? failureReason;
final List<PaymentExecutionOperation> operations;
final PaymentQuote? lastQuote;
final Map<String, String>? metadata;
final DateTime? createdAt;
@@ -19,6 +21,7 @@ class Payment {
required this.orchestrationState,
required this.failureCode,
required this.failureReason,
required this.operations,
required this.lastQuote,
required this.metadata,
required this.createdAt,

View File

@@ -2,24 +2,35 @@ import 'package:flutter/widgets.dart';
import 'package:pshared/generated/i18n/ps_localizations.dart';
enum OperationStatus {
pending,
processing,
retrying,
success,
skipped,
cancelled,
needsAttention,
error,
}
extension OperationStatusX on OperationStatus {
/// Returns the localized string for this status,
/// e.g. “Processing”, “Success”, “Error”.
String localized(BuildContext context) {
final loc = PSLocalizations.of(context)!;
switch (this) {
case OperationStatus.pending:
return loc.operationStatusPending;
case OperationStatus.processing:
return loc.operationStatusProcessing;
case OperationStatus.retrying:
return loc.operationStatusRetrying;
case OperationStatus.success:
return loc.operationStatusSuccess;
case OperationStatus.skipped:
return loc.operationStatusSkipped;
case OperationStatus.cancelled:
return loc.operationStatusCancelled;
case OperationStatus.needsAttention:
return loc.operationStatusNeedsAttention;
case OperationStatus.error:
return loc.operationStatusError;
}

View File

@@ -4,17 +4,30 @@ import 'package:pshared/models/file/downloaded_file.dart';
import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/services.dart';
class PaymentDocumentsService {
static final _logger = Logger('service.payment_documents');
static const String _objectType = Services.payments;
static Future<DownloadedFile> getAct(String organizationRef, String paymentRef) async {
final encodedRef = Uri.encodeQueryComponent(paymentRef);
final url = '/documents/act/$organizationRef?payment_ref=$encodedRef';
static Future<DownloadedFile> getAct(
String organizationRef,
String paymentRef, {
String? operationRef,
}) async {
final query = <String, String>{'payment_ref': paymentRef};
final operationRefValue = operationRef;
if (operationRefValue != null && operationRefValue.isNotEmpty) {
query['operation_ref'] = operationRefValue;
query['operationRef'] = operationRefValue;
}
final queryString = Uri(queryParameters: query).query;
final url = '/documents/act/$organizationRef?$queryString';
_logger.fine('Downloading act document for payment $paymentRef');
final response = await AuthorizationService.getGETBinaryResponse(_objectType, url);
final filename = _filenameFromDisposition(response.header('content-disposition')) ??
final response = await AuthorizationService.getGETBinaryResponse(
_objectType,
url,
);
final filename =
_filenameFromDisposition(response.header('content-disposition')) ??
'act_$paymentRef.pdf';
final mimeType = response.header('content-type') ?? 'application/pdf';
return DownloadedFile(

View File

@@ -69,6 +69,7 @@ void main() {
orchestrationState: PaymentOrchestrationState.created,
failureCode: null,
failureReason: null,
operations: [],
lastQuote: null,
metadata: null,
createdAt: null,
@@ -80,6 +81,7 @@ void main() {
orchestrationState: PaymentOrchestrationState.settled,
failureCode: null,
failureReason: null,
operations: [],
lastQuote: null,
metadata: null,
createdAt: null,
@@ -99,6 +101,7 @@ void main() {
orchestrationState: PaymentOrchestrationState.executing,
failureCode: 'failure_ledger',
failureReason: 'ledger failed',
operations: [],
lastQuote: null,
metadata: null,
createdAt: null,
@@ -110,6 +113,7 @@ void main() {
orchestrationState: PaymentOrchestrationState.failed,
failureCode: null,
failureReason: null,
operations: [],
lastQuote: null,
metadata: null,
createdAt: null,