multiple payout page and small fixes
This commit is contained in:
295
frontend/pweb/lib/providers/multiple_payouts.dart
Normal file
295
frontend/pweb/lib/providers/multiple_payouts.dart
Normal file
@@ -0,0 +1,295 @@
|
||||
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<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,
|
||||
PaymentsProvider payments,
|
||||
) {
|
||||
_bindQuotation(quotation);
|
||||
_payment = payment;
|
||||
_payments = payments;
|
||||
}
|
||||
|
||||
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 = 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<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);
|
||||
}
|
||||
}
|
||||
|
||||
void setError(Object error) {
|
||||
_setErrorObject(error);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Payment>> sendAndStorePayments() async {
|
||||
final result = await send();
|
||||
_payments?.addPayments(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user