Quotation
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:pshared/models/payment/currency_pair.dart';
|
||||
import 'package:pshared/models/payment/customer.dart';
|
||||
import 'package:pshared/models/payment/fx/intent.dart';
|
||||
import 'package:pshared/models/payment/fx/side.dart';
|
||||
import 'package:pshared/models/payment/kind.dart';
|
||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||
import 'package:pshared/models/payment/methods/type.dart';
|
||||
import 'package:pshared/models/payment/money.dart';
|
||||
import 'package:pshared/models/payment/settlement_mode.dart';
|
||||
import 'package:pshared/models/payment/intent.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/provider/payment/amount.dart';
|
||||
import 'package:pshared/provider/payment/flow.dart';
|
||||
import 'package:pshared/provider/payment/wallets.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
|
||||
|
||||
class QuotationIntentBuilder {
|
||||
PaymentIntent? build({
|
||||
required PaymentAmountProvider payment,
|
||||
required WalletsProvider wallets,
|
||||
required PaymentFlowProvider flow,
|
||||
required RecipientsProvider recipients,
|
||||
required PaymentMethodsProvider methods,
|
||||
}) {
|
||||
final selectedWallet = wallets.selectedWallet;
|
||||
final method = methods.methods.firstWhereOrNull((m) => m.type == flow.selectedType);
|
||||
if (selectedWallet == null || method == null) return null;
|
||||
|
||||
final customer = _buildCustomer(
|
||||
recipient: recipients.currentObject,
|
||||
method: method,
|
||||
);
|
||||
final amount = Money(
|
||||
amount: payment.amount.toString(),
|
||||
// TODO: adapt to possible other sources
|
||||
currency: currencyCodeToString(selectedWallet.currency),
|
||||
);
|
||||
final fxIntent = FxIntent(
|
||||
pair: CurrencyPair(
|
||||
base: currencyCodeToString(selectedWallet.currency),
|
||||
quote: 'RUB', // TODO: exentd target currencies
|
||||
),
|
||||
side: FxSide.sellBaseBuyQuote,
|
||||
);
|
||||
return PaymentIntent(
|
||||
kind: PaymentKind.payout,
|
||||
amount: amount,
|
||||
destination: method.data,
|
||||
source: ManagedWalletPaymentMethod(
|
||||
managedWalletRef: selectedWallet.id,
|
||||
),
|
||||
fx: fxIntent,
|
||||
settlementMode: payment.payerCoversFee ? SettlementMode.fixReceived : SettlementMode.fixSource,
|
||||
settlementCurrency: _resolveSettlementCurrency(amount: amount, fx: fxIntent),
|
||||
customer: customer,
|
||||
);
|
||||
}
|
||||
|
||||
String _resolveSettlementCurrency({
|
||||
required Money amount,
|
||||
required FxIntent? fx,
|
||||
}) {
|
||||
final pair = fx?.pair;
|
||||
if (pair != null) {
|
||||
switch (fx?.side ?? FxSide.unspecified) {
|
||||
case FxSide.buyBaseSellQuote:
|
||||
if (pair.base.isNotEmpty) return pair.base;
|
||||
break;
|
||||
case FxSide.sellBaseBuyQuote:
|
||||
if (pair.quote.isNotEmpty) return pair.quote;
|
||||
break;
|
||||
case FxSide.unspecified:
|
||||
break;
|
||||
}
|
||||
if (amount.currency == pair.base && pair.quote.isNotEmpty) return pair.quote;
|
||||
if (amount.currency == pair.quote && pair.base.isNotEmpty) return pair.base;
|
||||
if (pair.quote.isNotEmpty) return pair.quote;
|
||||
if (pair.base.isNotEmpty) return pair.base;
|
||||
}
|
||||
return amount.currency;
|
||||
}
|
||||
|
||||
Customer _buildCustomer({
|
||||
required Recipient? recipient,
|
||||
required PaymentMethod method,
|
||||
}) {
|
||||
final name = _resolveCustomerName(method, recipient);
|
||||
String? firstName;
|
||||
String? middleName;
|
||||
String? lastName;
|
||||
|
||||
if (name != null && name.isNotEmpty) {
|
||||
final parts = name.split(RegExp(r'\s+'));
|
||||
if (parts.isNotEmpty) {
|
||||
firstName = parts.first;
|
||||
}
|
||||
if (parts.length == 2) {
|
||||
lastName = parts.last;
|
||||
} else if (parts.length > 2) {
|
||||
lastName = parts.last;
|
||||
middleName = parts.sublist(1, parts.length - 1).join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return Customer(
|
||||
id: recipient?.id ?? method.recipientRef,
|
||||
firstName: firstName,
|
||||
middleName: middleName,
|
||||
lastName: lastName,
|
||||
country: method.cardData?.country,
|
||||
);
|
||||
}
|
||||
|
||||
String? _resolveCustomerName(PaymentMethod method, Recipient? recipient) {
|
||||
final card = method.cardData;
|
||||
if (card != null) {
|
||||
return '${card.firstName} ${card.lastName}'.trim();
|
||||
}
|
||||
|
||||
final iban = method.ibanData;
|
||||
if (iban != null && iban.accountHolder.trim().isNotEmpty) {
|
||||
return iban.accountHolder.trim();
|
||||
}
|
||||
|
||||
final bank = method.bankAccountData;
|
||||
if (bank != null && bank.recipientName.trim().isNotEmpty) {
|
||||
return bank.recipientName.trim();
|
||||
}
|
||||
|
||||
final recipientName = recipient?.name.trim();
|
||||
return recipientName?.isNotEmpty == true ? recipientName : null;
|
||||
}
|
||||
}
|
||||
126
frontend/pshared/lib/provider/payment/quotation/quotation.dart
Normal file
126
frontend/pshared/lib/provider/payment/quotation/quotation.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'package:pshared/api/requests/payment/quote.dart';
|
||||
import 'package:pshared/data/mapper/payment/intent/payment.dart';
|
||||
import 'package:pshared/models/asset.dart';
|
||||
import 'package:pshared/models/payment/intent.dart';
|
||||
import 'package:pshared/models/payment/quote/quote.dart';
|
||||
import 'package:pshared/models/payment/money.dart';
|
||||
import 'package:pshared/provider/organizations.dart';
|
||||
import 'package:pshared/provider/payment/amount.dart';
|
||||
import 'package:pshared/provider/payment/flow.dart';
|
||||
import 'package:pshared/provider/payment/wallets.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
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/exception.dart';
|
||||
|
||||
|
||||
class QuotationProvider extends ChangeNotifier {
|
||||
static final _logger = Logger('provider.payment.quotation');
|
||||
Resource<PaymentQuote> _quotation = Resource(data: null, isLoading: false, error: null);
|
||||
late OrganizationsProvider _organizations;
|
||||
bool _isLoaded = false;
|
||||
PaymentIntent? _lastIntent;
|
||||
final QuotationIntentBuilder _intentBuilder = QuotationIntentBuilder();
|
||||
|
||||
void update(
|
||||
OrganizationsProvider venue,
|
||||
PaymentAmountProvider payment,
|
||||
WalletsProvider wallets,
|
||||
PaymentFlowProvider flow,
|
||||
RecipientsProvider recipients,
|
||||
PaymentMethodsProvider methods,
|
||||
) {
|
||||
_organizations = venue;
|
||||
final intent = _intentBuilder.build(
|
||||
payment: payment,
|
||||
wallets: wallets,
|
||||
flow: flow,
|
||||
recipients: recipients,
|
||||
methods: methods,
|
||||
);
|
||||
if (intent == null) return;
|
||||
final intentKey = _buildIntentKey(intent);
|
||||
final lastIntent = _lastIntent;
|
||||
if (lastIntent != null && intentKey == _buildIntentKey(lastIntent)) return;
|
||||
getQuotation(intent, idempotencyKey: intentKey);
|
||||
}
|
||||
|
||||
PaymentQuote? get quotation => _quotation.data;
|
||||
bool get isLoading => _quotation.isLoading;
|
||||
Exception? get error => _quotation.error;
|
||||
bool get canRefresh => _lastIntent != null;
|
||||
bool get isReady => _isLoaded && !_quotation.isLoading && _quotation.error == null;
|
||||
|
||||
DateTime? get quoteExpiresAt {
|
||||
final expiresAtUnixMs = quotation?.fxQuote?.expiresAtUnixMs;
|
||||
if (expiresAtUnixMs == null) return null;
|
||||
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? _assetFromMoney(Money? money) {
|
||||
if (money == null) return null;
|
||||
return createAsset(money.currency, money.amount);
|
||||
}
|
||||
|
||||
void _setResource(Resource<PaymentQuote> quotation) {
|
||||
_quotation = quotation;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<PaymentQuote?> refreshQuotation() async {
|
||||
final intent = _lastIntent;
|
||||
if (intent == null) return null;
|
||||
return getQuotation(intent, idempotencyKey: _buildIntentKey(intent));
|
||||
}
|
||||
|
||||
Future<PaymentQuote?> getQuotation(PaymentIntent intent, {String? idempotencyKey}) async {
|
||||
if (!_organizations.isOrganizationSet) throw StateError('Organization is not set');
|
||||
_lastIntent = intent;
|
||||
final intentKey = idempotencyKey ?? _buildIntentKey(intent);
|
||||
try {
|
||||
_setResource(_quotation.copyWith(isLoading: true, error: null));
|
||||
final response = await QuotationService.getQuotation(
|
||||
_organizations.current.id,
|
||||
QuotePaymentRequest(
|
||||
idempotencyKey: intentKey,
|
||||
intent: intent.toDTO(),
|
||||
),
|
||||
);
|
||||
_isLoaded = true;
|
||||
_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,
|
||||
));
|
||||
}
|
||||
return _quotation.data;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_isLoaded = false;
|
||||
_lastIntent = null;
|
||||
_setResource(Resource(data: null, isLoading: false, error: null));
|
||||
}
|
||||
|
||||
String _buildIntentKey(PaymentIntent intent) {
|
||||
final payload = jsonEncode(intent.toDTO().toJson());
|
||||
return Uuid().v5(Namespace.url.value, 'quote:$payload');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user