diff --git a/api/payments/orchestrator/internal/service/orchestrator/convert.go b/api/payments/orchestrator/internal/service/orchestrator/convert.go index 4108ac0..41b624e 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/convert.go +++ b/api/payments/orchestrator/internal/service/orchestrator/convert.go @@ -5,9 +5,7 @@ import ( "time" "github.com/tech/sendico/payments/orchestrator/storage/model" - "github.com/tech/sendico/pkg/merrors" fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" - paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" @@ -413,74 +411,3 @@ func cloneNetworkEstimate(resp *chainv1.EstimateTransferFeeResponse) *chainv1.Es } return nil } - -func protoFailureToModel(code orchestratorv1.PaymentFailureCode) model.PaymentFailureCode { - switch code { - case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_BALANCE: - return model.PaymentFailureCodeBalance - case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_LEDGER: - return model.PaymentFailureCodeLedger - case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_FX: - return model.PaymentFailureCodeFX - case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_CHAIN: - return model.PaymentFailureCodeChain - case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_FEES: - return model.PaymentFailureCodeFees - case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_POLICY: - return model.PaymentFailureCodePolicy - default: - return model.PaymentFailureCodeUnspecified - } -} - -func applyProtoPaymentToModel(src *orchestratorv1.Payment, dst *model.Payment) error { - if src == nil || dst == nil { - return merrors.InvalidArgument("payment payload is required") - } - dst.PaymentRef = strings.TrimSpace(src.GetPaymentRef()) - dst.IdempotencyKey = strings.TrimSpace(src.GetIdempotencyKey()) - dst.Intent = intentFromProto(src.GetIntent()) - dst.State = modelStateFromProto(src.GetState()) - dst.FailureCode = protoFailureToModel(src.GetFailureCode()) - dst.FailureReason = strings.TrimSpace(src.GetFailureReason()) - dst.Metadata = cloneMetadata(src.GetMetadata()) - dst.LastQuote = quoteSnapshotToModel(src.GetLastQuote()) - dst.Execution = executionFromProto(src.GetExecution()) - if src.GetCardPayout() != nil { - dst.CardPayout = &model.CardPayout{ - PayoutRef: strings.TrimSpace(src.GetCardPayout().GetPayoutRef()), - ProviderPaymentID: strings.TrimSpace(src.GetCardPayout().GetProviderPaymentId()), - Status: strings.TrimSpace(src.GetCardPayout().GetStatus()), - FailureReason: strings.TrimSpace(src.GetCardPayout().GetFailureReason()), - CardCountry: strings.TrimSpace(src.GetCardPayout().GetCardCountry()), - MaskedPan: strings.TrimSpace(src.GetCardPayout().GetMaskedPan()), - ProviderCode: strings.TrimSpace(src.GetCardPayout().GetProviderCode()), - GatewayReference: strings.TrimSpace(src.GetCardPayout().GetGatewayReference()), - } - } - return nil -} - -func executionFromProto(src *orchestratorv1.ExecutionRefs) *model.ExecutionRefs { - if src == nil { - return nil - } - return &model.ExecutionRefs{ - DebitEntryRef: strings.TrimSpace(src.GetDebitEntryRef()), - CreditEntryRef: strings.TrimSpace(src.GetCreditEntryRef()), - FXEntryRef: strings.TrimSpace(src.GetFxEntryRef()), - ChainTransferRef: strings.TrimSpace(src.GetChainTransferRef()), - CardPayoutRef: strings.TrimSpace(src.GetCardPayoutRef()), - FeeTransferRef: strings.TrimSpace(src.GetFeeTransferRef()), - } -} - -func ensurePageRequest(req *orchestratorv1.ListPaymentsRequest) *paginationv1.CursorPageRequest { - if req == nil { - return &paginationv1.CursorPageRequest{} - } - if req.GetPage() == nil { - return &paginationv1.CursorPageRequest{} - } - return req.GetPage() -} diff --git a/api/payments/orchestrator/internal/service/orchestrator/helpers.go b/api/payments/orchestrator/internal/service/orchestrator/helpers.go index 5e11ed8..55962cf 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/helpers.go +++ b/api/payments/orchestrator/internal/service/orchestrator/helpers.go @@ -214,20 +214,6 @@ func decimalFromMoney(m *moneyv1.Money) (decimal.Decimal, error) { return decimal.NewFromString(m.GetAmount()) } -func decimalFromMoneyMatching(reference, candidate *moneyv1.Money) (*decimal.Decimal, error) { - if reference == nil || candidate == nil { - return nil, nil - } - if !strings.EqualFold(reference.GetCurrency(), candidate.GetCurrency()) { - return nil, nil - } - value, err := decimal.NewFromString(candidate.GetAmount()) - if err != nil { - return nil, err - } - return &value, nil -} - func makeMoney(currency string, value decimal.Decimal) *moneyv1.Money { return &moneyv1.Money{ Currency: currency, diff --git a/api/payments/orchestrator/internal/service/orchestrator/quote_batch_test.go b/api/payments/orchestrator/internal/service/orchestrator/quote_batch_test.go index 0eae3c9..a64cc43 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/quote_batch_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/quote_batch_test.go @@ -57,7 +57,7 @@ func TestMinQuoteExpiry(t *testing.T) { later := now.Add(10 * time.Minute) earliest := now.Add(5 * time.Minute) - min, ok := minQuoteExpiry([]time.Time{later, time.Time{}, earliest}) + min, ok := minQuoteExpiry([]time.Time{later, {}, earliest}) if !ok { t.Fatal("expected min expiry to be set") } @@ -65,7 +65,7 @@ func TestMinQuoteExpiry(t *testing.T) { t.Fatalf("expected min expiry %v, got %v", earliest, min) } - if _, ok := minQuoteExpiry([]time.Time{time.Time{}}); ok { + if _, ok := minQuoteExpiry([]time.Time{{}}); ok { t.Fatal("expected min expiry to be unset") } } diff --git a/frontend/pshared/lib/api/requests/payment/quotes.dart b/frontend/pshared/lib/api/requests/payment/quotes.dart new file mode 100644 index 0000000..40dbf57 --- /dev/null +++ b/frontend/pshared/lib/api/requests/payment/quotes.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:pshared/api/requests/payment/base.dart'; +import 'package:pshared/data/dto/payment/intent/payment.dart'; + +part 'quotes.g.dart'; + + +@JsonSerializable() +class QuotePaymentsRequest extends PaymentBaseRequest { + final List intents; + + @JsonKey(defaultValue: false) + final bool previewOnly; + + const QuotePaymentsRequest({ + required super.idempotencyKey, + super.metadata, + required this.intents, + this.previewOnly = false, + }); + + factory QuotePaymentsRequest.fromJson(Map json) => _$QuotePaymentsRequestFromJson(json); + @override + Map toJson() => _$QuotePaymentsRequestToJson(this); +} diff --git a/frontend/pshared/lib/api/responses/payment/quotes.dart b/frontend/pshared/lib/api/responses/payment/quotes.dart new file mode 100644 index 0000000..972eae2 --- /dev/null +++ b/frontend/pshared/lib/api/responses/payment/quotes.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/api/responses/base.dart'; +import 'package:pshared/api/responses/token.dart'; +import 'package:pshared/data/dto/payment/quotes.dart'; + +part 'quotes.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class PaymentQuotesResponse extends BaseAuthorizedResponse { + + final PaymentQuotesDTO quote; + + const PaymentQuotesResponse({required super.accessToken, required this.quote}); + + factory PaymentQuotesResponse.fromJson(Map json) => _$PaymentQuotesResponseFromJson(json); + @override + Map toJson() => _$PaymentQuotesResponseToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/payment/quote_aggregate.dart b/frontend/pshared/lib/data/dto/payment/quote_aggregate.dart new file mode 100644 index 0000000..140564e --- /dev/null +++ b/frontend/pshared/lib/data/dto/payment/quote_aggregate.dart @@ -0,0 +1,24 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/payment/money.dart'; + +part 'quote_aggregate.g.dart'; + + +@JsonSerializable() +class PaymentQuoteAggregateDTO { + final List? debitAmounts; + final List? expectedSettlementAmounts; + final List? expectedFeeTotals; + final List? networkFeeTotals; + + const PaymentQuoteAggregateDTO({ + this.debitAmounts, + this.expectedSettlementAmounts, + this.expectedFeeTotals, + this.networkFeeTotals, + }); + + factory PaymentQuoteAggregateDTO.fromJson(Map json) => _$PaymentQuoteAggregateDTOFromJson(json); + Map toJson() => _$PaymentQuoteAggregateDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/payment/quotes.dart b/frontend/pshared/lib/data/dto/payment/quotes.dart new file mode 100644 index 0000000..6a9174a --- /dev/null +++ b/frontend/pshared/lib/data/dto/payment/quotes.dart @@ -0,0 +1,23 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/payment/quote_aggregate.dart'; +import 'package:pshared/data/dto/payment/payment_quote.dart'; + +part 'quotes.g.dart'; + + +@JsonSerializable() +class PaymentQuotesDTO { + final String quoteRef; + final PaymentQuoteAggregateDTO? aggregate; + final List? quotes; + + const PaymentQuotesDTO({ + required this.quoteRef, + this.aggregate, + this.quotes, + }); + + factory PaymentQuotesDTO.fromJson(Map json) => _$PaymentQuotesDTOFromJson(json); + Map toJson() => _$PaymentQuotesDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/mapper/payment/quote_aggregate.dart b/frontend/pshared/lib/data/mapper/payment/quote_aggregate.dart new file mode 100644 index 0000000..45d9fc0 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/payment/quote_aggregate.dart @@ -0,0 +1,22 @@ +import 'package:pshared/data/dto/payment/quote_aggregate.dart'; +import 'package:pshared/data/mapper/payment/money.dart'; +import 'package:pshared/models/payment/quote_aggregate.dart'; + + +extension PaymentQuoteAggregateDTOMapper on PaymentQuoteAggregateDTO { + PaymentQuoteAggregate toDomain() => PaymentQuoteAggregate( + debitAmounts: debitAmounts?.map((amount) => amount.toDomain()).toList(), + expectedSettlementAmounts: expectedSettlementAmounts?.map((amount) => amount.toDomain()).toList(), + expectedFeeTotals: expectedFeeTotals?.map((amount) => amount.toDomain()).toList(), + networkFeeTotals: networkFeeTotals?.map((amount) => amount.toDomain()).toList(), + ); +} + +extension PaymentQuoteAggregateMapper on PaymentQuoteAggregate { + PaymentQuoteAggregateDTO toDTO() => PaymentQuoteAggregateDTO( + debitAmounts: debitAmounts?.map((amount) => amount.toDTO()).toList(), + expectedSettlementAmounts: expectedSettlementAmounts?.map((amount) => amount.toDTO()).toList(), + expectedFeeTotals: expectedFeeTotals?.map((amount) => amount.toDTO()).toList(), + networkFeeTotals: networkFeeTotals?.map((amount) => amount.toDTO()).toList(), + ); +} diff --git a/frontend/pshared/lib/data/mapper/payment/quotes.dart b/frontend/pshared/lib/data/mapper/payment/quotes.dart new file mode 100644 index 0000000..3b9785f --- /dev/null +++ b/frontend/pshared/lib/data/mapper/payment/quotes.dart @@ -0,0 +1,21 @@ +import 'package:pshared/data/dto/payment/quotes.dart'; +import 'package:pshared/data/mapper/payment/payment_quote.dart'; +import 'package:pshared/data/mapper/payment/quote_aggregate.dart'; +import 'package:pshared/models/payment/quotes.dart'; + + +extension PaymentQuotesDTOMapper on PaymentQuotesDTO { + PaymentQuotes toDomain() => PaymentQuotes( + quoteRef: quoteRef, + aggregate: aggregate?.toDomain(), + quotes: quotes?.map((quote) => quote.toDomain()).toList(), + ); +} + +extension PaymentQuotesMapper on PaymentQuotes { + PaymentQuotesDTO toDTO() => PaymentQuotesDTO( + quoteRef: quoteRef, + aggregate: aggregate?.toDTO(), + quotes: quotes?.map((quote) => quote.toDTO()).toList(), + ); +} diff --git a/frontend/pshared/lib/models/payment/quote_aggregate.dart b/frontend/pshared/lib/models/payment/quote_aggregate.dart new file mode 100644 index 0000000..07a006e --- /dev/null +++ b/frontend/pshared/lib/models/payment/quote_aggregate.dart @@ -0,0 +1,16 @@ +import 'package:pshared/models/payment/money.dart'; + + +class PaymentQuoteAggregate { + final List? debitAmounts; + final List? expectedSettlementAmounts; + final List? expectedFeeTotals; + final List? networkFeeTotals; + + const PaymentQuoteAggregate({ + required this.debitAmounts, + required this.expectedSettlementAmounts, + required this.expectedFeeTotals, + required this.networkFeeTotals, + }); +} diff --git a/frontend/pshared/lib/models/payment/quotes.dart b/frontend/pshared/lib/models/payment/quotes.dart new file mode 100644 index 0000000..6d548c7 --- /dev/null +++ b/frontend/pshared/lib/models/payment/quotes.dart @@ -0,0 +1,15 @@ +import 'package:pshared/models/payment/quote.dart'; +import 'package:pshared/models/payment/quote_aggregate.dart'; + + +class PaymentQuotes { + final String quoteRef; + final PaymentQuoteAggregate? aggregate; + final List? quotes; + + const PaymentQuotes({ + required this.quoteRef, + required this.aggregate, + required this.quotes, + }); +} diff --git a/frontend/pshared/lib/service/payment/quotation.dart b/frontend/pshared/lib/service/payment/quotation.dart index d3382cc..93ba559 100644 --- a/frontend/pshared/lib/service/payment/quotation.dart +++ b/frontend/pshared/lib/service/payment/quotation.dart @@ -1,9 +1,13 @@ import 'package:logging/logging.dart'; import 'package:pshared/api/requests/payment/quote.dart'; +import 'package:pshared/api/requests/payment/quotes.dart'; import 'package:pshared/api/responses/payment/quotation.dart'; +import 'package:pshared/api/responses/payment/quotes.dart'; import 'package:pshared/data/mapper/payment/payment_quote.dart'; +import 'package:pshared/data/mapper/payment/quotes.dart'; import 'package:pshared/models/payment/quote.dart'; +import 'package:pshared/models/payment/quotes.dart'; import 'package:pshared/service/authorization/service.dart'; import 'package:pshared/service/services.dart'; @@ -21,4 +25,14 @@ class QuotationService { ); return PaymentQuoteResponse.fromJson(response).quote.toDomain(); } + + static Future getMultipleQuotation(String organizationRef, QuotePaymentsRequest request) async { + _logger.fine('Quoting payments for organization $organizationRef'); + final response = await AuthorizationService.getPOSTResponse( + _objectType, + '/quote-multiple/$organizationRef', + request.toJson(), + ); + return PaymentQuotesResponse.fromJson(response).quote.toDomain(); + } }