small fixes for single payout and big chunck for multiple payouts

This commit is contained in:
Arseni
2026-02-05 21:58:37 +03:00
parent 8034847e46
commit b9748b8ab2
37 changed files with 1708 additions and 224 deletions

View File

@@ -0,0 +1,244 @@
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:pweb/models/multiple_payouts/csv_row.dart';
import 'package:pweb/models/multiple_payouts/state.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;
WalletsController? _wallets;
MultiQuotationProvider? _quotation;
MultiPaymentProvider? _payment;
MultiplePayoutsState _state = MultiplePayoutsState.idle;
String? _selectedFileName;
List<CsvPayoutRow> _rows = const <CsvPayoutRow>[];
int _sentCount = 0;
Exception? _error;
MultiplePayoutsController({
required CsvInputService csvInput,
MultipleCsvParser? csvParser,
MultipleIntentBuilder? intentBuilder,
}) : _csvInput = csvInput,
_csvParser = csvParser ?? MultipleCsvParser(),
_intentBuilder = intentBuilder ?? MultipleIntentBuilder();
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;
}
return Money(amount: amountToString(total), currency: currency);
}
Money? get aggregateSettlementAmount {
if (_rows.isEmpty) return null;
return _moneyForSourceCurrency(
_quotation?.quotation?.aggregate?.expectedSettlementAmounts,
);
}
Money? get aggregateFeeAmount {
if (_rows.isEmpty) return null;
return _moneyForSourceCurrency(
_quotation?.quotation?.aggregate?.expectedFeeTotals,
);
}
double? get aggregateFeePercent {
final debit = aggregateDebitAmount;
final fee = aggregateFeeAmount;
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> 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;
}
try {
_setState(MultiplePayoutsState.quoting);
_error = null;
_sentCount = 0;
final picked = await _csvInput.pickCsv();
if (picked == null) {
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(),
},
);
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();
}
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;
}
}

View File

@@ -0,0 +1,71 @@
import 'package:flutter/foundation.dart';
import 'package:pshared/provider/payment/flow.dart';
import 'package:pshared/provider/payment/provider.dart';
import 'package:pshared/provider/payment/quotation/quotation.dart';
import 'package:pshared/provider/recipient/provider.dart';
class PaymentPageController extends ChangeNotifier {
PaymentProvider? _payment;
QuotationProvider? _quotation;
PaymentFlowProvider? _flow;
RecipientsProvider? _recipients;
bool _isSending = false;
Exception? _error;
bool get isSending => _isSending;
Exception? get error => _error;
void update(
PaymentProvider payment,
QuotationProvider quotation,
PaymentFlowProvider flow,
RecipientsProvider recipients,
) {
_payment = payment;
_quotation = quotation;
_flow = flow;
_recipients = recipients;
}
Future<bool> sendPayment() async {
if (_isSending) return false;
final payment = _payment;
if (payment == null) {
_setError(StateError('Payment provider is not ready'));
return false;
}
try {
_setSending(true);
_error = null;
final result = await payment.pay();
return result != null && payment.error == null;
} catch (e) {
_setError(e);
return false;
} finally {
_setSending(false);
}
}
void resetAfterSuccess() {
_quotation?.reset();
_payment?.reset();
_flow?.setManualPaymentData(null);
_recipients?.setCurrentObject(null);
}
void _setSending(bool value) {
if (_isSending == value) return;
_isSending = value;
notifyListeners();
}
void _setError(Object error) {
_error = error is Exception ? error : Exception(error.toString());
notifyListeners();
}
}