import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:pshared/controllers/payment/source.dart'; import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/asset.dart'; import 'package:pshared/models/payment/chain_network.dart'; import 'package:pshared/models/payment/methods/data.dart'; import 'package:pshared/models/payment/methods/ledger.dart'; import 'package:pshared/models/payment/methods/managed_wallet.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/quote/status_type.dart'; import 'package:pweb/models/payment/multiple_payouts/csv_row.dart'; import 'package:pweb/models/payment/multiple_payouts/state.dart'; import 'package:pweb/providers/multiple_payouts.dart'; import 'package:pweb/services/payments/csv_input.dart'; class MultiplePayoutsController extends ChangeNotifier { final CsvInputService _csvInput; MultiplePayoutsProvider? _provider; PaymentSourceController? _sourceController; _PickState _pickState = _PickState.idle; Exception? _uiError; String? _lastSourceKey; MultiplePayoutsController({required CsvInputService csvInput}) : _csvInput = csvInput; void update( MultiplePayoutsProvider provider, PaymentSourceController sourceController, ) { var shouldNotify = false; if (!identical(_provider, provider)) { _provider?.removeListener(_onProviderChanged); _provider = provider; _provider?.addListener(_onProviderChanged); shouldNotify = true; } if (!identical(_sourceController, sourceController)) { _sourceController?.removeListener(_onSourceChanged); _sourceController = sourceController; _sourceController?.addListener(_onSourceChanged); _lastSourceKey = _currentSourceKey; shouldNotify = true; } if (shouldNotify) { notifyListeners(); } } MultiplePayoutsState get state => _provider?.state ?? MultiplePayoutsState.idle; String? get selectedFileName => _provider?.selectedFileName; List get rows => _provider?.rows ?? const []; int get sentCount => _provider?.sentCount ?? 0; Exception? get error => _uiError ?? _provider?.error; bool get isQuoting => _provider?.isQuoting ?? false; bool get isSending => _provider?.isSending ?? false; bool get isBusy => _provider?.isBusy ?? false; bool get quoteIsLoading => _provider?.quoteIsLoading ?? false; QuoteStatusType get quoteStatusType => _provider?.quoteStatusType ?? QuoteStatusType.missing; Duration? get quoteTimeLeft => _provider?.quoteTimeLeft; bool get canSend => (_provider?.canSend ?? false) && _selectedSource != null; Money? get aggregateDebitAmount => _provider?.aggregateDebitAmountForCurrency(_selectedSourceCurrencyCode); Money? get requestedSentAmount => _provider?.requestedSentAmount; Money? get aggregateSettlementAmount => _provider ?.aggregateSettlementAmountForCurrency(_selectedSourceCurrencyCode); Money? get aggregateFeeAmount => _provider?.aggregateFeeAmountForCurrency(_selectedSourceCurrencyCode); double? get aggregateFeePercent => _provider?.aggregateFeePercentForCurrency(_selectedSourceCurrencyCode); Future pickAndQuote() async { if (_pickState == _PickState.picking) return; final provider = _provider; if (provider == null) { _setUiError(StateError('Multiple payouts provider is not ready')); return; } _clearUiError(); _pickState = _PickState.picking; try { final picked = await _csvInput.pickCsv(); if (picked == null) return; final source = _selectedSource; if (source == null) { _setUiError(StateError('Select source of funds first')); return; } await provider.quoteFromCsv( fileName: picked.name, content: picked.content, sourceMethod: source.method, sourceCurrencyCode: source.currencyCode, ); } catch (e) { _setUiError(e); } finally { _pickState = _PickState.idle; } } Future> send() async { return _provider?.send() ?? const []; } Future sendAndGetOutcome() async { _clearUiError(); final provider = _provider; if (provider == null) { _setUiError(StateError('Multiple payouts provider is not ready')); return MultiplePayoutSendOutcome.failure; } final payments = await provider.send(); final hasError = provider.error != null; if (hasError || payments.isEmpty) { return MultiplePayoutSendOutcome.failure; } return MultiplePayoutSendOutcome.success; } void removeUploadedFile() { _provider?.removeUploadedFile(); _clearUiError(notify: false); } void _onProviderChanged() { notifyListeners(); } void _onSourceChanged() { final currentSourceKey = _currentSourceKey; final sourceChanged = currentSourceKey != _lastSourceKey; _lastSourceKey = currentSourceKey; if (sourceChanged) { unawaited(_requoteWithUploadedRows()); } notifyListeners(); } String? get _selectedSourceCurrencyCode => _sourceController?.selectedCurrencyCode; String? get _currentSourceKey { final source = _sourceController; if (source == null || source.selectedType == null || source.selectedRef == null) { return null; } return '${source.selectedType!.name}:${source.selectedRef!}'; } ({PaymentMethodData method, String currencyCode})? get _selectedSource { final source = _sourceController; if (source == null) return null; final currencyCode = source.selectedCurrencyCode; if (currencyCode == null || currencyCode.isEmpty) return null; final wallet = source.selectedWallet; if (wallet != null) { final hasAsset = (wallet.tokenSymbol ?? '').isNotEmpty; final asset = hasAsset ? PaymentAsset( chain: wallet.network ?? ChainNetwork.unspecified, tokenSymbol: wallet.tokenSymbol!, contractAddress: wallet.contractAddress, ) : null; return ( method: ManagedWalletPaymentMethod( managedWalletRef: wallet.id, asset: asset, ), currencyCode: currencyCode, ); } final ledger = source.selectedLedgerAccount; if (ledger != null) { return ( method: LedgerPaymentMethod(ledgerAccountRef: ledger.ledgerAccountRef), currencyCode: currencyCode, ); } return null; } Future _requoteWithUploadedRows() async { final provider = _provider; if (provider == null) return; if (provider.selectedFileName == null || provider.rows.isEmpty) return; final source = _selectedSource; if (source == null) return; _clearUiError(notify: false); await provider.requoteUploadedRows( sourceMethod: source.method, sourceCurrencyCode: source.currencyCode, ); } void _setUiError(Object error) { _uiError = error is Exception ? error : Exception(error.toString()); notifyListeners(); } void _clearUiError({bool notify = true}) { if (_uiError == null) return; _uiError = null; if (notify) { notifyListeners(); } } @override void dispose() { _provider?.removeListener(_onProviderChanged); _sourceController?.removeListener(_onSourceChanged); super.dispose(); } } enum _PickState { idle, picking } enum MultiplePayoutSendOutcome { success, failure }