Files
sendico/frontend/pweb/lib/providers/multiple_payouts.dart
2026-02-21 21:55:20 +03:00

283 lines
8.0 KiB
Dart

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/utils/currency.dart';
import 'package:pshared/utils/money.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<CsvPayoutRow> _rows = const <CsvPayoutRow>[];
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<CsvPayoutRow> 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 = parseMoneyAmount(row.amount, fallback: double.nan);
if (value.isNaN) 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 = 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<void> 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: <String, String>{
'upload_filename': fileName,
'upload_rows': rows.length.toString(),
...?_uploadAmountMetadata(),
},
);
if (quotation.error != null) {
_setErrorObject(quotation.error!);
}
} catch (e) {
_setErrorObject(e);
} finally {
_setState(MultiplePayoutsState.idle);
}
}
Future<List<Payment>> send() async {
if (isBusy) return const <Payment>[];
final payment = _payment;
if (payment == null) {
_setErrorObject(
StateError('Multiple payouts payment provider is not ready'),
);
return const <Payment>[];
}
if (!canSend) {
_setErrorObject(
StateError('Upload CSV and wait for quote before sending'),
);
return const <Payment>[];
}
try {
_setState(MultiplePayoutsState.sending);
_error = null;
final result = await payment.pay(
metadata: <String, String>{
...?_selectedFileName == null
? null
: <String, String>{'upload_filename': _selectedFileName!},
'upload_rows': _rows.length.toString(),
...?_uploadAmountMetadata(),
},
);
_sentCount = result.length;
return result;
} catch (e) {
_setErrorObject(e);
return const <Payment>[];
} finally {
_setState(MultiplePayoutsState.idle);
}
}
void removeUploadedFile() {
if (isBusy) return;
_selectedFileName = null;
_rows = const <CsvPayoutRow>[];
_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<String, String>? _uploadAmountMetadata() {
final sentAmount = requestedSentAmount;
if (sentAmount == null) return null;
return <String, String>{
'upload_amount': sentAmount.amount,
'upload_currency': sentAmount.currency,
};
}
Money? _moneyForSourceCurrency(
List<Money>? 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();
}
}