idempotency key delivery fix

This commit is contained in:
Stephan D
2026-01-21 16:50:47 +01:00
parent 87f99be01d
commit bd5dfb4f26
11 changed files with 63 additions and 24 deletions

View File

@@ -69,7 +69,10 @@ func (h *quotePaymentCommand) Execute(
return h.mapQuoteErr(err) 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) { func (h *quotePaymentCommand) prepareQuoteCtx(req *orchestratorv1.QuotePaymentRequest) (*quoteCtx, error) {
@@ -315,6 +318,7 @@ func (h *quotePaymentsCommand) Execute(
) )
return gsresponse.Success(&orchestratorv1.QuotePaymentsResponse{ return gsresponse.Success(&orchestratorv1.QuotePaymentsResponse{
IdempotencyKey: req.GetIdempotencyKey(),
QuoteRef: quoteRef, QuoteRef: quoteRef,
Aggregate: aggregate, Aggregate: aggregate,
Quotes: quotes, Quotes: quotes,

View File

@@ -248,6 +248,7 @@ message QuotePaymentsResponse {
string quote_ref = 1; string quote_ref = 1;
PaymentQuoteAggregate aggregate = 2; PaymentQuoteAggregate aggregate = 2;
repeated PaymentQuote quotes = 3; repeated PaymentQuote quotes = 3;
string idempotency_key = 4;
} }
message InitiatePaymentsRequest { message InitiatePaymentsRequest {

View File

@@ -35,7 +35,6 @@ type FxQuote struct {
} }
type PaymentQuote struct { type PaymentQuote struct {
IdempotencyKey string `json:"idempotencyKey"`
QuoteRef string `json:"quoteRef,omitempty"` QuoteRef string `json:"quoteRef,omitempty"`
DebitAmount *model.Money `json:"debitAmount,omitempty"` DebitAmount *model.Money `json:"debitAmount,omitempty"`
ExpectedSettlementAmount *model.Money `json:"expectedSettlementAmount,omitempty"` ExpectedSettlementAmount *model.Money `json:"expectedSettlementAmount,omitempty"`
@@ -68,6 +67,7 @@ type Payment struct {
type paymentQuoteResponse struct { type paymentQuoteResponse struct {
authResponse `json:",inline"` authResponse `json:",inline"`
IdempotencyKey string `json:"idempotencyKey,omitempty"`
Quote *PaymentQuote `json:"quote"` Quote *PaymentQuote `json:"quote"`
} }
@@ -88,9 +88,10 @@ type paymentResponse struct {
} }
// PaymentQuote wraps a payment quote with refreshed access token. // 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{ return response.Ok(logger, paymentQuoteResponse{
Quote: toPaymentQuote(quote), Quote: toPaymentQuote(quote),
IdempotencyKey: idempotencyKey,
authResponse: authResponse{AccessToken: *token}, authResponse: authResponse{AccessToken: *token},
}) })
} }
@@ -216,6 +217,7 @@ func toPaymentQuotes(resp *orchestratorv1.QuotePaymentsResponse) *PaymentQuotes
quotes = nil quotes = nil
} }
return &PaymentQuotes{ return &PaymentQuotes{
IdempotencyKey: resp.GetIdempotencyKey(),
QuoteRef: resp.GetQuoteRef(), QuoteRef: resp.GetQuoteRef(),
Aggregate: toPaymentQuoteAggregate(resp.GetAggregate()), Aggregate: toPaymentQuoteAggregate(resp.GetAggregate()),
Quotes: quotes, Quotes: quotes,

View File

@@ -65,7 +65,7 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
return response.Auto(a.logger, a.Name(), err) 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 { func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {

View File

@@ -11,8 +11,9 @@ part 'quotation.g.dart';
class PaymentQuoteResponse extends BaseAuthorizedResponse { class PaymentQuoteResponse extends BaseAuthorizedResponse {
final PaymentQuoteDTO quote; 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<String, dynamic> json) => _$PaymentQuoteResponseFromJson(json); factory PaymentQuoteResponse.fromJson(Map<String, dynamic> json) => _$PaymentQuoteResponseFromJson(json);
@override @override

View File

@@ -9,6 +9,7 @@ part 'quotes.g.dart';
@JsonSerializable() @JsonSerializable()
class PaymentQuotesDTO { class PaymentQuotesDTO {
final String quoteRef; final String quoteRef;
final String idempotencyKey;
final PaymentQuoteAggregateDTO? aggregate; final PaymentQuoteAggregateDTO? aggregate;
final List<PaymentQuoteDTO>? quotes; final List<PaymentQuoteDTO>? quotes;
@@ -16,6 +17,7 @@ class PaymentQuotesDTO {
required this.quoteRef, required this.quoteRef,
this.aggregate, this.aggregate,
this.quotes, this.quotes,
required this.idempotencyKey,
}); });
factory PaymentQuotesDTO.fromJson(Map<String, dynamic> json) => _$PaymentQuotesDTOFromJson(json); factory PaymentQuotesDTO.fromJson(Map<String, dynamic> json) => _$PaymentQuotesDTOFromJson(json);

View File

@@ -9,11 +9,13 @@ extension PaymentQuotesDTOMapper on PaymentQuotesDTO {
quoteRef: quoteRef, quoteRef: quoteRef,
aggregate: aggregate?.toDomain(), aggregate: aggregate?.toDomain(),
quotes: quotes?.map((quote) => quote.toDomain()).toList(), quotes: quotes?.map((quote) => quote.toDomain()).toList(),
idempotencyKey: idempotencyKey
); );
} }
extension PaymentQuotesMapper on PaymentQuotes { extension PaymentQuotesMapper on PaymentQuotes {
PaymentQuotesDTO toDTO() => PaymentQuotesDTO( PaymentQuotesDTO toDTO() => PaymentQuotesDTO(
idempotencyKey: idempotencyKey,
quoteRef: quoteRef, quoteRef: quoteRef,
aggregate: aggregate?.toDTO(), aggregate: aggregate?.toDTO(),
quotes: quotes?.map((quote) => quote.toDTO()).toList(), quotes: quotes?.map((quote) => quote.toDTO()).toList(),

View File

@@ -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/fees/line.dart';
import 'package:pshared/models/payment/fx/quote.dart'; import 'package:pshared/models/payment/fx/quote.dart';
import 'package:pshared/models/payment/money.dart'; import 'package:pshared/models/payment/money.dart';
@@ -23,3 +24,29 @@ class PaymentQuote {
required this.fxQuote, 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,
);
}

View File

@@ -4,6 +4,7 @@ import 'package:pshared/models/payment/quote_aggregate.dart';
class PaymentQuotes { class PaymentQuotes {
final String quoteRef; final String quoteRef;
final String idempotencyKey;
final PaymentQuoteAggregate? aggregate; final PaymentQuoteAggregate? aggregate;
final List<PaymentQuote>? quotes; final List<PaymentQuote>? quotes;
@@ -11,5 +12,6 @@ class PaymentQuotes {
required this.quoteRef, required this.quoteRef,
required this.aggregate, required this.aggregate,
required this.quotes, required this.quotes,
required this.idempotencyKey,
}); });
} }

View File

@@ -28,10 +28,11 @@ import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/resource.dart'; import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/payment/quotation.dart'; import 'package:pshared/service/payment/quotation.dart';
import 'package:pshared/utils/currency.dart'; import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/exception.dart';
class QuotationProvider extends ChangeNotifier { class QuotationProvider extends ChangeNotifier {
Resource<PaymentQuote> _quotation = Resource(data: null, isLoading: false, error: null); Resource<PaymentQuoteX> _quotation = Resource(data: null, isLoading: false, error: null);
late OrganizationsProvider _organizations; late OrganizationsProvider _organizations;
bool _isLoaded = false; bool _isLoaded = false;
@@ -162,7 +163,7 @@ class QuotationProvider extends ChangeNotifier {
return recipientName?.isNotEmpty == true ? recipientName : null; return recipientName?.isNotEmpty == true ? recipientName : null;
} }
void _setResource(Resource<PaymentQuote> quotation) { void _setResource(Resource<PaymentQuoteX> quotation) {
_quotation = quotation; _quotation = quotation;
notifyListeners(); notifyListeners();
} }
@@ -181,11 +182,7 @@ class QuotationProvider extends ChangeNotifier {
_isLoaded = true; _isLoaded = true;
_setResource(_quotation.copyWith(data: response, isLoading: false, error: null)); _setResource(_quotation.copyWith(data: response, isLoading: false, error: null));
} catch (e) { } catch (e) {
_setResource(_quotation.copyWith( _setResource(_quotation.copyWith(data: null, error: toException(e), isLoading: false));
data: null,
error: e is Exception ? e : Exception(e.toString()),
isLoading: false,
));
} }
notifyListeners(); notifyListeners();
return _quotation.data; return _quotation.data;

View File

@@ -16,14 +16,15 @@ class QuotationService {
static final _logger = Logger('service.payment.quotation'); static final _logger = Logger('service.payment.quotation');
static const String _objectType = Services.payments; static const String _objectType = Services.payments;
static Future<PaymentQuote> getQuotation(String organizationRef, QuotePaymentRequest request) async { static Future<PaymentQuoteX> getQuotation(String organizationRef, QuotePaymentRequest request) async {
_logger.fine('Quoting payment for organization $organizationRef'); _logger.fine('Quoting payment for organization $organizationRef');
final response = await AuthorizationService.getPOSTResponse( final response = await AuthorizationService.getPOSTResponse(
_objectType, _objectType,
'/quote/$organizationRef', '/quote/$organizationRef',
request.toJson(), 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<PaymentQuotes> getMultiQuotation(String organizationRef, QuotePaymentsRequest request) async { static Future<PaymentQuotes> getMultiQuotation(String organizationRef, QuotePaymentsRequest request) async {