updated document upload according to fresh api

This commit is contained in:
Arseni
2026-03-04 18:07:08 +03:00
parent aff804ec58
commit c59538869b
12 changed files with 122 additions and 53 deletions

View File

@@ -1,6 +1,7 @@
class PaymentOperationDTO { class PaymentOperationDTO {
final String? stepRef; final String? stepRef;
final String? operationRef; final String? operationRef;
final String? gateway;
final String? code; final String? code;
final String? state; final String? state;
final String? label; final String? label;
@@ -12,6 +13,7 @@ class PaymentOperationDTO {
const PaymentOperationDTO({ const PaymentOperationDTO({
this.stepRef, this.stepRef,
this.operationRef, this.operationRef,
this.gateway,
this.code, this.code,
this.state, this.state,
this.label, this.label,
@@ -25,6 +27,7 @@ class PaymentOperationDTO {
PaymentOperationDTO( PaymentOperationDTO(
stepRef: _asString(json['stepRef'] ?? json['step_ref']), stepRef: _asString(json['stepRef'] ?? json['step_ref']),
operationRef: _asString(json['operationRef'] ?? json['operation_ref']), operationRef: _asString(json['operationRef'] ?? json['operation_ref']),
gateway: _asString(json['gateway']),
code: _asString(json['code']), code: _asString(json['code']),
state: _asString(json['state']), state: _asString(json['state']),
label: _asString(json['label']), label: _asString(json['label']),
@@ -39,6 +42,7 @@ class PaymentOperationDTO {
Map<String, dynamic> toJson() => <String, dynamic>{ Map<String, dynamic> toJson() => <String, dynamic>{
'stepRef': stepRef, 'stepRef': stepRef,
'operationRef': operationRef, 'operationRef': operationRef,
'gateway': gateway,
'code': code, 'code': code,
'state': state, 'state': state,
'label': label, 'label': label,

View File

@@ -1,11 +1,11 @@
import 'package:pshared/data/dto/payment/operation.dart'; import 'package:pshared/data/dto/payment/operation.dart';
import 'package:pshared/models/payment/execution_operation.dart'; import 'package:pshared/models/payment/execution_operation.dart';
extension PaymentOperationDTOMapper on PaymentOperationDTO { extension PaymentOperationDTOMapper on PaymentOperationDTO {
PaymentExecutionOperation toDomain() => PaymentExecutionOperation( PaymentExecutionOperation toDomain() => PaymentExecutionOperation(
stepRef: stepRef, stepRef: stepRef,
operationRef: operationRef, operationRef: operationRef,
gateway: gateway,
code: code, code: code,
state: state, state: state,
label: label, label: label,
@@ -20,6 +20,7 @@ extension PaymentExecutionOperationMapper on PaymentExecutionOperation {
PaymentOperationDTO toDTO() => PaymentOperationDTO( PaymentOperationDTO toDTO() => PaymentOperationDTO(
stepRef: stepRef, stepRef: stepRef,
operationRef: operationRef, operationRef: operationRef,
gateway: gateway,
code: code, code: code,
state: state, state: state,
label: label, label: label,

View File

@@ -1,6 +1,7 @@
class PaymentExecutionOperation { class PaymentExecutionOperation {
final String? stepRef; final String? stepRef;
final String? operationRef; final String? operationRef;
final String? gateway;
final String? code; final String? code;
final String? state; final String? state;
final String? label; final String? label;
@@ -12,6 +13,7 @@ class PaymentExecutionOperation {
const PaymentExecutionOperation({ const PaymentExecutionOperation({
required this.stepRef, required this.stepRef,
required this.operationRef, required this.operationRef,
required this.gateway,
required this.code, required this.code,
required this.state, required this.state,
required this.label, required this.label,

View File

@@ -1,7 +1,6 @@
import 'package:pshared/models/payment/methods/type.dart'; import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/models/payment/status.dart'; import 'package:pshared/models/payment/status.dart';
class OperationItem { class OperationItem {
final OperationStatus status; final OperationStatus status;
final String? fileName; final String? fileName;
@@ -11,6 +10,8 @@ class OperationItem {
final String toCurrency; final String toCurrency;
final String payId; final String payId;
final String? paymentRef; final String? paymentRef;
final String? operationRef;
final String? gatewayService;
final String? cardNumber; final String? cardNumber;
final PaymentMethod? paymentMethod; final PaymentMethod? paymentMethod;
final String name; final String name;
@@ -26,6 +27,8 @@ class OperationItem {
required this.toCurrency, required this.toCurrency,
required this.payId, required this.payId,
this.paymentRef, this.paymentRef,
this.operationRef,
this.gatewayService,
this.cardNumber, this.cardNumber,
this.paymentMethod, this.paymentMethod,
required this.name, required this.name,

View File

@@ -8,27 +8,27 @@ class PaymentDocumentsService {
static final _logger = Logger('service.payment_documents'); static final _logger = Logger('service.payment_documents');
static const String _objectType = Services.payments; static const String _objectType = Services.payments;
static Future<DownloadedFile> getAct( static Future<DownloadedFile> getOperationDocument(
String organizationRef, String organizationRef,
String paymentRef, { String gatewayService,
String? operationRef, String operationRef,
}) async { ) async {
final query = <String, String>{'payment_ref': paymentRef}; final query = <String, String>{
final operationRefValue = operationRef; 'gateway_service': gatewayService,
if (operationRefValue != null && operationRefValue.isNotEmpty) { 'operation_ref': operationRef,
query['operation_ref'] = operationRefValue; };
query['operationRef'] = operationRefValue;
}
final queryString = Uri(queryParameters: query).query; final queryString = Uri(queryParameters: query).query;
final url = '/documents/act/$organizationRef?$queryString'; final url = '/documents/operation/$organizationRef?$queryString';
_logger.fine('Downloading act document for payment $paymentRef'); _logger.fine(
'Downloading operation document for operation $operationRef in gateway $gatewayService',
);
final response = await AuthorizationService.getGETBinaryResponse( final response = await AuthorizationService.getGETBinaryResponse(
_objectType, _objectType,
url, url,
); );
final filename = final filename =
_filenameFromDisposition(response.header('content-disposition')) ?? _filenameFromDisposition(response.header('content-disposition')) ??
'act_$paymentRef.pdf'; 'operation_$operationRef.pdf';
final mimeType = response.header('content-type') ?? 'application/pdf'; final mimeType = response.header('content-type') ?? 'application/pdf';
return DownloadedFile( return DownloadedFile(
bytes: response.bytes, bytes: response.bytes,

View File

@@ -26,9 +26,20 @@ class PaymentDetailsController extends ChangeNotifier {
bool get canDownload { bool get canDownload {
final current = _payment; final current = _payment;
if (current == null) return false; if (current == null) return false;
final status = statusFromPayment(current); if (statusFromPayment(current) != OperationStatus.success) return false;
final paymentRef = current.paymentRef ?? ''; return primaryOperationDocumentRequest != null;
return status == OperationStatus.success && paymentRef.trim().isNotEmpty; }
OperationDocumentRequestModel? get primaryOperationDocumentRequest {
final current = _payment;
if (current == null) return null;
for (final operation in current.operations) {
final request = operationDocumentRequest(operation);
if (request != null) {
return request;
}
}
return null;
} }
OperationDocumentRequestModel? operationDocumentRequest( OperationDocumentRequestModel? operationDocumentRequest(
@@ -37,18 +48,17 @@ class PaymentDetailsController extends ChangeNotifier {
final current = _payment; final current = _payment;
if (current == null) return null; if (current == null) return null;
final paymentRef = current.paymentRef?.trim() ?? '';
if (paymentRef.isEmpty) return null;
final operationRef = operation.operationRef; final operationRef = operation.operationRef;
if (operationRef == null || operationRef.isEmpty) return null; if (operationRef == null || operationRef.isEmpty) return null;
final gatewayService = operation.gateway;
if (gatewayService == null || gatewayService.isEmpty) return null;
final pair = parseOperationCodePair(operation.code); final pair = parseOperationCodePair(operation.code);
if (pair == null) return null; if (pair == null) return null;
if (pair.operation != 'card_payout' || pair.action != 'send') return null; if (pair.operation != 'card_payout' || pair.action != 'send') return null;
return OperationDocumentRequestModel( return OperationDocumentRequestModel(
paymentRef: paymentRef, gatewayService: gatewayService,
operationRef: operationRef, operationRef: operationRef,
); );
} }

View File

@@ -1,9 +1,9 @@
class OperationDocumentRequestModel { class OperationDocumentRequestModel {
final String paymentRef; final String gatewayService;
final String operationRef; final String operationRef;
const OperationDocumentRequestModel({ const OperationDocumentRequestModel({
required this.paymentRef, required this.gatewayService,
required this.operationRef, required this.operationRef,
}); });
} }

View File

@@ -0,0 +1,9 @@
class OperationDocumentInfo {
final String operationRef;
final String gatewayService;
const OperationDocumentInfo({
required this.operationRef,
required this.gatewayService,
});
}

View File

@@ -65,7 +65,15 @@ class _PaymentDetailsView extends StatelessWidget {
payment: payment, payment: payment,
onBack: () => _handleBack(context), onBack: () => _handleBack(context),
onDownloadAct: controller.canDownload onDownloadAct: controller.canDownload
? () => downloadPaymentAct(context, payment.paymentRef ?? '') ? () {
final request = controller.primaryOperationDocumentRequest;
if (request == null) return;
downloadPaymentAct(
context,
gatewayService: request.gatewayService,
operationRef: request.operationRef,
);
}
: null, : null,
canDownloadOperationDocument: canDownloadOperationDocument:
controller.canDownloadOperationDocument, controller.canDownloadOperationDocument,
@@ -74,7 +82,7 @@ class _PaymentDetailsView extends StatelessWidget {
if (request == null) return; if (request == null) return;
downloadPaymentAct( downloadPaymentAct(
context, context,
request.paymentRef, gatewayService: request.gatewayService,
operationRef: request.operationRef, operationRef: request.operationRef,
); );
}, },

View File

@@ -9,7 +9,6 @@ import 'package:pweb/utils/report/download_act.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
class OperationRow { class OperationRow {
static DataRow build(OperationItem op, BuildContext context) { static DataRow build(OperationItem op, BuildContext context) {
final isUnknownDate = op.date.millisecondsSinceEpoch == 0; final isUnknownDate = op.date.millisecondsSinceEpoch == 0;
@@ -20,18 +19,25 @@ class OperationRow {
: '${TimeOfDay.fromDateTime(localDate).format(context)}\n' : '${TimeOfDay.fromDateTime(localDate).format(context)}\n'
'${localDate.toIso8601String().split("T").first}'; '${localDate.toIso8601String().split("T").first}';
final canDownload = op.status == OperationStatus.success && final canDownload =
(op.paymentRef ?? '').trim().isNotEmpty; op.status == OperationStatus.success &&
(op.operationRef ?? '').trim().isNotEmpty &&
(op.gatewayService ?? '').trim().isNotEmpty;
final documentCell = canDownload final documentCell = canDownload
? TextButton.icon( ? TextButton.icon(
onPressed: () => downloadPaymentAct(context, op.paymentRef ?? ''), onPressed: () => downloadPaymentAct(
context,
gatewayService: op.gatewayService ?? '',
operationRef: op.operationRef ?? '',
),
icon: const Icon(Icons.download), icon: const Icon(Icons.download),
label: Text(loc.downloadAct), label: Text(loc.downloadAct),
) )
: Text(op.fileName ?? ''); : Text(op.fileName ?? '');
return DataRow(cells: [ return DataRow(
cells: [
DataCell(OperationStatusBadge(status: op.status)), DataCell(OperationStatusBadge(status: op.status)),
DataCell(documentCell), DataCell(documentCell),
DataCell(Text('${amountToString(op.amount)} ${op.currency}')), DataCell(Text('${amountToString(op.amount)} ${op.currency}')),
@@ -41,6 +47,7 @@ class OperationRow {
DataCell(Text(op.name)), DataCell(Text(op.name)),
DataCell(Text(dateLabel)), DataCell(Text(dateLabel)),
DataCell(Text(op.comment)), DataCell(Text(op.comment)),
]); ],
);
} }
} }

View File

@@ -10,18 +10,18 @@ import 'package:pweb/utils/error/snackbar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
Future<void> downloadPaymentAct( Future<void> downloadPaymentAct(
BuildContext context, BuildContext context, {
String paymentRef, { required String gatewayService,
String? operationRef, required String operationRef,
}) async { }) async {
final organizations = context.read<OrganizationsProvider>(); final organizations = context.read<OrganizationsProvider>();
if (!organizations.isOrganizationSet) { if (!organizations.isOrganizationSet) {
return; return;
} }
final trimmed = paymentRef.trim(); final gateway = gatewayService.trim();
if (trimmed.isEmpty) { final operation = operationRef.trim();
if (gateway.isEmpty || operation.isEmpty) {
return; return;
} }
@@ -29,10 +29,10 @@ Future<void> downloadPaymentAct(
await executeActionWithNotification( await executeActionWithNotification(
context: context, context: context,
action: () async { action: () async {
final file = await PaymentDocumentsService.getAct( final file = await PaymentDocumentsService.getOperationDocument(
organizations.current.id, organizations.current.id,
trimmed, gateway,
operationRef: operationRef, operation,
); );
await downloadFile(file); await downloadFile(file);
}, },

View File

@@ -4,6 +4,9 @@ import 'package:pshared/models/payment/state.dart';
import 'package:pshared/models/payment/status.dart'; import 'package:pshared/models/payment/status.dart';
import 'package:pshared/utils/money.dart'; import 'package:pshared/utils/money.dart';
import 'package:pweb/models/report/operation/document.dart';
import 'package:pweb/utils/payment/operation_code.dart';
OperationItem mapPaymentToOperation(Payment payment) { OperationItem mapPaymentToOperation(Payment payment) {
final debit = payment.lastQuote?.amounts?.sourceDebitTotal; final debit = payment.lastQuote?.amounts?.sourceDebitTotal;
@@ -33,6 +36,7 @@ OperationItem mapPaymentToOperation(Payment payment) {
payment.state, payment.state,
]) ?? ]) ??
''; '';
final operationDocument = _resolveOperationDocument(payment);
return OperationItem( return OperationItem(
status: statusFromPayment(payment), status: statusFromPayment(payment),
@@ -43,6 +47,8 @@ OperationItem mapPaymentToOperation(Payment payment) {
toCurrency: toCurrency, toCurrency: toCurrency,
payId: payId, payId: payId,
paymentRef: payment.paymentRef, paymentRef: payment.paymentRef,
operationRef: operationDocument?.operationRef,
gatewayService: operationDocument?.gatewayService,
cardNumber: null, cardNumber: null,
name: name, name: name,
date: resolvePaymentDate(payment), date: resolvePaymentDate(payment),
@@ -50,6 +56,25 @@ OperationItem mapPaymentToOperation(Payment payment) {
); );
} }
OperationDocumentInfo? _resolveOperationDocument(Payment payment) {
for (final operation in payment.operations) {
final operationRef = operation.operationRef;
final gatewayService = operation.gateway;
if (operationRef == null || operationRef.isEmpty) continue;
if (gatewayService == null || gatewayService.isEmpty) continue;
final pair = parseOperationCodePair(operation.code);
if (pair == null) continue;
if (pair.operation != 'card_payout' || pair.action != 'send') continue;
return OperationDocumentInfo(
operationRef: operationRef,
gatewayService: gatewayService,
);
}
return null;
}
OperationStatus statusFromPayment(Payment payment) { OperationStatus statusFromPayment(Payment payment) {
switch (payment.orchestrationState) { switch (payment.orchestrationState) {
case PaymentOrchestrationState.failed: case PaymentOrchestrationState.failed: