removed legacy from bff

This commit is contained in:
Stephan D
2026-02-24 21:18:23 +01:00
parent a998b59072
commit da11be526a
26 changed files with 343 additions and 273 deletions

View File

@@ -42,25 +42,25 @@ 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"`
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"`
IdempotencyKey string `json:"idempotencyKey,omitempty"`
QuoteRef string `json:"quoteRef,omitempty"`
Aggregate *PaymentQuoteAggregate `json:"aggregate,omitempty"`
Quotes []PaymentQuote `json:"quotes,omitempty"`
Items []PaymentQuote `json:"items,omitempty"`
}
type Payment struct {
@@ -196,11 +196,12 @@ 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()),
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

View File

@@ -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<String, dynamic> json) => _$NetworkFeeDTOFromJson(json);
Map<String, dynamic> toJson() => _$NetworkFeeDTOToJson(this);
}

View File

@@ -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<FeeLineDTO>? 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<String, dynamic> json) => _$PaymentQuoteDTOFromJson(json);
factory PaymentQuoteDTO.fromJson(Map<String, dynamic> json) =>
_$PaymentQuoteDTOFromJson(json);
Map<String, dynamic> toJson() => _$PaymentQuoteDTOToJson(this);
}

View File

@@ -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<MoneyDTO>? debitAmounts;
final List<MoneyDTO>? expectedSettlementAmounts;
final List<MoneyDTO>? expectedFeeTotals;
final List<MoneyDTO>? networkFeeTotals;
const PaymentQuoteAggregateDTO({
this.debitAmounts,
this.expectedSettlementAmounts,
this.expectedFeeTotals,
this.networkFeeTotals,
});
factory PaymentQuoteAggregateDTO.fromJson(Map<String, dynamic> json) => _$PaymentQuoteAggregateDTOFromJson(json);
Map<String, dynamic> toJson() => _$PaymentQuoteAggregateDTOToJson(this);
}

View File

@@ -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<String, dynamic> json) =>
_$QuoteAmountsDTOFromJson(json);
Map<String, dynamic> toJson() => _$QuoteAmountsDTOToJson(this);
}

View File

@@ -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<FeeLineDTO>? lines;
const QuoteFeesDTO({this.lines});
factory QuoteFeesDTO.fromJson(Map<String, dynamic> json) =>
_$QuoteFeesDTOFromJson(json);
Map<String, dynamic> toJson() => _$QuoteFeesDTOToJson(this);
}

View File

@@ -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<PaymentQuoteDTO>? quotes;
final List<PaymentQuoteDTO>? items;
const PaymentQuotesDTO({
required this.quoteRef,
this.idempotencyKey,
this.aggregate,
this.quotes,
this.items,
});
factory PaymentQuotesDTO.fromJson(Map<String, dynamic> json) =>

View File

@@ -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,
);
}

View File

@@ -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(),
);
}

View File

@@ -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(),
);
}

View File

@@ -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(),
);
}

View File

@@ -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());
}

View File

@@ -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(),
);
}

View File

@@ -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,
});
}

View File

@@ -1,16 +0,0 @@
import 'package:pshared/models/money.dart';
class PaymentQuoteAggregate {
final List<Money>? debitAmounts;
final List<Money>? expectedSettlementAmounts;
final List<Money>? expectedFeeTotals;
final List<Money>? networkFeeTotals;
const PaymentQuoteAggregate({
required this.debitAmounts,
required this.expectedSettlementAmounts,
required this.expectedFeeTotals,
required this.networkFeeTotals,
});
}

View File

@@ -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,
});
}

View File

@@ -0,0 +1,7 @@
import 'package:pshared/models/payment/fees/line.dart';
class QuoteFees {
final List<FeeLine>? lines;
const QuoteFees({required this.lines});
}

View File

@@ -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<FeeLine>? 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,
});
}

View File

@@ -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<PaymentQuote>? quotes;
final List<PaymentQuote>? items;
const PaymentQuotes({
required this.quoteRef,
required this.idempotencyKey,
required this.aggregate,
required this.quotes,
required this.items,
});
}

View File

@@ -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

View File

@@ -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<PaymentQuote> _quotation = Resource(data: null, isLoading: false, error: null);
Resource<PaymentQuote> _quotation = Resource(
data: null,
isLoading: false,
error: null,
);
late OrganizationsProvider _organizations;
bool _isLoaded = false;
PaymentIntent? _lastIntent;
@@ -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,7 +106,9 @@ class QuotationProvider extends ChangeNotifier {
}
Future<PaymentQuote?> 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));
@@ -113,14 +120,18 @@ class QuotationProvider extends ChangeNotifier {
),
);
_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(
_setResource(
_quotation.copyWith(
data: null,
error: toException(e),
isLoading: false,
));
),
);
}
return _quotation.data;
}

View File

@@ -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<FeeLine>? lines, {
String? preferredCurrency,
}) {
if (lines == null || lines.isEmpty) return null;
final normalizedPreferred = _normalizeCurrency(preferredCurrency);
final totalsByCurrency = <String, double>{};
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<Money> aggregateMoneyByCurrency(Iterable<Money?> values) {
final totals = <String, double>{};
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;
}

View File

@@ -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', () {

View File

@@ -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),
),
),
],

View File

@@ -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<Money>? values,
Wallet? sourceWallet,
) {
Money? _moneyForSourceCurrency(List<Money>? 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<PaymentQuote> _quoteItems() =>
_quotation?.quotation?.items ?? const <PaymentQuote>[];
@override
void dispose() {
_quotation?.removeListener(_onQuotationChanged);

View File

@@ -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 != '-') {