multiple payout page and small fixes

This commit is contained in:
Arseni
2026-02-11 02:48:30 +03:00
parent 66989ea36c
commit edb43f9909
77 changed files with 2120 additions and 1289 deletions

View File

@@ -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 }

View 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 '-';
}
}