multiple payout page and small fixes
This commit is contained in:
@@ -3,242 +3,133 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/models/money.dart';
|
||||
import 'package:pshared/models/payment/payment.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/models/payment/quote/status_type.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
|
||||
import 'package:pweb/models/multiple_payouts/csv_row.dart';
|
||||
import 'package:pweb/models/multiple_payouts/state.dart';
|
||||
import 'package:pweb/providers/multiple_payouts.dart';
|
||||
import 'package:pweb/services/payments/csv_input.dart';
|
||||
import 'package:pweb/utils/payment/multiple_csv_parser.dart';
|
||||
import 'package:pweb/utils/payment/multiple_intent_builder.dart';
|
||||
|
||||
|
||||
class MultiplePayoutsController extends ChangeNotifier {
|
||||
final CsvInputService _csvInput;
|
||||
final MultipleCsvParser _csvParser;
|
||||
final MultipleIntentBuilder _intentBuilder;
|
||||
|
||||
MultiplePayoutsProvider? _provider;
|
||||
WalletsController? _wallets;
|
||||
MultiQuotationProvider? _quotation;
|
||||
MultiPaymentProvider? _payment;
|
||||
|
||||
MultiplePayoutsState _state = MultiplePayoutsState.idle;
|
||||
String? _selectedFileName;
|
||||
List<CsvPayoutRow> _rows = const <CsvPayoutRow>[];
|
||||
int _sentCount = 0;
|
||||
Exception? _error;
|
||||
_PickState _pickState = _PickState.idle;
|
||||
|
||||
MultiplePayoutsController({
|
||||
required CsvInputService csvInput,
|
||||
MultipleCsvParser? csvParser,
|
||||
MultipleIntentBuilder? intentBuilder,
|
||||
}) : _csvInput = csvInput,
|
||||
_csvParser = csvParser ?? MultipleCsvParser(),
|
||||
_intentBuilder = intentBuilder ?? MultipleIntentBuilder();
|
||||
}) : _csvInput = csvInput;
|
||||
|
||||
void update(
|
||||
WalletsController wallets,
|
||||
MultiQuotationProvider quotation,
|
||||
MultiPaymentProvider payment,
|
||||
) {
|
||||
_wallets = wallets;
|
||||
_quotation = 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 canSend {
|
||||
if (isBusy || _rows.isEmpty) return false;
|
||||
final quoteRef = _quotation?.quotation?.quoteRef;
|
||||
return quoteRef != null && quoteRef.isNotEmpty;
|
||||
}
|
||||
|
||||
Money? get aggregateDebitAmount {
|
||||
if (_rows.isEmpty) return null;
|
||||
return _moneyForSourceCurrency(
|
||||
_quotation?.quotation?.aggregate?.debitAmounts,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
void update(MultiplePayoutsProvider provider, WalletsController wallets) {
|
||||
var shouldNotify = false;
|
||||
if (!identical(_provider, provider)) {
|
||||
_provider?.removeListener(_onProviderChanged);
|
||||
_provider = provider;
|
||||
_provider?.addListener(_onProviderChanged);
|
||||
shouldNotify = true;
|
||||
}
|
||||
if (!identical(_wallets, wallets)) {
|
||||
_wallets?.removeListener(_onWalletsChanged);
|
||||
_wallets = wallets;
|
||||
_wallets?.addListener(_onWalletsChanged);
|
||||
shouldNotify = true;
|
||||
}
|
||||
if (shouldNotify) {
|
||||
notifyListeners();
|
||||
}
|
||||
return Money(amount: amountToString(total), currency: currency);
|
||||
}
|
||||
|
||||
Money? get aggregateSettlementAmount {
|
||||
if (_rows.isEmpty) return null;
|
||||
return _moneyForSourceCurrency(
|
||||
_quotation?.quotation?.aggregate?.expectedSettlementAmounts,
|
||||
);
|
||||
}
|
||||
MultiplePayoutsState get state =>
|
||||
_provider?.state ?? MultiplePayoutsState.idle;
|
||||
String? get selectedFileName => _provider?.selectedFileName;
|
||||
List<CsvPayoutRow> get rows => _provider?.rows ?? const <CsvPayoutRow>[];
|
||||
int get sentCount => _provider?.sentCount ?? 0;
|
||||
Exception? get error => _provider?.error;
|
||||
|
||||
Money? get aggregateFeeAmount {
|
||||
if (_rows.isEmpty) return null;
|
||||
return _moneyForSourceCurrency(
|
||||
_quotation?.quotation?.aggregate?.expectedFeeTotals,
|
||||
);
|
||||
}
|
||||
bool get isQuoting => _provider?.isQuoting ?? false;
|
||||
bool get isSending => _provider?.isSending ?? false;
|
||||
bool get isBusy => _provider?.isBusy ?? false;
|
||||
|
||||
double? get aggregateFeePercent {
|
||||
final debit = aggregateDebitAmount;
|
||||
final fee = aggregateFeeAmount;
|
||||
if (debit == null || fee == null) return null;
|
||||
bool get quoteIsLoading => _provider?.quoteIsLoading ?? false;
|
||||
QuoteStatusType get quoteStatusType =>
|
||||
_provider?.quoteStatusType ?? QuoteStatusType.missing;
|
||||
Duration? get quoteTimeLeft => _provider?.quoteTimeLeft;
|
||||
|
||||
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;
|
||||
}
|
||||
bool get canSend => _provider?.canSend ?? false;
|
||||
Money? get aggregateDebitAmount =>
|
||||
_provider?.aggregateDebitAmountFor(_selectedWallet);
|
||||
Money? get requestedSentAmount => _provider?.requestedSentAmount;
|
||||
Money? get aggregateSettlementAmount =>
|
||||
_provider?.aggregateSettlementAmountFor(_selectedWallet);
|
||||
Money? get aggregateFeeAmount =>
|
||||
_provider?.aggregateFeeAmountFor(_selectedWallet);
|
||||
double? get aggregateFeePercent =>
|
||||
_provider?.aggregateFeePercentFor(_selectedWallet);
|
||||
|
||||
Future<void> pickAndQuote() async {
|
||||
if (isBusy) return;
|
||||
|
||||
final wallets = _wallets;
|
||||
final quotation = _quotation;
|
||||
if (wallets == null || quotation == null) {
|
||||
_setErrorObject(
|
||||
StateError('Multiple payouts dependencies are not ready'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_pickState == _PickState.picking) return;
|
||||
final provider = _provider;
|
||||
if (provider == null) return;
|
||||
|
||||
_pickState = _PickState.picking;
|
||||
try {
|
||||
_setState(MultiplePayoutsState.quoting);
|
||||
_error = null;
|
||||
_sentCount = 0;
|
||||
|
||||
final picked = await _csvInput.pickCsv();
|
||||
if (picked == null) {
|
||||
if (picked == null) return;
|
||||
final wallet = _selectedWallet;
|
||||
if (wallet == null) {
|
||||
provider.setError(StateError('Select source wallet first'));
|
||||
return;
|
||||
}
|
||||
|
||||
final rows = _csvParser.parseRows(picked.content);
|
||||
final intents = _intentBuilder.buildIntents(wallets, rows);
|
||||
|
||||
_selectedFileName = picked.name;
|
||||
_rows = rows;
|
||||
|
||||
await quotation.quotePayments(
|
||||
intents,
|
||||
metadata: <String, String>{
|
||||
'upload_filename': picked.name,
|
||||
'upload_rows': rows.length.toString(),
|
||||
...?_uploadAmountMetadata(),
|
||||
},
|
||||
await provider.quoteFromCsv(
|
||||
fileName: picked.name,
|
||||
content: picked.content,
|
||||
sourceWallet: wallet,
|
||||
);
|
||||
|
||||
if (quotation.error != null) {
|
||||
_setErrorObject(quotation.error!);
|
||||
}
|
||||
} catch (e) {
|
||||
_setErrorObject(e);
|
||||
provider.setError(e);
|
||||
} finally {
|
||||
_setState(MultiplePayoutsState.idle);
|
||||
_pickState = _PickState.idle;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Payment>> send() async {
|
||||
if (isBusy) return const <Payment>[];
|
||||
return _provider?.send() ?? 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<MultiplePayoutSendOutcome> sendAndStorePayments() async {
|
||||
final payments =
|
||||
await _provider?.sendAndStorePayments() ?? const <Payment>[];
|
||||
final hasError = _provider?.error != null;
|
||||
if (hasError || payments.isEmpty) {
|
||||
return MultiplePayoutSendOutcome.failure;
|
||||
}
|
||||
return MultiplePayoutSendOutcome.success;
|
||||
}
|
||||
|
||||
void removeUploadedFile() {
|
||||
if (isBusy) return;
|
||||
_provider?.removeUploadedFile();
|
||||
}
|
||||
|
||||
_selectedFileName = null;
|
||||
_rows = const <CsvPayoutRow>[];
|
||||
_sentCount = 0;
|
||||
_error = null;
|
||||
void _onProviderChanged() {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setState(MultiplePayoutsState value) {
|
||||
_state = value;
|
||||
void _onWalletsChanged() {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setErrorObject(Object error) {
|
||||
_error = error is Exception ? error : Exception(error.toString());
|
||||
notifyListeners();
|
||||
}
|
||||
Wallet? get _selectedWallet => _wallets?.selectedWallet;
|
||||
|
||||
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) {
|
||||
if (values == null || values.isEmpty) return null;
|
||||
|
||||
final selectedWallet = _wallets?.selectedWallet;
|
||||
if (selectedWallet != null) {
|
||||
final sourceCurrency = currencyCodeToString(selectedWallet.currency);
|
||||
for (final value in values) {
|
||||
if (value.currency.toUpperCase() == sourceCurrency.toUpperCase()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values.first;
|
||||
@override
|
||||
void dispose() {
|
||||
_provider?.removeListener(_onProviderChanged);
|
||||
_wallets?.removeListener(_onWalletsChanged);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
enum _PickState { idle, picking }
|
||||
|
||||
enum MultiplePayoutSendOutcome { success, failure }
|
||||
|
||||
20
frontend/pweb/lib/controllers/upload_history_table.dart
Normal file
20
frontend/pweb/lib/controllers/upload_history_table.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
|
||||
|
||||
class UploadHistoryTableController {
|
||||
const UploadHistoryTableController();
|
||||
|
||||
String amountText(Payment payment) {
|
||||
final receivedAmount = payment.lastQuote?.expectedSettlementAmount;
|
||||
if (receivedAmount != null) {
|
||||
return '${receivedAmount.amount} ${receivedAmount.currency}';
|
||||
}
|
||||
|
||||
final fallbackAmount = payment.lastQuote?.debitAmount;
|
||||
if (fallbackAmount != null) {
|
||||
return '${fallbackAmount.amount} ${fallbackAmount.currency}';
|
||||
}
|
||||
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user