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,157 @@
import 'package:pweb/models/multiple_payouts/csv_row.dart';
class MultipleCsvParser {
List<CsvPayoutRow> parseRows(String content) {
final lines = content
.replaceAll('\r\n', '\n')
.replaceAll('\r', '\n')
.split('\n')
.where((line) => line.trim().isNotEmpty)
.toList(growable: false);
if (lines.isEmpty) {
throw FormatException('CSV is empty');
}
final header = _parseCsvLine(
lines.first,
).map((value) => value.trim().toLowerCase()).toList(growable: false);
final panIndex = _resolveHeaderIndex(header, const ['pan', 'card_pan']);
final firstNameIndex = _resolveHeaderIndex(header, const [
'first_name',
'firstname',
]);
final lastNameIndex = _resolveHeaderIndex(header, const [
'last_name',
'lastname',
]);
final expMonthIndex = _resolveHeaderIndex(header, const [
'exp_month',
'expiry_month',
]);
final expYearIndex = _resolveHeaderIndex(header, const [
'exp_year',
'expiry_year',
]);
final amountIndex = _resolveHeaderIndex(header, const ['amount', 'sum']);
if (panIndex < 0 ||
firstNameIndex < 0 ||
lastNameIndex < 0 ||
expMonthIndex < 0 ||
expYearIndex < 0 ||
amountIndex < 0) {
throw FormatException(
'CSV header must contain pan, first_name, last_name, exp_month, exp_year, amount columns',
);
}
final rows = <CsvPayoutRow>[];
for (var i = 1; i < lines.length; i++) {
final raw = _parseCsvLine(lines[i]);
final pan = _cell(raw, panIndex);
final firstName = _cell(raw, firstNameIndex);
final lastName = _cell(raw, lastNameIndex);
final expMonthRaw = _cell(raw, expMonthIndex);
final expYearRaw = _cell(raw, expYearIndex);
final amount = _normalizeAmount(_cell(raw, amountIndex));
if (pan.isEmpty) {
throw FormatException('CSV row ${i + 1}: pan is required');
}
if (firstName.isEmpty) {
throw FormatException('CSV row ${i + 1}: first_name is required');
}
if (lastName.isEmpty) {
throw FormatException('CSV row ${i + 1}: last_name is required');
}
if (amount.isEmpty) {
throw FormatException('CSV row ${i + 1}: amount is required');
}
final parsedAmount = double.tryParse(amount);
if (parsedAmount == null || parsedAmount <= 0) {
throw FormatException(
'CSV row ${i + 1}: amount must be greater than 0',
);
}
final expMonth = int.tryParse(expMonthRaw);
if (expMonth == null || expMonth < 1 || expMonth > 12) {
throw FormatException('CSV row ${i + 1}: exp_month must be 1-12');
}
final expYear = int.tryParse(expYearRaw);
if (expYear == null || expYear < 0) {
throw FormatException('CSV row ${i + 1}: exp_year is invalid');
}
rows.add(
CsvPayoutRow(
pan: pan,
firstName: firstName,
lastName: lastName,
expMonth: expMonth,
expYear: expYear,
amount: amount,
),
);
}
if (rows.isEmpty) {
throw FormatException('CSV does not contain payout rows');
}
return rows;
}
int _resolveHeaderIndex(List<String> header, List<String> candidates) {
for (final key in candidates) {
final idx = header.indexOf(key);
if (idx >= 0) return idx;
}
return -1;
}
List<String> _parseCsvLine(String line) {
final values = <String>[];
final buffer = StringBuffer();
var inQuotes = false;
for (var i = 0; i < line.length; i++) {
final char = line[i];
if (char == '"') {
final isEscaped = inQuotes && i + 1 < line.length && line[i + 1] == '"';
if (isEscaped) {
buffer.write('"');
i++;
} else {
inQuotes = !inQuotes;
}
continue;
}
if (char == ',' && !inQuotes) {
values.add(buffer.toString());
buffer.clear();
continue;
}
buffer.write(char);
}
values.add(buffer.toString());
return values;
}
String _cell(List<String> row, int index) {
if (index < 0 || index >= row.length) return '';
return row[index].trim();
}
String _normalizeAmount(String value) {
return value.trim().replaceAll(' ', '').replaceAll(',', '.');
}
}

View File

@@ -0,0 +1,57 @@
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/models/payment/asset.dart';
import 'package:pshared/models/payment/chain_network.dart';
import 'package:pshared/models/payment/intent.dart';
import 'package:pshared/models/payment/kind.dart';
import 'package:pshared/models/payment/methods/card.dart';
import 'package:pshared/models/payment/methods/managed_wallet.dart';
import 'package:pshared/models/payment/settlement_mode.dart';
import 'package:pweb/models/multiple_payouts/csv_row.dart';
class MultipleIntentBuilder {
static const String _currency = 'RUB';
List<PaymentIntent> buildIntents(
WalletsController wallets,
List<CsvPayoutRow> rows,
) {
final sourceWallet = wallets.selectedWallet;
if (sourceWallet == null) {
throw StateError('Select source wallet first');
}
final hasAsset = (sourceWallet.tokenSymbol ?? '').isNotEmpty;
final sourceAsset = hasAsset
? PaymentAsset(
chain: sourceWallet.network ?? ChainNetwork.unspecified,
tokenSymbol: sourceWallet.tokenSymbol!,
contractAddress: sourceWallet.contractAddress,
)
: null;
return rows
.map(
(row) => PaymentIntent(
kind: PaymentKind.payout,
source: ManagedWalletPaymentMethod(
managedWalletRef: sourceWallet.id,
asset: sourceAsset,
),
destination: CardPaymentMethod(
pan: row.pan,
firstName: row.firstName,
lastName: row.lastName,
expMonth: row.expMonth,
expYear: row.expYear,
),
amount: Money(amount: row.amount, currency: _currency),
settlementMode: SettlementMode.fixSource,
settlementCurrency: _currency,
),
)
.toList(growable: false);
}
}