WIP: integration with ledger
This commit is contained in:
67
frontend/pshared/lib/controllers/payment/source.dart
Normal file
67
frontend/pshared/lib/controllers/payment/source.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:pshared/models/payment/source.dart';
|
||||
import 'package:pshared/provider/payment/source.dart';
|
||||
|
||||
|
||||
class PaymentSourceController extends ChangeNotifier {
|
||||
PaymentSourceProvider? _provider;
|
||||
String? _selectedSourceKey;
|
||||
|
||||
List<PaymentSource> get sources => _provider?.sources ?? const [];
|
||||
|
||||
PaymentSource? get selectedSource {
|
||||
final key = _selectedSourceKey;
|
||||
if (key == null) return null;
|
||||
return sources.firstWhereOrNull((source) => source.key == key);
|
||||
}
|
||||
|
||||
void update(PaymentSourceProvider provider) {
|
||||
_provider = provider;
|
||||
final nextSources = provider.sources;
|
||||
final nextSelectedKey = _resolveSelectedKey(
|
||||
currentKey: _selectedSourceKey,
|
||||
sources: nextSources,
|
||||
);
|
||||
|
||||
if (nextSelectedKey == _selectedSourceKey) return;
|
||||
_selectedSourceKey = nextSelectedKey;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void selectSource(PaymentSource source) {
|
||||
if (_selectedSourceKey == source.key) return;
|
||||
_selectedSourceKey = source.key;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void selectWalletByRef(String walletRef) {
|
||||
final source = sources.firstWhereOrNull(
|
||||
(s) => s.type == PaymentSourceType.wallet && s.id == walletRef,
|
||||
);
|
||||
if (source == null) return;
|
||||
selectSource(source);
|
||||
}
|
||||
|
||||
void selectLedgerByRef(String ledgerAccountRef) {
|
||||
final source = sources.firstWhereOrNull(
|
||||
(s) => s.type == PaymentSourceType.ledger && s.id == ledgerAccountRef,
|
||||
);
|
||||
if (source == null) return;
|
||||
selectSource(source);
|
||||
}
|
||||
|
||||
String? _resolveSelectedKey({
|
||||
required String? currentKey,
|
||||
required List<PaymentSource> sources,
|
||||
}) {
|
||||
if (sources.isEmpty) return null;
|
||||
if (currentKey != null &&
|
||||
sources.any((source) => source.key == currentKey)) {
|
||||
return currentKey;
|
||||
}
|
||||
return sources.first.key;
|
||||
}
|
||||
}
|
||||
31
frontend/pshared/lib/models/payment/source.dart
Normal file
31
frontend/pshared/lib/models/payment/source.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
|
||||
|
||||
enum PaymentSourceType { wallet, ledger }
|
||||
|
||||
class PaymentSource {
|
||||
final PaymentSourceType type;
|
||||
final Wallet? wallet;
|
||||
final LedgerAccount? ledgerAccount;
|
||||
|
||||
const PaymentSource._({required this.type, this.wallet, this.ledgerAccount});
|
||||
|
||||
const PaymentSource.wallet(Wallet wallet)
|
||||
: this._(type: PaymentSourceType.wallet, wallet: wallet);
|
||||
|
||||
const PaymentSource.ledger(LedgerAccount account)
|
||||
: this._(type: PaymentSourceType.ledger, ledgerAccount: account);
|
||||
|
||||
String get id => switch (type) {
|
||||
PaymentSourceType.wallet => wallet!.id,
|
||||
PaymentSourceType.ledger => ledgerAccount!.ledgerAccountRef,
|
||||
};
|
||||
|
||||
String get key => '${type.name}:$id';
|
||||
|
||||
String get name => switch (type) {
|
||||
PaymentSourceType.wallet => wallet!.name,
|
||||
PaymentSourceType.ledger => ledgerAccount!.name,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/models/payment/asset.dart';
|
||||
import 'package:pshared/models/payment/chain_network.dart';
|
||||
import 'package:pshared/models/payment/currency_pair.dart';
|
||||
@@ -9,66 +8,109 @@ import 'package:pshared/models/payment/kind.dart';
|
||||
import 'package:pshared/models/payment/methods/card.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/methods/iban.dart';
|
||||
import 'package:pshared/models/payment/methods/ledger.dart';
|
||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
||||
import 'package:pshared/models/payment/source.dart';
|
||||
import 'package:pshared/models/payment/methods/type.dart';
|
||||
import 'package:pshared/models/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/controllers/payment/source.dart';
|
||||
import 'package:pshared/provider/payment/amount.dart';
|
||||
import 'package:pshared/provider/payment/flow.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
|
||||
|
||||
class QuotationIntentBuilder {
|
||||
PaymentIntent? build({
|
||||
required PaymentAmountProvider payment,
|
||||
required WalletsController wallets,
|
||||
required PaymentSourceController sources,
|
||||
required PaymentFlowProvider flow,
|
||||
required RecipientsProvider recipients,
|
||||
}) {
|
||||
final selectedWallet = wallets.selectedWallet;
|
||||
final selectedSource = sources.selectedSource;
|
||||
final paymentData = flow.selectedPaymentData;
|
||||
final selectedMethod = flow.selectedMethod;
|
||||
if (selectedWallet == null || paymentData == null) return null;
|
||||
if (selectedSource == null || paymentData == null) return null;
|
||||
|
||||
final customer = _buildCustomer(
|
||||
recipient: recipients.currentObject,
|
||||
method: selectedMethod,
|
||||
data: paymentData,
|
||||
);
|
||||
final sourceCurrency = _resolveSourceCurrency(selectedSource);
|
||||
if (sourceCurrency == null) return null;
|
||||
final targetCurrency = _resolveTargetCurrency(paymentData);
|
||||
final fxIntent = _buildFxIntent(
|
||||
baseCurrency: sourceCurrency,
|
||||
quoteCurrency: targetCurrency,
|
||||
);
|
||||
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,
|
||||
currency: sourceCurrency,
|
||||
);
|
||||
return PaymentIntent(
|
||||
kind: PaymentKind.payout,
|
||||
amount: amount,
|
||||
destination: paymentData,
|
||||
source: ManagedWalletPaymentMethod(
|
||||
managedWalletRef: selectedWallet.id,
|
||||
asset: PaymentAsset(
|
||||
tokenSymbol: selectedWallet.tokenSymbol ?? '',
|
||||
chain: selectedWallet.network ?? ChainNetwork.unspecified,
|
||||
)
|
||||
),
|
||||
source: _buildSourceEndpoint(selectedSource),
|
||||
fx: fxIntent,
|
||||
settlementMode: payment.payerCoversFee ? SettlementMode.fixReceived : SettlementMode.fixSource,
|
||||
settlementCurrency: _resolveSettlementCurrency(amount: amount, fx: fxIntent),
|
||||
settlementMode: payment.payerCoversFee
|
||||
? SettlementMode.fixReceived
|
||||
: SettlementMode.fixSource,
|
||||
settlementCurrency: _resolveSettlementCurrency(
|
||||
amount: amount,
|
||||
fx: fxIntent,
|
||||
),
|
||||
customer: customer,
|
||||
);
|
||||
}
|
||||
|
||||
String _resolveTargetCurrency(PaymentMethodData destination) {
|
||||
// Current payout flow is RUB-settlement oriented.
|
||||
// Avoid requesting unsupported self-pairs (e.g. RUB/RUB).
|
||||
return 'RUB';
|
||||
}
|
||||
|
||||
FxIntent? _buildFxIntent({
|
||||
required String baseCurrency,
|
||||
required String quoteCurrency,
|
||||
}) {
|
||||
final base = baseCurrency.trim().toUpperCase();
|
||||
final quote = quoteCurrency.trim().toUpperCase();
|
||||
if (base.isEmpty || quote.isEmpty || base == quote) {
|
||||
return null;
|
||||
}
|
||||
return FxIntent(
|
||||
pair: CurrencyPair(base: base, quote: quote),
|
||||
side: FxSide.sellBaseBuyQuote,
|
||||
);
|
||||
}
|
||||
|
||||
String? _resolveSourceCurrency(PaymentSource source) {
|
||||
return switch (source.type) {
|
||||
PaymentSourceType.wallet => currencyCodeToString(source.wallet!.currency),
|
||||
PaymentSourceType.ledger => source.ledgerAccount?.currency,
|
||||
};
|
||||
}
|
||||
|
||||
PaymentMethodData _buildSourceEndpoint(PaymentSource source) {
|
||||
return switch (source.type) {
|
||||
PaymentSourceType.wallet => ManagedWalletPaymentMethod(
|
||||
managedWalletRef: source.wallet!.id,
|
||||
asset: PaymentAsset(
|
||||
tokenSymbol: source.wallet?.tokenSymbol ?? '',
|
||||
chain: source.wallet?.network ?? ChainNetwork.unspecified,
|
||||
),
|
||||
),
|
||||
PaymentSourceType.ledger => LedgerPaymentMethod(
|
||||
ledgerAccountRef: source.ledgerAccount!.ledgerAccountRef,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
String _resolveSettlementCurrency({
|
||||
required Money amount,
|
||||
required FxIntent? fx,
|
||||
@@ -85,8 +127,12 @@ class QuotationIntentBuilder {
|
||||
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 (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;
|
||||
}
|
||||
@@ -111,8 +157,9 @@ class QuotationIntentBuilder {
|
||||
: name.trim().split(RegExp(r'\s+'));
|
||||
final firstName = parts.isNotEmpty ? parts.first : null;
|
||||
final lastName = parts.length >= 2 ? parts.last : null;
|
||||
final middleName =
|
||||
parts.length > 2 ? parts.sublist(1, parts.length - 1).join(' ') : null;
|
||||
final middleName = parts.length > 2
|
||||
? parts.sublist(1, parts.length - 1).join(' ')
|
||||
: null;
|
||||
|
||||
return Customer(
|
||||
id: id,
|
||||
@@ -139,7 +186,9 @@ class QuotationIntentBuilder {
|
||||
return iban.accountHolder.trim();
|
||||
}
|
||||
|
||||
final bank = method?.bankAccountData ?? (data is RussianBankAccountPaymentMethod ? data : null);
|
||||
final bank =
|
||||
method?.bankAccountData ??
|
||||
(data is RussianBankAccountPaymentMethod ? data : null);
|
||||
if (bank != null && bank.recipientName.trim().isNotEmpty) {
|
||||
return bank.recipientName.trim();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:logging/logging.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'package:pshared/api/requests/payment/quote.dart';
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/controllers/payment/source.dart';
|
||||
import 'package:pshared/data/mapper/payment/intent/payment.dart';
|
||||
import 'package:pshared/models/asset.dart';
|
||||
import 'package:pshared/models/payment/intent.dart';
|
||||
@@ -23,19 +23,22 @@ 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);
|
||||
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,
|
||||
OrganizationsProvider venue,
|
||||
PaymentAmountProvider payment,
|
||||
WalletsController wallets,
|
||||
PaymentSourceController sources,
|
||||
PaymentFlowProvider flow,
|
||||
RecipientsProvider recipients,
|
||||
PaymentMethodsProvider _,
|
||||
@@ -43,7 +46,7 @@ class QuotationProvider extends ChangeNotifier {
|
||||
_organizations = venue;
|
||||
final intent = _intentBuilder.build(
|
||||
payment: payment,
|
||||
wallets: wallets,
|
||||
sources: sources,
|
||||
flow: flow,
|
||||
recipients: recipients,
|
||||
);
|
||||
@@ -58,7 +61,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;
|
||||
|
||||
DateTime? get quoteExpiresAt {
|
||||
final expiresAtUnixMs = quotation?.fxQuote?.expiresAtUnixMs;
|
||||
@@ -66,10 +70,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 recipientGets =>
|
||||
_assetFromMoney(quotation?.expectedSettlementAmount);
|
||||
|
||||
Asset? _assetFromMoney(Money? money) {
|
||||
if (money == null) return null;
|
||||
@@ -88,26 +92,32 @@ 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));
|
||||
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;
|
||||
}
|
||||
|
||||
33
frontend/pshared/lib/provider/payment/source.dart
Normal file
33
frontend/pshared/lib/provider/payment/source.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/models/payment/source.dart';
|
||||
import 'package:pshared/provider/ledger.dart';
|
||||
import 'package:pshared/provider/payment/wallets.dart';
|
||||
|
||||
class PaymentSourceProvider extends ChangeNotifier {
|
||||
List<PaymentSource> _sources = const [];
|
||||
|
||||
List<PaymentSource> get sources => _sources;
|
||||
|
||||
void update(
|
||||
WalletsProvider walletsProvider,
|
||||
LedgerAccountsProvider ledgerProvider,
|
||||
) {
|
||||
final nextSources = <PaymentSource>[
|
||||
...walletsProvider.wallets.map(PaymentSource.wallet),
|
||||
...ledgerProvider.accounts.map(PaymentSource.ledger),
|
||||
];
|
||||
|
||||
final currentKeys = _sources
|
||||
.map((source) => source.key)
|
||||
.toList(growable: false);
|
||||
final nextKeys = nextSources
|
||||
.map((source) => source.key)
|
||||
.toList(growable: false);
|
||||
|
||||
if (listEquals(currentKeys, nextKeys)) return;
|
||||
|
||||
_sources = nextSources;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user