import 'package:flutter/foundation.dart'; import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/quote/status_type.dart'; import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/provider/payment/multiple/provider.dart'; import 'package:pshared/provider/payment/multiple/quotation.dart'; import 'package:pshared/provider/payment/payments.dart'; import 'package:pshared/utils/currency.dart'; import 'package:pweb/models/multiple_payouts/csv_row.dart'; import 'package:pweb/models/multiple_payouts/state.dart'; import 'package:pweb/utils/payment/multiple_csv_parser.dart'; import 'package:pweb/utils/payment/multiple_intent_builder.dart'; class MultiplePayoutsProvider extends ChangeNotifier { final MultipleCsvParser _csvParser; final MultipleIntentBuilder _intentBuilder; MultiQuotationProvider? _quotation; MultiPaymentProvider? _payment; PaymentsProvider? _payments; MultiplePayoutsState _state = MultiplePayoutsState.idle; String? _selectedFileName; List _rows = const []; int _sentCount = 0; Exception? _error; MultiplePayoutsProvider({ MultipleCsvParser? csvParser, MultipleIntentBuilder? intentBuilder, }) : _csvParser = csvParser ?? MultipleCsvParser(), _intentBuilder = intentBuilder ?? MultipleIntentBuilder(); void update( MultiQuotationProvider quotation, MultiPaymentProvider payment, PaymentsProvider payments, ) { _bindQuotation(quotation); _payment = payment; _payments = payments; } MultiplePayoutsState get state => _state; String? get selectedFileName => _selectedFileName; List get rows => List.unmodifiable(_rows); int get sentCount => _sentCount; Exception? get error => _error; bool get isQuoting => _state == MultiplePayoutsState.quoting; bool get isSending => _state == MultiplePayoutsState.sending; bool get isBusy => isQuoting || isSending; bool get quoteIsLoading => _quotation?.isLoading ?? false; QuoteStatusType get quoteStatusType { final quotation = _quotation; if (quotation == null) return QuoteStatusType.missing; if (quotation.isLoading) return QuoteStatusType.loading; if (quotation.error != null) return QuoteStatusType.error; if (quotation.quotation == null) return QuoteStatusType.missing; if (_isQuoteExpired(quotation.quoteExpiresAt)) return QuoteStatusType.expired; return QuoteStatusType.active; } Duration? get quoteTimeLeft { final expiresAt = _quotation?.quoteExpiresAt; if (expiresAt == null) return null; return expiresAt.difference(DateTime.now().toUtc()); } bool get canSend { if (isBusy || _rows.isEmpty) return false; final quoteRef = _quotation?.quotation?.quoteRef; return quoteRef != null && quoteRef.isNotEmpty; } Money? aggregateDebitAmountFor(Wallet? sourceWallet) { if (_rows.isEmpty) return null; return _moneyForSourceCurrency( _quotation?.quotation?.aggregate?.debitAmounts, sourceWallet, ); } Money? get requestedSentAmount { if (_rows.isEmpty) return null; const currency = 'RUB'; double total = 0; for (final row in _rows) { final value = double.tryParse(row.amount); if (value == null) return null; total += value; } return Money(amount: amountToString(total), currency: currency); } Money? aggregateSettlementAmountFor(Wallet? sourceWallet) { if (_rows.isEmpty) return null; return _moneyForSourceCurrency( _quotation?.quotation?.aggregate?.expectedSettlementAmounts, sourceWallet, ); } Money? aggregateFeeAmountFor(Wallet? sourceWallet) { if (_rows.isEmpty) return null; return _moneyForSourceCurrency( _quotation?.quotation?.aggregate?.expectedFeeTotals, sourceWallet, ); } double? aggregateFeePercentFor(Wallet? sourceWallet) { final debit = aggregateDebitAmountFor(sourceWallet); final fee = aggregateFeeAmountFor(sourceWallet); if (debit == null || fee == null) return null; final debitValue = double.tryParse(debit.amount); final feeValue = double.tryParse(fee.amount); if (debit.currency.toUpperCase() != fee.currency.toUpperCase()) return null; if (debitValue == null || feeValue == null || debitValue <= 0) return null; return (feeValue / debitValue) * 100; } Future quoteFromCsv({ required String fileName, required String content, required Wallet sourceWallet, }) async { if (isBusy) return; final quotation = _quotation; if (quotation == null) { _setErrorObject( StateError('Multiple payouts dependencies are not ready'), ); return; } try { _setState(MultiplePayoutsState.quoting); _error = null; _sentCount = 0; final rows = _csvParser.parseRows(content); final intents = _intentBuilder.buildIntents(sourceWallet, rows); _selectedFileName = fileName; _rows = rows; await quotation.quotePayments( intents, metadata: { 'upload_filename': fileName, 'upload_rows': rows.length.toString(), ...?_uploadAmountMetadata(), }, ); if (quotation.error != null) { _setErrorObject(quotation.error!); } } catch (e) { _setErrorObject(e); } finally { _setState(MultiplePayoutsState.idle); } } void setError(Object error) { _setErrorObject(error); } Future> send() async { if (isBusy) return const []; final payment = _payment; if (payment == null) { _setErrorObject( StateError('Multiple payouts payment provider is not ready'), ); return const []; } if (!canSend) { _setErrorObject( StateError('Upload CSV and wait for quote before sending'), ); return const []; } try { _setState(MultiplePayoutsState.sending); _error = null; final result = await payment.pay( metadata: { ...?_selectedFileName == null ? null : {'upload_filename': _selectedFileName!}, 'upload_rows': _rows.length.toString(), ...?_uploadAmountMetadata(), }, ); _sentCount = result.length; return result; } catch (e) { _setErrorObject(e); return const []; } finally { _setState(MultiplePayoutsState.idle); } } Future> sendAndStorePayments() async { final result = await send(); _payments?.addPayments(result); return result; } void removeUploadedFile() { if (isBusy) return; _selectedFileName = null; _rows = const []; _sentCount = 0; _error = null; notifyListeners(); } void _setState(MultiplePayoutsState value) { _state = value; notifyListeners(); } void _setErrorObject(Object error) { _error = error is Exception ? error : Exception(error.toString()); notifyListeners(); } void _bindQuotation(MultiQuotationProvider quotation) { if (identical(_quotation, quotation)) return; _quotation?.removeListener(_onQuotationChanged); _quotation = quotation; _quotation?.addListener(_onQuotationChanged); } void _onQuotationChanged() { notifyListeners(); } bool _isQuoteExpired(DateTime? expiresAt) { if (expiresAt == null) return false; return expiresAt.difference(DateTime.now().toUtc()) <= Duration.zero; } Map? _uploadAmountMetadata() { final sentAmount = requestedSentAmount; if (sentAmount == null) return null; return { 'upload_amount': sentAmount.amount, 'upload_currency': sentAmount.currency, }; } Money? _moneyForSourceCurrency( List? values, Wallet? sourceWallet, ) { if (values == null || values.isEmpty) return null; if (sourceWallet != null) { final sourceCurrency = currencyCodeToString(sourceWallet.currency); for (final value in values) { if (value.currency.toUpperCase() == sourceCurrency.toUpperCase()) { return value; } } } return values.first; } @override void dispose() { _quotation?.removeListener(_onQuotationChanged); super.dispose(); } }