small fixes for single payout and big chunck for multiple payouts
This commit is contained in:
157
frontend/pweb/lib/utils/payment/multiple_csv_parser.dart
Normal file
157
frontend/pweb/lib/utils/payment/multiple_csv_parser.dart
Normal 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(',', '.');
|
||||
}
|
||||
}
|
||||
57
frontend/pweb/lib/utils/payment/multiple_intent_builder.dart
Normal file
57
frontend/pweb/lib/utils/payment/multiple_intent_builder.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user