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

@@ -41,26 +41,26 @@ type FxQuote struct {
} }
type PaymentQuote struct { type PaymentQuote struct {
QuoteRef string `json:"quoteRef,omitempty"` QuoteRef string `json:"quoteRef,omitempty"`
DebitAmount *paymenttypes.Money `json:"debitAmount,omitempty"` Amounts *QuoteAmounts `json:"amounts,omitempty"`
DebitSettlementAmount *paymenttypes.Money `json:"debitSettlementAmount,omitempty"` Fees *QuoteFees `json:"fees,omitempty"`
ExpectedSettlementAmount *paymenttypes.Money `json:"expectedSettlementAmount,omitempty"` FxQuote *FxQuote `json:"fxQuote,omitempty"`
ExpectedFeeTotal *paymenttypes.Money `json:"expectedFeeTotal,omitempty"`
FeeLines []FeeLine `json:"feeLines,omitempty"`
FxQuote *FxQuote `json:"fxQuote,omitempty"`
} }
type PaymentQuoteAggregate struct { type QuoteAmounts struct {
DebitAmounts []*paymenttypes.Money `json:"debitAmounts,omitempty"` SourcePrincipal *paymenttypes.Money `json:"sourcePrincipal,omitempty"`
ExpectedSettlementAmounts []*paymenttypes.Money `json:"expectedSettlementAmounts,omitempty"` SourceDebitTotal *paymenttypes.Money `json:"sourceDebitTotal,omitempty"`
ExpectedFeeTotals []*paymenttypes.Money `json:"expectedFeeTotals,omitempty"` DestinationSettlement *paymenttypes.Money `json:"destinationSettlement,omitempty"`
}
type QuoteFees struct {
Lines []FeeLine `json:"lines,omitempty"`
} }
type PaymentQuotes struct { type PaymentQuotes struct {
IdempotencyKey string `json:"idempotencyKey"` IdempotencyKey string `json:"idempotencyKey,omitempty"`
QuoteRef string `json:"quoteRef,omitempty"` QuoteRef string `json:"quoteRef,omitempty"`
Aggregate *PaymentQuoteAggregate `json:"aggregate,omitempty"` Items []PaymentQuote `json:"items,omitempty"`
Quotes []PaymentQuote `json:"quotes,omitempty"`
} }
type Payment struct { type Payment struct {
@@ -196,12 +196,13 @@ func toPaymentQuote(q *quotationv2.PaymentQuote) *PaymentQuote {
if q == nil { if q == nil {
return nil return nil
} }
amounts := toQuoteAmounts(q)
fees := toQuoteFees(q.GetFeeLines())
return &PaymentQuote{ return &PaymentQuote{
QuoteRef: q.GetQuoteRef(), QuoteRef: q.GetQuoteRef(),
DebitAmount: toMoney(q.GetPayerTotalDebitAmount()), Amounts: amounts,
ExpectedSettlementAmount: toMoney(q.GetDestinationAmount()), Fees: fees,
FeeLines: toFeeLines(q.GetFeeLines()), FxQuote: toFxQuote(q.GetFxQuote()),
FxQuote: toFxQuote(q.GetFxQuote()),
} }
} }
@@ -209,22 +210,45 @@ func toPaymentQuotes(resp *quotationv2.QuotePaymentsResponse) *PaymentQuotes {
if resp == nil { if resp == nil {
return nil return nil
} }
quotes := make([]PaymentQuote, 0, len(resp.GetQuotes())) items := make([]PaymentQuote, 0, len(resp.GetQuotes()))
for _, quote := range resp.GetQuotes() { for _, quote := range resp.GetQuotes() {
if dto := toPaymentQuote(quote); dto != nil { if dto := toPaymentQuote(quote); dto != nil {
quotes = append(quotes, *dto) items = append(items, *dto)
} }
} }
if len(quotes) == 0 { if len(items) == 0 {
quotes = nil items = nil
} }
return &PaymentQuotes{ return &PaymentQuotes{
IdempotencyKey: resp.GetIdempotencyKey(), IdempotencyKey: resp.GetIdempotencyKey(),
QuoteRef: resp.GetQuoteRef(), 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 { func toPayments(items []*orchestrationv2.Payment) []Payment {
if len(items) == 0 { if len(items) == 0 {
return nil 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: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/payment/fx_quote.dart';
import 'package:pshared/data/dto/money.dart'; import 'package:pshared/data/dto/payment/quote_amounts.dart';
import 'package:pshared/data/dto/payment/network_fee.dart'; import 'package:pshared/data/dto/payment/quote_fees.dart';
part 'payment_quote.g.dart'; part 'payment_quote.g.dart';
@JsonSerializable() @JsonSerializable()
class PaymentQuoteDTO { class PaymentQuoteDTO {
final String? quoteRef; final String? quoteRef;
final MoneyDTO? debitAmount; final QuoteAmountsDTO? amounts;
final MoneyDTO? debitSettlementAmount; final QuoteFeesDTO? fees;
final MoneyDTO? expectedSettlementAmount;
final MoneyDTO? expectedFeeTotal;
final List<FeeLineDTO>? feeLines;
final NetworkFeeDTO? networkFee;
final FxQuoteDTO? fxQuote; final FxQuoteDTO? fxQuote;
const PaymentQuoteDTO({ const PaymentQuoteDTO({this.quoteRef, this.amounts, this.fees, this.fxQuote});
this.quoteRef,
this.debitAmount,
this.debitSettlementAmount,
this.expectedSettlementAmount,
this.expectedFeeTotal,
this.feeLines,
this.networkFee,
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); 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:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/payment/quote_aggregate.dart';
import 'package:pshared/data/dto/payment/payment_quote.dart'; import 'package:pshared/data/dto/payment/payment_quote.dart';
part 'quotes.g.dart'; part 'quotes.g.dart';
@@ -9,14 +8,12 @@ part 'quotes.g.dart';
class PaymentQuotesDTO { class PaymentQuotesDTO {
final String quoteRef; final String quoteRef;
final String? idempotencyKey; final String? idempotencyKey;
final PaymentQuoteAggregateDTO? aggregate; final List<PaymentQuoteDTO>? items;
final List<PaymentQuoteDTO>? quotes;
const PaymentQuotesDTO({ const PaymentQuotesDTO({
required this.quoteRef, required this.quoteRef,
this.idempotencyKey, this.idempotencyKey,
this.aggregate, this.items,
this.quotes,
}); });
factory PaymentQuotesDTO.fromJson(Map<String, dynamic> json) => 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/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/payment/fx_quote.dart';
import 'package:pshared/data/mapper/money.dart'; import 'package:pshared/data/mapper/payment/quote/amounts.dart';
import 'package:pshared/data/mapper/payment/network_fee.dart'; import 'package:pshared/data/mapper/payment/quote/fees.dart';
import 'package:pshared/models/payment/quote/quote.dart'; import 'package:pshared/models/payment/quote/quote.dart';
extension PaymentQuoteDTOMapper on PaymentQuoteDTO { extension PaymentQuoteDTOMapper on PaymentQuoteDTO {
PaymentQuote toDomain({String? idempotencyKey}) => PaymentQuote( PaymentQuote toDomain({String? idempotencyKey}) => PaymentQuote(
quoteRef: quoteRef, quoteRef: quoteRef,
idempotencyKey: idempotencyKey, idempotencyKey: idempotencyKey,
debitAmount: debitAmount?.toDomain(), amounts: amounts?.toDomain(),
debitSettlementAmount: debitSettlementAmount?.toDomain(), fees: fees?.toDomain(),
expectedSettlementAmount: expectedSettlementAmount?.toDomain(),
expectedFeeTotal: expectedFeeTotal?.toDomain(),
feeLines: feeLines?.map((line) => line.toDomain()).toList(),
networkFee: networkFee?.toDomain(),
fxQuote: fxQuote?.toDomain(), fxQuote: fxQuote?.toDomain(),
); );
} }
@@ -23,12 +17,8 @@ extension PaymentQuoteDTOMapper on PaymentQuoteDTO {
extension PaymentQuoteMapper on PaymentQuote { extension PaymentQuoteMapper on PaymentQuote {
PaymentQuoteDTO toDTO() => PaymentQuoteDTO( PaymentQuoteDTO toDTO() => PaymentQuoteDTO(
quoteRef: quoteRef, quoteRef: quoteRef,
debitAmount: debitAmount?.toDTO(), amounts: amounts?.toDTO(),
debitSettlementAmount: debitSettlementAmount?.toDTO(), fees: fees?.toDTO(),
expectedSettlementAmount: expectedSettlementAmount?.toDTO(),
expectedFeeTotal: expectedFeeTotal?.toDTO(),
feeLines: feeLines?.map((line) => line.toDTO()).toList(),
networkFee: networkFee?.toDTO(),
fxQuote: fxQuote?.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/dto/payment/quotes.dart';
import 'package:pshared/data/mapper/payment/quote.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'; import 'package:pshared/models/payment/quote/quotes.dart';
extension PaymentQuotesDTOMapper on PaymentQuotesDTO { extension PaymentQuotesDTOMapper on PaymentQuotesDTO {
PaymentQuotes toDomain({String? idempotencyKey}) => PaymentQuotes( PaymentQuotes toDomain({String? idempotencyKey}) => PaymentQuotes(
quoteRef: quoteRef, quoteRef: quoteRef,
idempotencyKey: idempotencyKey ?? this.idempotencyKey, idempotencyKey: idempotencyKey ?? this.idempotencyKey,
aggregate: aggregate?.toDomain(), items: items?.map((quote) => quote.toDomain()).toList(),
quotes: quotes?.map((quote) => quote.toDomain()).toList(),
); );
} }
@@ -16,7 +14,6 @@ extension PaymentQuotesMapper on PaymentQuotes {
PaymentQuotesDTO toDTO() => PaymentQuotesDTO( PaymentQuotesDTO toDTO() => PaymentQuotesDTO(
quoteRef: quoteRef, quoteRef: quoteRef,
idempotencyKey: idempotencyKey, idempotencyKey: idempotencyKey,
aggregate: aggregate?.toDTO(), items: items?.map((quote) => quote.toDTO()).toList(),
quotes: quotes?.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/payment/fx/quote.dart';
import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/quote/amounts.dart';
import 'package:pshared/models/payment/fees/network.dart'; import 'package:pshared/models/payment/quote/fees.dart';
class PaymentQuote { class PaymentQuote {
final String? quoteRef; final String? quoteRef;
final String? idempotencyKey; final String? idempotencyKey;
final Money? debitAmount; final QuoteAmounts? amounts;
final Money? debitSettlementAmount; final QuoteFees? fees;
final Money? expectedSettlementAmount;
final Money? expectedFeeTotal;
final List<FeeLine>? feeLines;
final NetworkFee? networkFee;
final FxQuote? fxQuote; final FxQuote? fxQuote;
const PaymentQuote({ const PaymentQuote({
required this.quoteRef, required this.quoteRef,
required this.idempotencyKey, required this.idempotencyKey,
required this.debitAmount, required this.amounts,
required this.debitSettlementAmount, required this.fees,
required this.expectedSettlementAmount,
required this.expectedFeeTotal,
required this.feeLines,
required this.networkFee,
required this.fxQuote, required this.fxQuote,
}); });
} }

View File

@@ -1,17 +1,13 @@
import 'package:pshared/models/payment/quote/quote.dart'; import 'package:pshared/models/payment/quote/quote.dart';
import 'package:pshared/models/payment/quote/aggregate.dart';
class PaymentQuotes { class PaymentQuotes {
final String quoteRef; final String quoteRef;
final String? idempotencyKey; final String? idempotencyKey;
final PaymentQuoteAggregate? aggregate; final List<PaymentQuote>? items;
final List<PaymentQuote>? quotes;
const PaymentQuotes({ const PaymentQuotes({
required this.quoteRef, required this.quoteRef,
required this.idempotencyKey, required this.idempotencyKey,
required this.aggregate, required this.items,
required this.quotes,
}); });
} }

View File

@@ -34,11 +34,11 @@ class MultiQuotationProvider extends ChangeNotifier {
quotation != null && !_quotation.isLoading && _quotation.error == null; quotation != null && !_quotation.isLoading && _quotation.error == null;
DateTime? get quoteExpiresAt { DateTime? get quoteExpiresAt {
final quotes = quotation?.quotes; final items = quotation?.items;
if (quotes == null || quotes.isEmpty) return null; if (items == null || items.isEmpty) return null;
int? minExpiresAt; int? minExpiresAt;
for (final quote in quotes) { for (final quote in items) {
final expiresAtUnixMs = quote.fxQuote?.expiresAtUnixMs; final expiresAtUnixMs = quote.fxQuote?.expiresAtUnixMs;
if (expiresAtUnixMs == null) continue; if (expiresAtUnixMs == null) continue;
minExpiresAt = minExpiresAt == null 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/resource.dart';
import 'package:pshared/provider/payment/quotation/intent_builder.dart'; import 'package:pshared/provider/payment/quotation/intent_builder.dart';
import 'package:pshared/service/payment/quotation.dart'; import 'package:pshared/service/payment/quotation.dart';
import 'package:pshared/utils/payment/quote_helpers.dart';
import 'package:pshared/utils/exception.dart'; import 'package:pshared/utils/exception.dart';
class QuotationProvider extends ChangeNotifier { class QuotationProvider extends ChangeNotifier {
static final _logger = Logger('provider.payment.quotation'); 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; late OrganizationsProvider _organizations;
bool _isLoaded = false; bool _isLoaded = false;
PaymentIntent? _lastIntent; PaymentIntent? _lastIntent;
@@ -37,7 +41,7 @@ class QuotationProvider extends ChangeNotifier {
AutoRefreshMode _autoRefreshMode = AutoRefreshMode.on; AutoRefreshMode _autoRefreshMode = AutoRefreshMode.on;
void update( void update(
OrganizationsProvider venue, OrganizationsProvider venue,
PaymentAmountProvider payment, PaymentAmountProvider payment,
WalletsController wallets, WalletsController wallets,
PaymentFlowProvider flow, PaymentFlowProvider flow,
@@ -62,7 +66,8 @@ class QuotationProvider extends ChangeNotifier {
bool get isLoading => _quotation.isLoading; bool get isLoading => _quotation.isLoading;
Exception? get error => _quotation.error; Exception? get error => _quotation.error;
bool get canRefresh => _lastIntent != null; 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; AutoRefreshMode get autoRefreshMode => _autoRefreshMode;
DateTime? get quoteExpiresAt { DateTime? get quoteExpiresAt {
@@ -71,10 +76,10 @@ class QuotationProvider extends ChangeNotifier {
return DateTime.fromMillisecondsSinceEpoch(expiresAtUnixMs, isUtc: true); return DateTime.fromMillisecondsSinceEpoch(expiresAtUnixMs, isUtc: true);
} }
Asset? get fee => _assetFromMoney(quoteFeeTotal(quotation));
Asset? get fee => _assetFromMoney(quotation?.expectedFeeTotal); Asset? get total => _assetFromMoney(quotation?.amounts?.sourceDebitTotal);
Asset? get total => _assetFromMoney(quotation?.debitAmount); Asset? get recipientGets =>
Asset? get recipientGets => _assetFromMoney(quotation?.expectedSettlementAmount); _assetFromMoney(quotation?.amounts?.destinationSettlement);
Asset? _assetFromMoney(Money? money) { Asset? _assetFromMoney(Money? money) {
if (money == null) return null; if (money == null) return null;
@@ -101,26 +106,32 @@ class QuotationProvider extends ChangeNotifier {
} }
Future<PaymentQuote?> getQuotation(PaymentIntent intent) async { 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; _lastIntent = intent;
try { try {
_setResource(_quotation.copyWith(isLoading: true, error: null)); _setResource(_quotation.copyWith(isLoading: true, error: null));
final response = await QuotationService.getQuotation( final response = await QuotationService.getQuotation(
_organizations.current.id, _organizations.current.id,
QuotePaymentRequest( QuotePaymentRequest(
idempotencyKey: Uuid().v4(), idempotencyKey: Uuid().v4(),
intent: intent.toDTO(), intent: intent.toDTO(),
), ),
); );
_isLoaded = true; _isLoaded = true;
_setResource(_quotation.copyWith(data: response, isLoading: false, error: null)); _setResource(
_quotation.copyWith(data: response, isLoading: false, error: null),
);
} catch (e, st) { } catch (e, st) {
_logger.warning('Failed to get quotation', e, st); _logger.warning('Failed to get quotation', e, st);
_setResource(_quotation.copyWith( _setResource(
data: null, _quotation.copyWith(
error: toException(e), data: null,
isLoading: false, error: toException(e),
)); isLoading: false,
),
);
} }
return _quotation.data; 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', 'idempotencyKey': 'idem-1',
'quote': { 'quote': {
'quoteRef': 'q-1', 'quoteRef': 'q-1',
'debitAmount': {'amount': '10', 'currency': 'USDT'}, 'amounts': {
'expectedSettlementAmount': {'amount': '760', 'currency': 'RUB'}, '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': { 'fxQuote': {
'quoteRef': 'fx-1', 'quoteRef': 'fx-1',
'baseCurrency': 'USDT', 'baseCurrency': 'USDT',
@@ -102,6 +116,8 @@ void main() {
}); });
expect(response.quote.fxQuote?.pricedAtUnixMs, equals(1771945907000)); 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', () { test('initiate payment by quote keeps expected fields', () {

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pshared/models/payment/payment.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/amount_headline.dart';
import 'package:pweb/pages/report/details/summary_card/copy_id.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'; import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentSummaryCard extends StatelessWidget { class PaymentSummaryCard extends StatelessWidget {
final Payment payment; final Payment payment;
final VoidCallback? onDownloadAct; final VoidCallback? onDownloadAct;
@@ -31,11 +31,11 @@ class PaymentSummaryCard extends StatelessWidget {
final status = statusFromPayment(payment); final status = statusFromPayment(payment);
final dateLabel = formatDateLabel(context, resolvePaymentDate(payment)); final dateLabel = formatDateLabel(context, resolvePaymentDate(payment));
final primaryAmount = payment.lastQuote?.debitAmount ?? final primaryAmount =
payment.lastQuote?.expectedSettlementAmount; payment.lastQuote?.amounts?.sourceDebitTotal ??
final toAmount = payment.lastQuote?.expectedSettlementAmount; payment.lastQuote?.amounts?.destinationSettlement;
final fee = payment.lastQuote?.expectedFeeTotal ?? final toAmount = payment.lastQuote?.amounts?.destinationSettlement;
payment.lastQuote?.networkFee?.networkFee; final fee = quoteFeeTotal(payment.lastQuote);
final amountLabel = formatMoney(primaryAmount); final amountLabel = formatMoney(primaryAmount);
final toAmountLabel = formatMoney(toAmount); final toAmountLabel = formatMoney(toAmount);
@@ -108,11 +108,8 @@ class PaymentSummaryCard extends StatelessWidget {
child: CopyableId( child: CopyableId(
label: loc.paymentIdLabel, label: loc.paymentIdLabel,
value: paymentRef, value: paymentRef,
onCopy: () => copyToClipboard( onCopy: () =>
context, copyToClipboard(context, paymentRef, loc.paymentIdCopied),
paymentRef,
loc.paymentIdCopied,
),
), ),
), ),
], ],

View File

@@ -2,19 +2,20 @@ import 'package:flutter/foundation.dart';
import 'package:pshared/models/money.dart'; import 'package:pshared/models/money.dart';
import 'package:pshared/models/payment/payment.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/quote/status_type.dart';
import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/provider/payment/multiple/provider.dart'; import 'package:pshared/provider/payment/multiple/provider.dart';
import 'package:pshared/provider/payment/multiple/quotation.dart'; import 'package:pshared/provider/payment/multiple/quotation.dart';
import 'package:pshared/utils/currency.dart'; import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.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/csv_row.dart';
import 'package:pweb/models/payment/multiple_payouts/state.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_csv_parser.dart';
import 'package:pweb/utils/payment/multiple_intent_builder.dart'; import 'package:pweb/utils/payment/multiple_intent_builder.dart';
class MultiplePayoutsProvider extends ChangeNotifier { class MultiplePayoutsProvider extends ChangeNotifier {
final MultipleCsvParser _csvParser; final MultipleCsvParser _csvParser;
final MultipleIntentBuilder _intentBuilder; final MultipleIntentBuilder _intentBuilder;
@@ -34,10 +35,7 @@ class MultiplePayoutsProvider extends ChangeNotifier {
}) : _csvParser = csvParser ?? MultipleCsvParser(), }) : _csvParser = csvParser ?? MultipleCsvParser(),
_intentBuilder = intentBuilder ?? MultipleIntentBuilder(); _intentBuilder = intentBuilder ?? MultipleIntentBuilder();
void update( void update(MultiQuotationProvider quotation, MultiPaymentProvider payment) {
MultiQuotationProvider quotation,
MultiPaymentProvider payment,
) {
_bindQuotation(quotation); _bindQuotation(quotation);
_payment = payment; _payment = payment;
} }
@@ -60,7 +58,9 @@ class MultiplePayoutsProvider extends ChangeNotifier {
if (quotation.isLoading) return QuoteStatusType.loading; if (quotation.isLoading) return QuoteStatusType.loading;
if (quotation.error != null) return QuoteStatusType.error; if (quotation.error != null) return QuoteStatusType.error;
if (quotation.quotation == null) return QuoteStatusType.missing; if (quotation.quotation == null) return QuoteStatusType.missing;
if (_isQuoteExpired(quotation.quoteExpiresAt)) return QuoteStatusType.expired; if (_isQuoteExpired(quotation.quoteExpiresAt)) {
return QuoteStatusType.expired;
}
return QuoteStatusType.active; return QuoteStatusType.active;
} }
@@ -78,10 +78,10 @@ class MultiplePayoutsProvider extends ChangeNotifier {
Money? aggregateDebitAmountFor(Wallet? sourceWallet) { Money? aggregateDebitAmountFor(Wallet? sourceWallet) {
if (_rows.isEmpty) return null; if (_rows.isEmpty) return null;
return _moneyForSourceCurrency( final totals = aggregateMoneyByCurrency(
_quotation?.quotation?.aggregate?.debitAmounts, _quoteItems().map((quote) => quote.amounts?.sourceDebitTotal),
sourceWallet,
); );
return _moneyForSourceCurrency(totals, sourceWallet);
} }
Money? get requestedSentAmount { Money? get requestedSentAmount {
@@ -99,18 +99,16 @@ class MultiplePayoutsProvider extends ChangeNotifier {
Money? aggregateSettlementAmountFor(Wallet? sourceWallet) { Money? aggregateSettlementAmountFor(Wallet? sourceWallet) {
if (_rows.isEmpty) return null; if (_rows.isEmpty) return null;
return _moneyForSourceCurrency( final totals = aggregateMoneyByCurrency(
_quotation?.quotation?.aggregate?.expectedSettlementAmounts, _quoteItems().map((quote) => quote.amounts?.destinationSettlement),
sourceWallet,
); );
return _moneyForSourceCurrency(totals, sourceWallet);
} }
Money? aggregateFeeAmountFor(Wallet? sourceWallet) { Money? aggregateFeeAmountFor(Wallet? sourceWallet) {
if (_rows.isEmpty) return null; if (_rows.isEmpty) return null;
return _moneyForSourceCurrency( final totals = aggregateMoneyByCurrency(_quoteItems().map(quoteFeeTotal));
_quotation?.quotation?.aggregate?.expectedFeeTotals, return _moneyForSourceCurrency(totals, sourceWallet);
sourceWallet,
);
} }
double? aggregateFeePercentFor(Wallet? sourceWallet) { double? aggregateFeePercentFor(Wallet? sourceWallet) {
@@ -256,10 +254,7 @@ class MultiplePayoutsProvider extends ChangeNotifier {
}; };
} }
Money? _moneyForSourceCurrency( Money? _moneyForSourceCurrency(List<Money>? values, Wallet? sourceWallet) {
List<Money>? values,
Wallet? sourceWallet,
) {
if (values == null || values.isEmpty) return null; if (values == null || values.isEmpty) return null;
if (sourceWallet != null) { if (sourceWallet != null) {
@@ -274,6 +269,9 @@ class MultiplePayoutsProvider extends ChangeNotifier {
return values.first; return values.first;
} }
List<PaymentQuote> _quoteItems() =>
_quotation?.quotation?.items ?? const <PaymentQuote>[];
@override @override
void dispose() { void dispose() {
_quotation?.removeListener(_onQuotationChanged); _quotation?.removeListener(_onQuotationChanged);

View File

@@ -5,10 +5,9 @@ import 'package:pshared/utils/money.dart';
import 'package:pweb/models/payment/payment_state.dart'; import 'package:pweb/models/payment/payment_state.dart';
OperationItem mapPaymentToOperation(Payment payment) { OperationItem mapPaymentToOperation(Payment payment) {
final debit = payment.lastQuote?.debitAmount; final debit = payment.lastQuote?.amounts?.sourceDebitTotal;
final settlement = payment.lastQuote?.expectedSettlementAmount; final settlement = payment.lastQuote?.amounts?.destinationSettlement;
final amountMoney = debit ?? settlement; final amountMoney = debit ?? settlement;
final amount = parseMoneyAmount(amountMoney?.amount); final amount = parseMoneyAmount(amountMoney?.amount);
@@ -18,18 +17,17 @@ OperationItem mapPaymentToOperation(Payment payment) {
: parseMoneyAmount(settlement.amount); : parseMoneyAmount(settlement.amount);
final toCurrency = settlement?.currency ?? currency; final toCurrency = settlement?.currency ?? currency;
final payId = _firstNonEmpty([ final payId =
payment.paymentRef, _firstNonEmpty([payment.paymentRef, payment.idempotencyKey]) ?? '-';
payment.idempotencyKey, final name =
]) ?? _firstNonEmpty([
'-';
final name = _firstNonEmpty([
payment.lastQuote?.quoteRef, payment.lastQuote?.quoteRef,
payment.paymentRef, payment.paymentRef,
payment.idempotencyKey, payment.idempotencyKey,
]) ?? ]) ??
'-'; '-';
final comment = _firstNonEmpty([ final comment =
_firstNonEmpty([
payment.failureReason, payment.failureReason,
payment.failureCode, payment.failureCode,
payment.state, payment.state,
@@ -72,17 +70,17 @@ DateTime resolvePaymentDate(Payment payment) {
final expiresAt = payment.lastQuote?.fxQuote?.expiresAtUnixMs; final expiresAt = payment.lastQuote?.fxQuote?.expiresAtUnixMs;
if (expiresAt != null && expiresAt > 0) { if (expiresAt != null && expiresAt > 0) {
return DateTime.fromMillisecondsSinceEpoch(expiresAt, isUtc: true).toLocal(); return DateTime.fromMillisecondsSinceEpoch(
expiresAt,
isUtc: true,
).toLocal();
} }
return DateTime.fromMillisecondsSinceEpoch(0); return DateTime.fromMillisecondsSinceEpoch(0);
} }
String? paymentIdFromOperation(OperationItem operation) { String? paymentIdFromOperation(OperationItem operation) {
final candidates = [ final candidates = [operation.paymentRef, operation.payId];
operation.paymentRef,
operation.payId,
];
for (final candidate in candidates) { for (final candidate in candidates) {
final trimmed = candidate?.trim(); final trimmed = candidate?.trim();
if (trimmed != null && trimmed.isNotEmpty && trimmed != '-') { if (trimmed != null && trimmed.isNotEmpty && trimmed != '-') {