diff --git a/api/server/interface/api/sresponse/payment.go b/api/server/interface/api/sresponse/payment.go index 4d79b82f..31a35a8a 100644 --- a/api/server/interface/api/sresponse/payment.go +++ b/api/server/interface/api/sresponse/payment.go @@ -41,26 +41,26 @@ type FxQuote struct { } type PaymentQuote struct { - QuoteRef string `json:"quoteRef,omitempty"` - DebitAmount *paymenttypes.Money `json:"debitAmount,omitempty"` - DebitSettlementAmount *paymenttypes.Money `json:"debitSettlementAmount,omitempty"` - ExpectedSettlementAmount *paymenttypes.Money `json:"expectedSettlementAmount,omitempty"` - ExpectedFeeTotal *paymenttypes.Money `json:"expectedFeeTotal,omitempty"` - FeeLines []FeeLine `json:"feeLines,omitempty"` - FxQuote *FxQuote `json:"fxQuote,omitempty"` + QuoteRef string `json:"quoteRef,omitempty"` + Amounts *QuoteAmounts `json:"amounts,omitempty"` + Fees *QuoteFees `json:"fees,omitempty"` + FxQuote *FxQuote `json:"fxQuote,omitempty"` } -type PaymentQuoteAggregate struct { - DebitAmounts []*paymenttypes.Money `json:"debitAmounts,omitempty"` - ExpectedSettlementAmounts []*paymenttypes.Money `json:"expectedSettlementAmounts,omitempty"` - ExpectedFeeTotals []*paymenttypes.Money `json:"expectedFeeTotals,omitempty"` +type QuoteAmounts struct { + SourcePrincipal *paymenttypes.Money `json:"sourcePrincipal,omitempty"` + SourceDebitTotal *paymenttypes.Money `json:"sourceDebitTotal,omitempty"` + DestinationSettlement *paymenttypes.Money `json:"destinationSettlement,omitempty"` +} + +type QuoteFees struct { + Lines []FeeLine `json:"lines,omitempty"` } type PaymentQuotes struct { - IdempotencyKey string `json:"idempotencyKey"` - QuoteRef string `json:"quoteRef,omitempty"` - Aggregate *PaymentQuoteAggregate `json:"aggregate,omitempty"` - Quotes []PaymentQuote `json:"quotes,omitempty"` + IdempotencyKey string `json:"idempotencyKey,omitempty"` + QuoteRef string `json:"quoteRef,omitempty"` + Items []PaymentQuote `json:"items,omitempty"` } type Payment struct { @@ -196,12 +196,13 @@ func toPaymentQuote(q *quotationv2.PaymentQuote) *PaymentQuote { if q == nil { return nil } + amounts := toQuoteAmounts(q) + fees := toQuoteFees(q.GetFeeLines()) return &PaymentQuote{ - QuoteRef: q.GetQuoteRef(), - DebitAmount: toMoney(q.GetPayerTotalDebitAmount()), - ExpectedSettlementAmount: toMoney(q.GetDestinationAmount()), - FeeLines: toFeeLines(q.GetFeeLines()), - FxQuote: toFxQuote(q.GetFxQuote()), + QuoteRef: q.GetQuoteRef(), + Amounts: amounts, + Fees: fees, + FxQuote: toFxQuote(q.GetFxQuote()), } } @@ -209,22 +210,45 @@ func toPaymentQuotes(resp *quotationv2.QuotePaymentsResponse) *PaymentQuotes { if resp == nil { return nil } - quotes := make([]PaymentQuote, 0, len(resp.GetQuotes())) + items := make([]PaymentQuote, 0, len(resp.GetQuotes())) for _, quote := range resp.GetQuotes() { if dto := toPaymentQuote(quote); dto != nil { - quotes = append(quotes, *dto) + items = append(items, *dto) } } - if len(quotes) == 0 { - quotes = nil + if len(items) == 0 { + items = nil } return &PaymentQuotes{ IdempotencyKey: resp.GetIdempotencyKey(), QuoteRef: resp.GetQuoteRef(), - Quotes: quotes, + Items: items, } } +func toQuoteAmounts(q *quotationv2.PaymentQuote) *QuoteAmounts { + if q == nil { + return nil + } + amounts := &QuoteAmounts{ + SourcePrincipal: toMoney(q.GetTransferPrincipalAmount()), + SourceDebitTotal: toMoney(q.GetPayerTotalDebitAmount()), + DestinationSettlement: toMoney(q.GetDestinationAmount()), + } + if amounts.SourcePrincipal == nil && amounts.SourceDebitTotal == nil && amounts.DestinationSettlement == nil { + return nil + } + return amounts +} + +func toQuoteFees(lines []*feesv1.DerivedPostingLine) *QuoteFees { + feeLines := toFeeLines(lines) + if len(feeLines) == 0 { + return nil + } + return &QuoteFees{Lines: feeLines} +} + func toPayments(items []*orchestrationv2.Payment) []Payment { if len(items) == 0 { return nil diff --git a/frontend/pshared/lib/data/dto/payment/network_fee.dart b/frontend/pshared/lib/data/dto/payment/network_fee.dart deleted file mode 100644 index b9b2f344..00000000 --- a/frontend/pshared/lib/data/dto/payment/network_fee.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -import 'package:pshared/data/dto/money.dart'; - -part 'network_fee.g.dart'; - - -@JsonSerializable() -class NetworkFeeDTO { - final MoneyDTO? networkFee; - final String? estimationContext; - - const NetworkFeeDTO({ - this.networkFee, - this.estimationContext, - }); - - factory NetworkFeeDTO.fromJson(Map json) => _$NetworkFeeDTOFromJson(json); - Map toJson() => _$NetworkFeeDTOToJson(this); -} diff --git a/frontend/pshared/lib/data/dto/payment/payment_quote.dart b/frontend/pshared/lib/data/dto/payment/payment_quote.dart index b1e665d9..326b979a 100644 --- a/frontend/pshared/lib/data/dto/payment/payment_quote.dart +++ b/frontend/pshared/lib/data/dto/payment/payment_quote.dart @@ -1,35 +1,21 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:pshared/data/dto/payment/fee_line.dart'; import 'package:pshared/data/dto/payment/fx_quote.dart'; -import 'package:pshared/data/dto/money.dart'; -import 'package:pshared/data/dto/payment/network_fee.dart'; +import 'package:pshared/data/dto/payment/quote_amounts.dart'; +import 'package:pshared/data/dto/payment/quote_fees.dart'; part 'payment_quote.g.dart'; - @JsonSerializable() class PaymentQuoteDTO { final String? quoteRef; - final MoneyDTO? debitAmount; - final MoneyDTO? debitSettlementAmount; - final MoneyDTO? expectedSettlementAmount; - final MoneyDTO? expectedFeeTotal; - final List? feeLines; - final NetworkFeeDTO? networkFee; + final QuoteAmountsDTO? amounts; + final QuoteFeesDTO? fees; final FxQuoteDTO? fxQuote; - const PaymentQuoteDTO({ - this.quoteRef, - this.debitAmount, - this.debitSettlementAmount, - this.expectedSettlementAmount, - this.expectedFeeTotal, - this.feeLines, - this.networkFee, - this.fxQuote, - }); + const PaymentQuoteDTO({this.quoteRef, this.amounts, this.fees, this.fxQuote}); - factory PaymentQuoteDTO.fromJson(Map json) => _$PaymentQuoteDTOFromJson(json); + factory PaymentQuoteDTO.fromJson(Map json) => + _$PaymentQuoteDTOFromJson(json); Map toJson() => _$PaymentQuoteDTOToJson(this); } diff --git a/frontend/pshared/lib/data/dto/payment/quote_aggregate.dart b/frontend/pshared/lib/data/dto/payment/quote_aggregate.dart deleted file mode 100644 index 34dd779b..00000000 --- a/frontend/pshared/lib/data/dto/payment/quote_aggregate.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -import 'package:pshared/data/dto/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/quote_amounts.dart b/frontend/pshared/lib/data/dto/payment/quote_amounts.dart new file mode 100644 index 00000000..4e3dcdfc --- /dev/null +++ b/frontend/pshared/lib/data/dto/payment/quote_amounts.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/money.dart'; + +part 'quote_amounts.g.dart'; + +@JsonSerializable() +class QuoteAmountsDTO { + final MoneyDTO? sourcePrincipal; + final MoneyDTO? sourceDebitTotal; + final MoneyDTO? destinationSettlement; + + const QuoteAmountsDTO({ + this.sourcePrincipal, + this.sourceDebitTotal, + this.destinationSettlement, + }); + + factory QuoteAmountsDTO.fromJson(Map json) => + _$QuoteAmountsDTOFromJson(json); + Map toJson() => _$QuoteAmountsDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/payment/quote_fees.dart b/frontend/pshared/lib/data/dto/payment/quote_fees.dart new file mode 100644 index 00000000..658183ef --- /dev/null +++ b/frontend/pshared/lib/data/dto/payment/quote_fees.dart @@ -0,0 +1,16 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/payment/fee_line.dart'; + +part 'quote_fees.g.dart'; + +@JsonSerializable() +class QuoteFeesDTO { + final List? lines; + + const QuoteFeesDTO({this.lines}); + + factory QuoteFeesDTO.fromJson(Map json) => + _$QuoteFeesDTOFromJson(json); + Map toJson() => _$QuoteFeesDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/payment/quotes.dart b/frontend/pshared/lib/data/dto/payment/quotes.dart index 71935cb3..23442814 100644 --- a/frontend/pshared/lib/data/dto/payment/quotes.dart +++ b/frontend/pshared/lib/data/dto/payment/quotes.dart @@ -1,6 +1,5 @@ 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'; @@ -9,14 +8,12 @@ part 'quotes.g.dart'; class PaymentQuotesDTO { final String quoteRef; final String? idempotencyKey; - final PaymentQuoteAggregateDTO? aggregate; - final List? quotes; + final List? items; const PaymentQuotesDTO({ required this.quoteRef, this.idempotencyKey, - this.aggregate, - this.quotes, + this.items, }); factory PaymentQuotesDTO.fromJson(Map json) => diff --git a/frontend/pshared/lib/data/mapper/payment/network_fee.dart b/frontend/pshared/lib/data/mapper/payment/network_fee.dart deleted file mode 100644 index ac1d697e..00000000 --- a/frontend/pshared/lib/data/mapper/payment/network_fee.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:pshared/data/dto/payment/network_fee.dart'; -import 'package:pshared/data/mapper/money.dart'; -import 'package:pshared/models/payment/fees/network.dart'; - - -extension NetworkFeeDTOMapper on NetworkFeeDTO { - NetworkFee toDomain() => NetworkFee( - networkFee: networkFee?.toDomain(), - estimationContext: estimationContext, - ); -} - -extension NetworkFeeMapper on NetworkFee { - NetworkFeeDTO toDTO() => NetworkFeeDTO( - networkFee: networkFee?.toDTO(), - estimationContext: estimationContext, - ); -} diff --git a/frontend/pshared/lib/data/mapper/payment/quote.dart b/frontend/pshared/lib/data/mapper/payment/quote.dart index 6a825d74..e3decf77 100644 --- a/frontend/pshared/lib/data/mapper/payment/quote.dart +++ b/frontend/pshared/lib/data/mapper/payment/quote.dart @@ -1,21 +1,15 @@ import 'package:pshared/data/dto/payment/payment_quote.dart'; -import 'package:pshared/data/mapper/payment/fees/line.dart'; import 'package:pshared/data/mapper/payment/fx_quote.dart'; -import 'package:pshared/data/mapper/money.dart'; -import 'package:pshared/data/mapper/payment/network_fee.dart'; +import 'package:pshared/data/mapper/payment/quote/amounts.dart'; +import 'package:pshared/data/mapper/payment/quote/fees.dart'; import 'package:pshared/models/payment/quote/quote.dart'; - extension PaymentQuoteDTOMapper on PaymentQuoteDTO { PaymentQuote toDomain({String? idempotencyKey}) => PaymentQuote( quoteRef: quoteRef, idempotencyKey: idempotencyKey, - debitAmount: debitAmount?.toDomain(), - debitSettlementAmount: debitSettlementAmount?.toDomain(), - expectedSettlementAmount: expectedSettlementAmount?.toDomain(), - expectedFeeTotal: expectedFeeTotal?.toDomain(), - feeLines: feeLines?.map((line) => line.toDomain()).toList(), - networkFee: networkFee?.toDomain(), + amounts: amounts?.toDomain(), + fees: fees?.toDomain(), fxQuote: fxQuote?.toDomain(), ); } @@ -23,12 +17,8 @@ extension PaymentQuoteDTOMapper on PaymentQuoteDTO { extension PaymentQuoteMapper on PaymentQuote { PaymentQuoteDTO toDTO() => PaymentQuoteDTO( quoteRef: quoteRef, - debitAmount: debitAmount?.toDTO(), - debitSettlementAmount: debitSettlementAmount?.toDTO(), - expectedSettlementAmount: expectedSettlementAmount?.toDTO(), - expectedFeeTotal: expectedFeeTotal?.toDTO(), - feeLines: feeLines?.map((line) => line.toDTO()).toList(), - networkFee: networkFee?.toDTO(), + amounts: amounts?.toDTO(), + fees: fees?.toDTO(), fxQuote: fxQuote?.toDTO(), ); } diff --git a/frontend/pshared/lib/data/mapper/payment/quote/aggregate.dart b/frontend/pshared/lib/data/mapper/payment/quote/aggregate.dart deleted file mode 100644 index ab340283..00000000 --- a/frontend/pshared/lib/data/mapper/payment/quote/aggregate.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:pshared/data/dto/payment/quote_aggregate.dart'; -import 'package:pshared/data/mapper/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/quote/amounts.dart b/frontend/pshared/lib/data/mapper/payment/quote/amounts.dart new file mode 100644 index 00000000..aee03e48 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/payment/quote/amounts.dart @@ -0,0 +1,19 @@ +import 'package:pshared/data/dto/payment/quote_amounts.dart'; +import 'package:pshared/data/mapper/money.dart'; +import 'package:pshared/models/payment/quote/amounts.dart'; + +extension QuoteAmountsDTOMapper on QuoteAmountsDTO { + QuoteAmounts toDomain() => QuoteAmounts( + sourcePrincipal: sourcePrincipal?.toDomain(), + sourceDebitTotal: sourceDebitTotal?.toDomain(), + destinationSettlement: destinationSettlement?.toDomain(), + ); +} + +extension QuoteAmountsMapper on QuoteAmounts { + QuoteAmountsDTO toDTO() => QuoteAmountsDTO( + sourcePrincipal: sourcePrincipal?.toDTO(), + sourceDebitTotal: sourceDebitTotal?.toDTO(), + destinationSettlement: destinationSettlement?.toDTO(), + ); +} diff --git a/frontend/pshared/lib/data/mapper/payment/quote/fees.dart b/frontend/pshared/lib/data/mapper/payment/quote/fees.dart new file mode 100644 index 00000000..833dba3b --- /dev/null +++ b/frontend/pshared/lib/data/mapper/payment/quote/fees.dart @@ -0,0 +1,13 @@ +import 'package:pshared/data/dto/payment/quote_fees.dart'; +import 'package:pshared/data/mapper/payment/fees/line.dart'; +import 'package:pshared/models/payment/quote/fees.dart'; + +extension QuoteFeesDTOMapper on QuoteFeesDTO { + QuoteFees toDomain() => + QuoteFees(lines: lines?.map((line) => line.toDomain()).toList()); +} + +extension QuoteFeesMapper on QuoteFees { + QuoteFeesDTO toDTO() => + QuoteFeesDTO(lines: lines?.map((line) => line.toDTO()).toList()); +} diff --git a/frontend/pshared/lib/data/mapper/payment/quote/quotes.dart b/frontend/pshared/lib/data/mapper/payment/quote/quotes.dart index 6e0c84f2..6189b071 100644 --- a/frontend/pshared/lib/data/mapper/payment/quote/quotes.dart +++ b/frontend/pshared/lib/data/mapper/payment/quote/quotes.dart @@ -1,14 +1,12 @@ import 'package:pshared/data/dto/payment/quotes.dart'; import 'package:pshared/data/mapper/payment/quote.dart'; -import 'package:pshared/data/mapper/payment/quote/aggregate.dart'; import 'package:pshared/models/payment/quote/quotes.dart'; extension PaymentQuotesDTOMapper on PaymentQuotesDTO { PaymentQuotes toDomain({String? idempotencyKey}) => PaymentQuotes( quoteRef: quoteRef, idempotencyKey: idempotencyKey ?? this.idempotencyKey, - aggregate: aggregate?.toDomain(), - quotes: quotes?.map((quote) => quote.toDomain()).toList(), + items: items?.map((quote) => quote.toDomain()).toList(), ); } @@ -16,7 +14,6 @@ extension PaymentQuotesMapper on PaymentQuotes { PaymentQuotesDTO toDTO() => PaymentQuotesDTO( quoteRef: quoteRef, idempotencyKey: idempotencyKey, - aggregate: aggregate?.toDTO(), - quotes: quotes?.map((quote) => quote.toDTO()).toList(), + items: items?.map((quote) => quote.toDTO()).toList(), ); } diff --git a/frontend/pshared/lib/models/payment/fees/network.dart b/frontend/pshared/lib/models/payment/fees/network.dart deleted file mode 100644 index 98c77edd..00000000 --- a/frontend/pshared/lib/models/payment/fees/network.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:pshared/models/money.dart'; - - -class NetworkFee { - final Money? networkFee; - final String? estimationContext; - - const NetworkFee({ - required this.networkFee, - required this.estimationContext, - }); -} diff --git a/frontend/pshared/lib/models/payment/quote/aggregate.dart b/frontend/pshared/lib/models/payment/quote/aggregate.dart deleted file mode 100644 index 6c236e13..00000000 --- a/frontend/pshared/lib/models/payment/quote/aggregate.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:pshared/models/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/quote/amounts.dart b/frontend/pshared/lib/models/payment/quote/amounts.dart new file mode 100644 index 00000000..e5fc9ebe --- /dev/null +++ b/frontend/pshared/lib/models/payment/quote/amounts.dart @@ -0,0 +1,13 @@ +import 'package:pshared/models/money.dart'; + +class QuoteAmounts { + final Money? sourcePrincipal; + final Money? sourceDebitTotal; + final Money? destinationSettlement; + + const QuoteAmounts({ + required this.sourcePrincipal, + required this.sourceDebitTotal, + required this.destinationSettlement, + }); +} diff --git a/frontend/pshared/lib/models/payment/quote/fees.dart b/frontend/pshared/lib/models/payment/quote/fees.dart new file mode 100644 index 00000000..80b2f76a --- /dev/null +++ b/frontend/pshared/lib/models/payment/quote/fees.dart @@ -0,0 +1,7 @@ +import 'package:pshared/models/payment/fees/line.dart'; + +class QuoteFees { + final List? lines; + + const QuoteFees({required this.lines}); +} diff --git a/frontend/pshared/lib/models/payment/quote/quote.dart b/frontend/pshared/lib/models/payment/quote/quote.dart index c6940043..ecf3e5bb 100644 --- a/frontend/pshared/lib/models/payment/quote/quote.dart +++ b/frontend/pshared/lib/models/payment/quote/quote.dart @@ -1,29 +1,19 @@ -import 'package:pshared/models/payment/fees/line.dart'; import 'package:pshared/models/payment/fx/quote.dart'; -import 'package:pshared/models/money.dart'; -import 'package:pshared/models/payment/fees/network.dart'; - +import 'package:pshared/models/payment/quote/amounts.dart'; +import 'package:pshared/models/payment/quote/fees.dart'; class PaymentQuote { final String? quoteRef; final String? idempotencyKey; - final Money? debitAmount; - final Money? debitSettlementAmount; - final Money? expectedSettlementAmount; - final Money? expectedFeeTotal; - final List? feeLines; - final NetworkFee? networkFee; + final QuoteAmounts? amounts; + final QuoteFees? fees; final FxQuote? fxQuote; const PaymentQuote({ required this.quoteRef, required this.idempotencyKey, - required this.debitAmount, - required this.debitSettlementAmount, - required this.expectedSettlementAmount, - required this.expectedFeeTotal, - required this.feeLines, - required this.networkFee, + required this.amounts, + required this.fees, required this.fxQuote, }); } diff --git a/frontend/pshared/lib/models/payment/quote/quotes.dart b/frontend/pshared/lib/models/payment/quote/quotes.dart index 15fca09c..6f12601f 100644 --- a/frontend/pshared/lib/models/payment/quote/quotes.dart +++ b/frontend/pshared/lib/models/payment/quote/quotes.dart @@ -1,17 +1,13 @@ import 'package:pshared/models/payment/quote/quote.dart'; -import 'package:pshared/models/payment/quote/aggregate.dart'; - class PaymentQuotes { final String quoteRef; final String? idempotencyKey; - final PaymentQuoteAggregate? aggregate; - final List? quotes; + final List? items; const PaymentQuotes({ required this.quoteRef, required this.idempotencyKey, - required this.aggregate, - required this.quotes, + required this.items, }); } diff --git a/frontend/pshared/lib/provider/payment/multiple/quotation.dart b/frontend/pshared/lib/provider/payment/multiple/quotation.dart index 6881d3e8..077cbff2 100644 --- a/frontend/pshared/lib/provider/payment/multiple/quotation.dart +++ b/frontend/pshared/lib/provider/payment/multiple/quotation.dart @@ -34,11 +34,11 @@ class MultiQuotationProvider extends ChangeNotifier { quotation != null && !_quotation.isLoading && _quotation.error == null; DateTime? get quoteExpiresAt { - final quotes = quotation?.quotes; - if (quotes == null || quotes.isEmpty) return null; + final items = quotation?.items; + if (items == null || items.isEmpty) return null; int? minExpiresAt; - for (final quote in quotes) { + for (final quote in items) { final expiresAtUnixMs = quote.fxQuote?.expiresAtUnixMs; if (expiresAtUnixMs == null) continue; minExpiresAt = minExpiresAt == null diff --git a/frontend/pshared/lib/provider/payment/quotation/quotation.dart b/frontend/pshared/lib/provider/payment/quotation/quotation.dart index e205ad6b..08bc9c06 100644 --- a/frontend/pshared/lib/provider/payment/quotation/quotation.dart +++ b/frontend/pshared/lib/provider/payment/quotation/quotation.dart @@ -23,12 +23,16 @@ import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/resource.dart'; import 'package:pshared/provider/payment/quotation/intent_builder.dart'; import 'package:pshared/service/payment/quotation.dart'; +import 'package:pshared/utils/payment/quote_helpers.dart'; import 'package:pshared/utils/exception.dart'; - class QuotationProvider extends ChangeNotifier { static final _logger = Logger('provider.payment.quotation'); - Resource _quotation = Resource(data: null, isLoading: false, error: null); + Resource _quotation = Resource( + data: null, + isLoading: false, + error: null, + ); late OrganizationsProvider _organizations; bool _isLoaded = false; PaymentIntent? _lastIntent; @@ -37,7 +41,7 @@ class QuotationProvider extends ChangeNotifier { AutoRefreshMode _autoRefreshMode = AutoRefreshMode.on; void update( - OrganizationsProvider venue, + OrganizationsProvider venue, PaymentAmountProvider payment, WalletsController wallets, PaymentFlowProvider flow, @@ -62,7 +66,8 @@ class QuotationProvider extends ChangeNotifier { bool get isLoading => _quotation.isLoading; Exception? get error => _quotation.error; bool get canRefresh => _lastIntent != null; - bool get isReady => _isLoaded && !_quotation.isLoading && _quotation.error == null; + bool get isReady => + _isLoaded && !_quotation.isLoading && _quotation.error == null; AutoRefreshMode get autoRefreshMode => _autoRefreshMode; DateTime? get quoteExpiresAt { @@ -71,10 +76,10 @@ class QuotationProvider extends ChangeNotifier { return DateTime.fromMillisecondsSinceEpoch(expiresAtUnixMs, isUtc: true); } - - Asset? get fee => _assetFromMoney(quotation?.expectedFeeTotal); - Asset? get total => _assetFromMoney(quotation?.debitAmount); - Asset? get recipientGets => _assetFromMoney(quotation?.expectedSettlementAmount); + Asset? get fee => _assetFromMoney(quoteFeeTotal(quotation)); + Asset? get total => _assetFromMoney(quotation?.amounts?.sourceDebitTotal); + Asset? get recipientGets => + _assetFromMoney(quotation?.amounts?.destinationSettlement); Asset? _assetFromMoney(Money? money) { if (money == null) return null; @@ -101,26 +106,32 @@ class QuotationProvider extends ChangeNotifier { } Future getQuotation(PaymentIntent intent) async { - if (!_organizations.isOrganizationSet) throw StateError('Organization is not set'); + if (!_organizations.isOrganizationSet) { + throw StateError('Organization is not set'); + } _lastIntent = intent; try { _setResource(_quotation.copyWith(isLoading: true, error: null)); final response = await QuotationService.getQuotation( - _organizations.current.id, + _organizations.current.id, QuotePaymentRequest( idempotencyKey: Uuid().v4(), intent: intent.toDTO(), ), ); _isLoaded = true; - _setResource(_quotation.copyWith(data: response, isLoading: false, error: null)); + _setResource( + _quotation.copyWith(data: response, isLoading: false, error: null), + ); } catch (e, st) { _logger.warning('Failed to get quotation', e, st); - _setResource(_quotation.copyWith( - data: null, - error: toException(e), - isLoading: false, - )); + _setResource( + _quotation.copyWith( + data: null, + error: toException(e), + isLoading: false, + ), + ); } return _quotation.data; } diff --git a/frontend/pshared/lib/utils/payment/quote_helpers.dart b/frontend/pshared/lib/utils/payment/quote_helpers.dart new file mode 100644 index 00000000..99bbcbda --- /dev/null +++ b/frontend/pshared/lib/utils/payment/quote_helpers.dart @@ -0,0 +1,92 @@ +import 'package:pshared/models/money.dart'; +import 'package:pshared/models/payment/fees/line.dart'; +import 'package:pshared/models/payment/quote/quote.dart'; +import 'package:pshared/utils/currency.dart'; +import 'package:pshared/utils/money.dart'; + +Money? quoteFeeTotal(PaymentQuote? quote) { + final preferredCurrency = + quote?.amounts?.sourcePrincipal?.currency ?? + quote?.amounts?.sourceDebitTotal?.currency; + return quoteFeeTotalFromLines( + quote?.fees?.lines, + preferredCurrency: preferredCurrency, + ); +} + +Money? quoteFeeTotalFromLines( + List? lines, { + String? preferredCurrency, +}) { + if (lines == null || lines.isEmpty) return null; + + final normalizedPreferred = _normalizeCurrency(preferredCurrency); + final totalsByCurrency = {}; + + for (final line in lines) { + final money = line.amount; + if (money == null) continue; + + final currency = _normalizeCurrency(money.currency); + if (currency == null) continue; + + final amount = parseMoneyAmount(money.amount, fallback: double.nan); + if (amount.isNaN) continue; + + final sign = _lineSign(line.side); + final signedAmount = sign * amount.abs(); + totalsByCurrency[currency] = + (totalsByCurrency[currency] ?? 0) + signedAmount; + } + + if (totalsByCurrency.isEmpty) return null; + + final selectedCurrency = + normalizedPreferred != null && + totalsByCurrency.containsKey(normalizedPreferred) + ? normalizedPreferred + : totalsByCurrency.keys.first; + final total = totalsByCurrency[selectedCurrency]; + if (total == null) return null; + + return Money(amount: amountToString(total), currency: selectedCurrency); +} + +List aggregateMoneyByCurrency(Iterable values) { + final totals = {}; + for (final value in values) { + if (value == null) continue; + + final currency = _normalizeCurrency(value.currency); + if (currency == null) continue; + + final amount = parseMoneyAmount(value.amount, fallback: double.nan); + if (amount.isNaN) continue; + + totals[currency] = (totals[currency] ?? 0) + amount; + } + + return totals.entries + .map( + (entry) => + Money(amount: amountToString(entry.value), currency: entry.key), + ) + .toList(); +} + +double _lineSign(String? side) { + final normalized = side?.trim().toLowerCase() ?? ''; + switch (normalized) { + case 'entry_side_credit': + case 'credit': + return -1; + default: + return 1; + } +} + +String? _normalizeCurrency(String? currency) { + final normalized = currency?.trim().toUpperCase(); + if (normalized == null || normalized.isEmpty) return null; + return normalized; +} diff --git a/frontend/pshared/test/payment/request_dto_format_test.dart b/frontend/pshared/test/payment/request_dto_format_test.dart index 1159a425..7c4154b1 100644 --- a/frontend/pshared/test/payment/request_dto_format_test.dart +++ b/frontend/pshared/test/payment/request_dto_format_test.dart @@ -82,8 +82,22 @@ void main() { 'idempotencyKey': 'idem-1', 'quote': { 'quoteRef': 'q-1', - 'debitAmount': {'amount': '10', 'currency': 'USDT'}, - 'expectedSettlementAmount': {'amount': '760', 'currency': 'RUB'}, + 'amounts': { + 'sourcePrincipal': {'amount': '10', 'currency': 'USDT'}, + 'sourceDebitTotal': {'amount': '10.75', 'currency': 'USDT'}, + 'destinationSettlement': {'amount': '760', 'currency': 'RUB'}, + }, + 'fees': { + 'lines': [ + { + 'ledgerAccountRef': 'ledger:fees', + 'amount': {'amount': '0.75', 'currency': 'USDT'}, + 'lineType': 'posting_line_type_fee', + 'side': 'entry_side_debit', + 'meta': {'fee_target': 'wallet'}, + }, + ], + }, 'fxQuote': { 'quoteRef': 'fx-1', 'baseCurrency': 'USDT', @@ -102,6 +116,8 @@ void main() { }); expect(response.quote.fxQuote?.pricedAtUnixMs, equals(1771945907000)); + expect(response.quote.amounts?.sourceDebitTotal?.amount, equals('10.75')); + expect(response.quote.fees?.lines?.length, equals(1)); }); test('initiate payment by quote keeps expected fields', () { diff --git a/frontend/pweb/lib/pages/report/details/summary_card/widget.dart b/frontend/pweb/lib/pages/report/details/summary_card/widget.dart index cd7cf05c..80041e68 100644 --- a/frontend/pweb/lib/pages/report/details/summary_card/widget.dart +++ b/frontend/pweb/lib/pages/report/details/summary_card/widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:pshared/models/payment/payment.dart'; +import 'package:pshared/utils/payment/quote_helpers.dart'; import 'package:pweb/pages/report/details/summary_card/amount_headline.dart'; import 'package:pweb/pages/report/details/summary_card/copy_id.dart'; @@ -13,7 +14,6 @@ import 'package:pweb/utils/clipboard.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; - class PaymentSummaryCard extends StatelessWidget { final Payment payment; final VoidCallback? onDownloadAct; @@ -31,11 +31,11 @@ class PaymentSummaryCard extends StatelessWidget { final status = statusFromPayment(payment); final dateLabel = formatDateLabel(context, resolvePaymentDate(payment)); - final primaryAmount = payment.lastQuote?.debitAmount ?? - payment.lastQuote?.expectedSettlementAmount; - final toAmount = payment.lastQuote?.expectedSettlementAmount; - final fee = payment.lastQuote?.expectedFeeTotal ?? - payment.lastQuote?.networkFee?.networkFee; + final primaryAmount = + payment.lastQuote?.amounts?.sourceDebitTotal ?? + payment.lastQuote?.amounts?.destinationSettlement; + final toAmount = payment.lastQuote?.amounts?.destinationSettlement; + final fee = quoteFeeTotal(payment.lastQuote); final amountLabel = formatMoney(primaryAmount); final toAmountLabel = formatMoney(toAmount); @@ -108,11 +108,8 @@ class PaymentSummaryCard extends StatelessWidget { child: CopyableId( label: loc.paymentIdLabel, value: paymentRef, - onCopy: () => copyToClipboard( - context, - paymentRef, - loc.paymentIdCopied, - ), + onCopy: () => + copyToClipboard(context, paymentRef, loc.paymentIdCopied), ), ), ], diff --git a/frontend/pweb/lib/providers/multiple_payouts.dart b/frontend/pweb/lib/providers/multiple_payouts.dart index ecaa7406..b497b8f6 100644 --- a/frontend/pweb/lib/providers/multiple_payouts.dart +++ b/frontend/pweb/lib/providers/multiple_payouts.dart @@ -2,19 +2,20 @@ import 'package:flutter/foundation.dart'; import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/payment.dart'; +import 'package:pshared/models/payment/quote/quote.dart'; import 'package:pshared/models/payment/quote/status_type.dart'; import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/provider/payment/multiple/provider.dart'; import 'package:pshared/provider/payment/multiple/quotation.dart'; import 'package:pshared/utils/currency.dart'; import 'package:pshared/utils/money.dart'; +import 'package:pshared/utils/payment/quote_helpers.dart'; import 'package:pweb/models/payment/multiple_payouts/csv_row.dart'; import 'package:pweb/models/payment/multiple_payouts/state.dart'; import 'package:pweb/utils/payment/multiple_csv_parser.dart'; import 'package:pweb/utils/payment/multiple_intent_builder.dart'; - class MultiplePayoutsProvider extends ChangeNotifier { final MultipleCsvParser _csvParser; final MultipleIntentBuilder _intentBuilder; @@ -34,10 +35,7 @@ class MultiplePayoutsProvider extends ChangeNotifier { }) : _csvParser = csvParser ?? MultipleCsvParser(), _intentBuilder = intentBuilder ?? MultipleIntentBuilder(); - void update( - MultiQuotationProvider quotation, - MultiPaymentProvider payment, - ) { + void update(MultiQuotationProvider quotation, MultiPaymentProvider payment) { _bindQuotation(quotation); _payment = payment; } @@ -60,7 +58,9 @@ class MultiplePayoutsProvider extends ChangeNotifier { if (quotation.isLoading) return QuoteStatusType.loading; if (quotation.error != null) return QuoteStatusType.error; if (quotation.quotation == null) return QuoteStatusType.missing; - if (_isQuoteExpired(quotation.quoteExpiresAt)) return QuoteStatusType.expired; + if (_isQuoteExpired(quotation.quoteExpiresAt)) { + return QuoteStatusType.expired; + } return QuoteStatusType.active; } @@ -78,10 +78,10 @@ class MultiplePayoutsProvider extends ChangeNotifier { Money? aggregateDebitAmountFor(Wallet? sourceWallet) { if (_rows.isEmpty) return null; - return _moneyForSourceCurrency( - _quotation?.quotation?.aggregate?.debitAmounts, - sourceWallet, + final totals = aggregateMoneyByCurrency( + _quoteItems().map((quote) => quote.amounts?.sourceDebitTotal), ); + return _moneyForSourceCurrency(totals, sourceWallet); } Money? get requestedSentAmount { @@ -99,18 +99,16 @@ class MultiplePayoutsProvider extends ChangeNotifier { Money? aggregateSettlementAmountFor(Wallet? sourceWallet) { if (_rows.isEmpty) return null; - return _moneyForSourceCurrency( - _quotation?.quotation?.aggregate?.expectedSettlementAmounts, - sourceWallet, + final totals = aggregateMoneyByCurrency( + _quoteItems().map((quote) => quote.amounts?.destinationSettlement), ); + return _moneyForSourceCurrency(totals, sourceWallet); } Money? aggregateFeeAmountFor(Wallet? sourceWallet) { if (_rows.isEmpty) return null; - return _moneyForSourceCurrency( - _quotation?.quotation?.aggregate?.expectedFeeTotals, - sourceWallet, - ); + final totals = aggregateMoneyByCurrency(_quoteItems().map(quoteFeeTotal)); + return _moneyForSourceCurrency(totals, sourceWallet); } double? aggregateFeePercentFor(Wallet? sourceWallet) { @@ -256,10 +254,7 @@ class MultiplePayoutsProvider extends ChangeNotifier { }; } - Money? _moneyForSourceCurrency( - List? values, - Wallet? sourceWallet, - ) { + Money? _moneyForSourceCurrency(List? values, Wallet? sourceWallet) { if (values == null || values.isEmpty) return null; if (sourceWallet != null) { @@ -274,6 +269,9 @@ class MultiplePayoutsProvider extends ChangeNotifier { return values.first; } + List _quoteItems() => + _quotation?.quotation?.items ?? const []; + @override void dispose() { _quotation?.removeListener(_onQuotationChanged); diff --git a/frontend/pweb/lib/utils/report/payment_mapper.dart b/frontend/pweb/lib/utils/report/payment_mapper.dart index f091fb17..eee30945 100644 --- a/frontend/pweb/lib/utils/report/payment_mapper.dart +++ b/frontend/pweb/lib/utils/report/payment_mapper.dart @@ -5,10 +5,9 @@ import 'package:pshared/utils/money.dart'; import 'package:pweb/models/payment/payment_state.dart'; - OperationItem mapPaymentToOperation(Payment payment) { - final debit = payment.lastQuote?.debitAmount; - final settlement = payment.lastQuote?.expectedSettlementAmount; + final debit = payment.lastQuote?.amounts?.sourceDebitTotal; + final settlement = payment.lastQuote?.amounts?.destinationSettlement; final amountMoney = debit ?? settlement; final amount = parseMoneyAmount(amountMoney?.amount); @@ -18,18 +17,17 @@ OperationItem mapPaymentToOperation(Payment payment) { : parseMoneyAmount(settlement.amount); final toCurrency = settlement?.currency ?? currency; - final payId = _firstNonEmpty([ - payment.paymentRef, - payment.idempotencyKey, - ]) ?? - '-'; - final name = _firstNonEmpty([ + final payId = + _firstNonEmpty([payment.paymentRef, payment.idempotencyKey]) ?? '-'; + final name = + _firstNonEmpty([ payment.lastQuote?.quoteRef, payment.paymentRef, payment.idempotencyKey, ]) ?? '-'; - final comment = _firstNonEmpty([ + final comment = + _firstNonEmpty([ payment.failureReason, payment.failureCode, payment.state, @@ -72,17 +70,17 @@ DateTime resolvePaymentDate(Payment payment) { final expiresAt = payment.lastQuote?.fxQuote?.expiresAtUnixMs; if (expiresAt != null && expiresAt > 0) { - return DateTime.fromMillisecondsSinceEpoch(expiresAt, isUtc: true).toLocal(); + return DateTime.fromMillisecondsSinceEpoch( + expiresAt, + isUtc: true, + ).toLocal(); } return DateTime.fromMillisecondsSinceEpoch(0); } String? paymentIdFromOperation(OperationItem operation) { - final candidates = [ - operation.paymentRef, - operation.payId, - ]; + final candidates = [operation.paymentRef, operation.payId]; for (final candidate in candidates) { final trimmed = candidate?.trim(); if (trimmed != null && trimmed.isNotEmpty && trimmed != '-') {