WIP: integration with ledger

This commit is contained in:
Arseni
2026-02-04 02:01:22 +03:00
parent f1f16a30e6
commit f44ef56ff3
32 changed files with 1226 additions and 405 deletions

View File

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

View File

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

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