import 'package:flutter/foundation.dart'; import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/quote/quote.dart'; import 'package:pshared/models/payment/quote/status_type.dart'; import 'package:pshared/models/payment/methods/data.dart'; import 'package:pshared/provider/payment/multiple/provider.dart'; import 'package:pshared/provider/payment/multiple/quotation.dart'; import 'package:pshared/utils/currency.dart'; import 'package:pshared/utils/money.dart'; import 'package:pshared/utils/payment/quote_helpers.dart'; import 'package:pweb/models/payment/multiple_payouts/csv_row.dart'; import 'package:pweb/models/payment/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; 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) { _bindQuotation(quotation); _payment = payment; } 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? aggregateDebitAmountForCurrency(String? sourceCurrencyCode) { if (_rows.isEmpty) return null; final totals = aggregateMoneyByCurrency( _quoteItems().map((quote) => quote.amounts?.sourceDebitTotal), ); return _moneyForSourceCurrency(totals, sourceCurrencyCode); } Money? get requestedSentAmount { if (_rows.isEmpty) return null; const currency = 'RUB'; double total = 0; for (final row in _rows) { final value = parseMoneyAmount(row.amount, fallback: double.nan); if (value.isNaN) return null; total += value; } return Money(amount: amountToString(total), currency: currency); } Money? aggregateSettlementAmountForCurrency(String? sourceCurrencyCode) { if (_rows.isEmpty) return null; final totals = aggregateMoneyByCurrency( _quoteItems().map((quote) => quote.amounts?.destinationSettlement), ); return _moneyForSourceCurrency(totals, sourceCurrencyCode); } Money? aggregateFeeAmountForCurrency(String? sourceCurrencyCode) { if (_rows.isEmpty) return null; final totals = aggregateMoneyByCurrency(_quoteItems().map(quoteFeeTotal)); return _moneyForSourceCurrency(totals, sourceCurrencyCode); } double? aggregateFeePercentForCurrency(String? sourceCurrencyCode) { final debit = aggregateDebitAmountForCurrency(sourceCurrencyCode); final fee = aggregateFeeAmountForCurrency(sourceCurrencyCode); if (debit == null || fee == null) return null; final debitValue = parseMoneyAmount(debit.amount, fallback: double.nan); final feeValue = parseMoneyAmount(fee.amount, fallback: double.nan); if (debit.currency.toUpperCase() != fee.currency.toUpperCase()) return null; if (debitValue.isNaN || feeValue.isNaN || debitValue <= 0) return null; return (feeValue / debitValue) * 100; } Future quoteFromCsv({ required String fileName, required String content, required PaymentMethodData sourceMethod, required String sourceCurrencyCode, }) 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); await _quoteRows( quotation: quotation, fileName: fileName, rows: rows, sourceMethod: sourceMethod, sourceCurrencyCode: sourceCurrencyCode, ); if (quotation.error != null) { _setErrorObject(quotation.error!); } } catch (e) { _setErrorObject(e); } finally { _setState(MultiplePayoutsState.idle); } } Future requoteUploadedRows({ required PaymentMethodData sourceMethod, required String sourceCurrencyCode, }) async { if (isBusy || _rows.isEmpty || _selectedFileName == null) return; final quotation = _quotation; if (quotation == null) return; try { _setState(MultiplePayoutsState.quoting); _error = null; _sentCount = 0; await _quoteRows( quotation: quotation, fileName: _selectedFileName!, rows: _rows, sourceMethod: sourceMethod, sourceCurrencyCode: sourceCurrencyCode, ); if (quotation.error != null) { _setErrorObject(quotation.error!); } } catch (e) { _setErrorObject(e); } finally { _setState(MultiplePayoutsState.idle); } } 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); } } 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, String? sourceCurrencyCode, ) { if (values == null || values.isEmpty) return null; if (sourceCurrencyCode != null && sourceCurrencyCode.isNotEmpty) { final sourceCurrency = sourceCurrencyCode.trim().toUpperCase(); for (final value in values) { if (value.currency.toUpperCase() == sourceCurrency) { return value; } } } return values.first; } List _quoteItems() => _quotation?.quotation?.items ?? const []; Future _quoteRows({ required MultiQuotationProvider quotation, required String fileName, required List rows, required PaymentMethodData sourceMethod, required String sourceCurrencyCode, }) async { final intents = _intentBuilder.buildIntents( sourceMethod: sourceMethod, sourceCurrency: sourceCurrencyCode, rows: rows, ); _selectedFileName = fileName; _rows = rows; await quotation.quotePayments( intents, metadata: { 'upload_filename': fileName, 'upload_rows': rows.length.toString(), ...?_uploadAmountMetadata(), }, ); } @override void dispose() { _quotation?.removeListener(_onQuotationChanged); super.dispose(); } }