From bd5dfb4f267d3be313b34f26f5030ec6813ff49f Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 21 Jan 2026 16:50:47 +0100 Subject: [PATCH] idempotency key delivery fix --- .../service/orchestrator/handlers_commands.go | 12 ++++++--- .../orchestrator/v1/orchestrator.proto | 1 + api/server/interface/api/sresponse/payment.go | 20 +++++++------- .../internal/server/paymentapiimp/quote.go | 2 +- .../lib/api/responses/payment/quotation.dart | 3 ++- .../pshared/lib/data/dto/payment/quotes.dart | 2 ++ .../lib/data/mapper/payment/quotes.dart | 2 ++ .../pshared/lib/models/payment/quote.dart | 27 +++++++++++++++++++ .../pshared/lib/models/payment/quotes.dart | 2 ++ .../lib/provider/payment/quotation.dart | 11 +++----- .../lib/service/payment/quotation.dart | 5 ++-- 11 files changed, 63 insertions(+), 24 deletions(-) diff --git a/api/payments/orchestrator/internal/service/orchestrator/handlers_commands.go b/api/payments/orchestrator/internal/service/orchestrator/handlers_commands.go index 4e2b4a71..0a330467 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/handlers_commands.go +++ b/api/payments/orchestrator/internal/service/orchestrator/handlers_commands.go @@ -69,7 +69,10 @@ func (h *quotePaymentCommand) Execute( return h.mapQuoteErr(err) } - return gsresponse.Success(&orchestratorv1.QuotePaymentResponse{Quote: quoteProto}) + return gsresponse.Success(&orchestratorv1.QuotePaymentResponse{ + IdempotencyKey: req.GetIdempotencyKey(), + Quote: quoteProto, + }) } func (h *quotePaymentCommand) prepareQuoteCtx(req *orchestratorv1.QuotePaymentRequest) (*quoteCtx, error) { @@ -315,9 +318,10 @@ func (h *quotePaymentsCommand) Execute( ) return gsresponse.Success(&orchestratorv1.QuotePaymentsResponse{ - QuoteRef: quoteRef, - Aggregate: aggregate, - Quotes: quotes, + IdempotencyKey: req.GetIdempotencyKey(), + QuoteRef: quoteRef, + Aggregate: aggregate, + Quotes: quotes, }) } diff --git a/api/proto/payments/orchestrator/v1/orchestrator.proto b/api/proto/payments/orchestrator/v1/orchestrator.proto index d7538367..8fecbc37 100644 --- a/api/proto/payments/orchestrator/v1/orchestrator.proto +++ b/api/proto/payments/orchestrator/v1/orchestrator.proto @@ -248,6 +248,7 @@ message QuotePaymentsResponse { string quote_ref = 1; PaymentQuoteAggregate aggregate = 2; repeated PaymentQuote quotes = 3; + string idempotency_key = 4; } message InitiatePaymentsRequest { diff --git a/api/server/interface/api/sresponse/payment.go b/api/server/interface/api/sresponse/payment.go index b859c88e..c5dc678b 100644 --- a/api/server/interface/api/sresponse/payment.go +++ b/api/server/interface/api/sresponse/payment.go @@ -35,7 +35,6 @@ type FxQuote struct { } type PaymentQuote struct { - IdempotencyKey string `json:"idempotencyKey"` QuoteRef string `json:"quoteRef,omitempty"` DebitAmount *model.Money `json:"debitAmount,omitempty"` ExpectedSettlementAmount *model.Money `json:"expectedSettlementAmount,omitempty"` @@ -67,8 +66,9 @@ type Payment struct { } type paymentQuoteResponse struct { - authResponse `json:",inline"` - Quote *PaymentQuote `json:"quote"` + authResponse `json:",inline"` + IdempotencyKey string `json:"idempotencyKey,omitempty"` + Quote *PaymentQuote `json:"quote"` } type paymentQuotesResponse struct { @@ -88,10 +88,11 @@ type paymentResponse struct { } // PaymentQuote wraps a payment quote with refreshed access token. -func PaymentQuoteResponse(logger mlogger.Logger, quote *orchestratorv1.PaymentQuote, token *TokenData) http.HandlerFunc { +func PaymentQuoteResponse(logger mlogger.Logger, idempotencyKey string, quote *orchestratorv1.PaymentQuote, token *TokenData) http.HandlerFunc { return response.Ok(logger, paymentQuoteResponse{ - Quote: toPaymentQuote(quote), - authResponse: authResponse{AccessToken: *token}, + Quote: toPaymentQuote(quote), + IdempotencyKey: idempotencyKey, + authResponse: authResponse{AccessToken: *token}, }) } @@ -216,9 +217,10 @@ func toPaymentQuotes(resp *orchestratorv1.QuotePaymentsResponse) *PaymentQuotes quotes = nil } return &PaymentQuotes{ - QuoteRef: resp.GetQuoteRef(), - Aggregate: toPaymentQuoteAggregate(resp.GetAggregate()), - Quotes: quotes, + IdempotencyKey: resp.GetIdempotencyKey(), + QuoteRef: resp.GetQuoteRef(), + Aggregate: toPaymentQuoteAggregate(resp.GetAggregate()), + Quotes: quotes, } } diff --git a/api/server/internal/server/paymentapiimp/quote.go b/api/server/internal/server/paymentapiimp/quote.go index 1a9fd2a0..b9a7a0af 100644 --- a/api/server/internal/server/paymentapiimp/quote.go +++ b/api/server/internal/server/paymentapiimp/quote.go @@ -65,7 +65,7 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token return response.Auto(a.logger, a.Name(), err) } - return sresponse.PaymentQuoteResponse(a.logger, resp.GetQuote(), token) + return sresponse.PaymentQuoteResponse(a.logger, resp.GetIdempotencyKey(), resp.GetQuote(), token) } func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc { diff --git a/frontend/pshared/lib/api/responses/payment/quotation.dart b/frontend/pshared/lib/api/responses/payment/quotation.dart index 25202cd5..3bee1df2 100644 --- a/frontend/pshared/lib/api/responses/payment/quotation.dart +++ b/frontend/pshared/lib/api/responses/payment/quotation.dart @@ -11,8 +11,9 @@ part 'quotation.g.dart'; class PaymentQuoteResponse extends BaseAuthorizedResponse { final PaymentQuoteDTO quote; + final String idempotencyKey; - const PaymentQuoteResponse({required super.accessToken, required this.quote}); + const PaymentQuoteResponse({required super.accessToken, required this.idempotencyKey, required this.quote}); factory PaymentQuoteResponse.fromJson(Map json) => _$PaymentQuoteResponseFromJson(json); @override diff --git a/frontend/pshared/lib/data/dto/payment/quotes.dart b/frontend/pshared/lib/data/dto/payment/quotes.dart index 6a9174ad..9f8ce8ce 100644 --- a/frontend/pshared/lib/data/dto/payment/quotes.dart +++ b/frontend/pshared/lib/data/dto/payment/quotes.dart @@ -9,6 +9,7 @@ part 'quotes.g.dart'; @JsonSerializable() class PaymentQuotesDTO { final String quoteRef; + final String idempotencyKey; final PaymentQuoteAggregateDTO? aggregate; final List? quotes; @@ -16,6 +17,7 @@ class PaymentQuotesDTO { required this.quoteRef, this.aggregate, this.quotes, + required this.idempotencyKey, }); factory PaymentQuotesDTO.fromJson(Map json) => _$PaymentQuotesDTOFromJson(json); diff --git a/frontend/pshared/lib/data/mapper/payment/quotes.dart b/frontend/pshared/lib/data/mapper/payment/quotes.dart index 3b9785f2..300e25b0 100644 --- a/frontend/pshared/lib/data/mapper/payment/quotes.dart +++ b/frontend/pshared/lib/data/mapper/payment/quotes.dart @@ -9,11 +9,13 @@ extension PaymentQuotesDTOMapper on PaymentQuotesDTO { quoteRef: quoteRef, aggregate: aggregate?.toDomain(), quotes: quotes?.map((quote) => quote.toDomain()).toList(), + idempotencyKey: idempotencyKey ); } extension PaymentQuotesMapper on PaymentQuotes { PaymentQuotesDTO toDTO() => PaymentQuotesDTO( + idempotencyKey: idempotencyKey, quoteRef: quoteRef, aggregate: aggregate?.toDTO(), quotes: quotes?.map((quote) => quote.toDTO()).toList(), diff --git a/frontend/pshared/lib/models/payment/quote.dart b/frontend/pshared/lib/models/payment/quote.dart index aa080e6c..a56b2b91 100644 --- a/frontend/pshared/lib/models/payment/quote.dart +++ b/frontend/pshared/lib/models/payment/quote.dart @@ -1,3 +1,4 @@ +import 'package:pshared/api/responses/payment/quotation.dart'; import 'package:pshared/models/payment/fees/line.dart'; import 'package:pshared/models/payment/fx/quote.dart'; import 'package:pshared/models/payment/money.dart'; @@ -23,3 +24,29 @@ class PaymentQuote { required this.fxQuote, }); } + +class PaymentQuoteX extends PaymentQuote { + final String idempotencyKey; + + const PaymentQuoteX({ + required super.quoteRef, + required super.debitAmount, + required super.expectedSettlementAmount, + required super.expectedFeeTotal, + required super.feeLines, + required super.networkFee, + required super.fxQuote, + required this.idempotencyKey, + }); + + factory PaymentQuoteX.build({required PaymentQuote quote, required String idempotencyKey}) => PaymentQuoteX( + quoteRef: quote.quoteRef, + debitAmount: quote.debitAmount, + expectedSettlementAmount: quote.expectedSettlementAmount, + expectedFeeTotal: quote.expectedFeeTotal, + feeLines: quote.feeLines, + networkFee: quote.networkFee, + fxQuote: quote.fxQuote, + idempotencyKey: idempotencyKey, + ); +} diff --git a/frontend/pshared/lib/models/payment/quotes.dart b/frontend/pshared/lib/models/payment/quotes.dart index 6d548c78..4d475faa 100644 --- a/frontend/pshared/lib/models/payment/quotes.dart +++ b/frontend/pshared/lib/models/payment/quotes.dart @@ -4,6 +4,7 @@ import 'package:pshared/models/payment/quote_aggregate.dart'; class PaymentQuotes { final String quoteRef; + final String idempotencyKey; final PaymentQuoteAggregate? aggregate; final List? quotes; @@ -11,5 +12,6 @@ class PaymentQuotes { required this.quoteRef, required this.aggregate, required this.quotes, + required this.idempotencyKey, }); } diff --git a/frontend/pshared/lib/provider/payment/quotation.dart b/frontend/pshared/lib/provider/payment/quotation.dart index 6af30e02..285b48cd 100644 --- a/frontend/pshared/lib/provider/payment/quotation.dart +++ b/frontend/pshared/lib/provider/payment/quotation.dart @@ -28,10 +28,11 @@ import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/resource.dart'; import 'package:pshared/service/payment/quotation.dart'; import 'package:pshared/utils/currency.dart'; +import 'package:pshared/utils/exception.dart'; class QuotationProvider extends ChangeNotifier { - Resource _quotation = Resource(data: null, isLoading: false, error: null); + Resource _quotation = Resource(data: null, isLoading: false, error: null); late OrganizationsProvider _organizations; bool _isLoaded = false; @@ -162,7 +163,7 @@ class QuotationProvider extends ChangeNotifier { return recipientName?.isNotEmpty == true ? recipientName : null; } - void _setResource(Resource quotation) { + void _setResource(Resource quotation) { _quotation = quotation; notifyListeners(); } @@ -181,11 +182,7 @@ class QuotationProvider extends ChangeNotifier { _isLoaded = true; _setResource(_quotation.copyWith(data: response, isLoading: false, error: null)); } catch (e) { - _setResource(_quotation.copyWith( - data: null, - error: e is Exception ? e : Exception(e.toString()), - isLoading: false, - )); + _setResource(_quotation.copyWith(data: null, error: toException(e), isLoading: false)); } notifyListeners(); return _quotation.data; diff --git a/frontend/pshared/lib/service/payment/quotation.dart b/frontend/pshared/lib/service/payment/quotation.dart index 99a5155f..54c2a72f 100644 --- a/frontend/pshared/lib/service/payment/quotation.dart +++ b/frontend/pshared/lib/service/payment/quotation.dart @@ -16,14 +16,15 @@ class QuotationService { static final _logger = Logger('service.payment.quotation'); static const String _objectType = Services.payments; - static Future getQuotation(String organizationRef, QuotePaymentRequest request) async { + static Future getQuotation(String organizationRef, QuotePaymentRequest request) async { _logger.fine('Quoting payment for organization $organizationRef'); final response = await AuthorizationService.getPOSTResponse( _objectType, '/quote/$organizationRef', request.toJson(), ); - return PaymentQuoteResponse.fromJson(response).quote.toDomain(); + final resp = PaymentQuoteResponse.fromJson(response); + return PaymentQuoteX.build(quote: resp.quote.toDomain(), idempotencyKey: resp.idempotencyKey); } static Future getMultiQuotation(String organizationRef, QuotePaymentsRequest request) async { -- 2.49.1