multiple payout page and small fixes
This commit is contained in:
@@ -14,8 +14,10 @@ class MultipleCsvParser {
|
||||
throw FormatException('CSV is empty');
|
||||
}
|
||||
|
||||
final delimiter = _detectDelimiter(lines.first);
|
||||
final header = _parseCsvLine(
|
||||
lines.first,
|
||||
delimiter,
|
||||
).map((value) => value.trim().toLowerCase()).toList(growable: false);
|
||||
|
||||
final panIndex = _resolveHeaderIndex(header, const ['pan', 'card_pan']);
|
||||
@@ -27,6 +29,11 @@ class MultipleCsvParser {
|
||||
'last_name',
|
||||
'lastname',
|
||||
]);
|
||||
final expDateIndex = _resolveHeaderIndex(header, const [
|
||||
'exp_date',
|
||||
'expiry',
|
||||
'expiry_date',
|
||||
]);
|
||||
final expMonthIndex = _resolveHeaderIndex(header, const [
|
||||
'exp_month',
|
||||
'expiry_month',
|
||||
@@ -40,20 +47,21 @@ class MultipleCsvParser {
|
||||
if (panIndex < 0 ||
|
||||
firstNameIndex < 0 ||
|
||||
lastNameIndex < 0 ||
|
||||
expMonthIndex < 0 ||
|
||||
expYearIndex < 0 ||
|
||||
(expDateIndex < 0 &&
|
||||
(expMonthIndex < 0 || expYearIndex < 0)) ||
|
||||
amountIndex < 0) {
|
||||
throw FormatException(
|
||||
'CSV header must contain pan, first_name, last_name, exp_month, exp_year, amount columns',
|
||||
'CSV header must contain pan, first_name, last_name, amount columns and either exp_date/expiry or exp_month and exp_year',
|
||||
);
|
||||
}
|
||||
|
||||
final rows = <CsvPayoutRow>[];
|
||||
for (var i = 1; i < lines.length; i++) {
|
||||
final raw = _parseCsvLine(lines[i]);
|
||||
final raw = _parseCsvLine(lines[i], delimiter);
|
||||
final pan = _cell(raw, panIndex);
|
||||
final firstName = _cell(raw, firstNameIndex);
|
||||
final lastName = _cell(raw, lastNameIndex);
|
||||
final expDateRaw = expDateIndex >= 0 ? _cell(raw, expDateIndex) : '';
|
||||
final expMonthRaw = _cell(raw, expMonthIndex);
|
||||
final expYearRaw = _cell(raw, expYearIndex);
|
||||
final amount = _normalizeAmount(_cell(raw, amountIndex));
|
||||
@@ -78,13 +86,25 @@ class MultipleCsvParser {
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
int expMonth;
|
||||
int expYear;
|
||||
if (expDateIndex >= 0 && expDateRaw.isNotEmpty) {
|
||||
final parsed = _parseExpiryDate(expDateRaw, i + 1);
|
||||
expMonth = parsed.month;
|
||||
expYear = parsed.year;
|
||||
} else if (expMonthIndex >= 0 && expYearIndex >= 0) {
|
||||
final parsedMonth = int.tryParse(expMonthRaw);
|
||||
if (parsedMonth == null || parsedMonth < 1 || parsedMonth > 12) {
|
||||
throw FormatException('CSV row ${i + 1}: exp_month must be 1-12');
|
||||
}
|
||||
final parsedYear = int.tryParse(expYearRaw);
|
||||
if (parsedYear == null || parsedYear < 0) {
|
||||
throw FormatException('CSV row ${i + 1}: exp_year is invalid');
|
||||
}
|
||||
expMonth = parsedMonth;
|
||||
expYear = parsedYear;
|
||||
} else {
|
||||
throw FormatException('CSV row ${i + 1}: exp_date is required');
|
||||
}
|
||||
|
||||
rows.add(
|
||||
@@ -114,7 +134,36 @@ class MultipleCsvParser {
|
||||
return -1;
|
||||
}
|
||||
|
||||
List<String> _parseCsvLine(String line) {
|
||||
String _detectDelimiter(String line) {
|
||||
final commaCount = _countUnquoted(line, ',');
|
||||
final semicolonCount = _countUnquoted(line, ';');
|
||||
if (semicolonCount > commaCount) return ';';
|
||||
return ',';
|
||||
}
|
||||
|
||||
int _countUnquoted(String line, String needle) {
|
||||
var count = 0;
|
||||
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) {
|
||||
i++;
|
||||
} else {
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (char == needle && !inQuotes) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
List<String> _parseCsvLine(String line, String delimiter) {
|
||||
final values = <String>[];
|
||||
final buffer = StringBuffer();
|
||||
var inQuotes = false;
|
||||
@@ -133,7 +182,7 @@ class MultipleCsvParser {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char == ',' && !inQuotes) {
|
||||
if (char == delimiter && !inQuotes) {
|
||||
values.add(buffer.toString());
|
||||
buffer.clear();
|
||||
continue;
|
||||
@@ -154,4 +203,26 @@ class MultipleCsvParser {
|
||||
String _normalizeAmount(String value) {
|
||||
return value.trim().replaceAll(' ', '').replaceAll(',', '.');
|
||||
}
|
||||
|
||||
_ExpiryDate _parseExpiryDate(String value, int rowNumber) {
|
||||
final match = RegExp(r'^\s*(\d{1,2})\s*/\s*(\d{2})\s*$').firstMatch(value);
|
||||
if (match == null) {
|
||||
throw FormatException(
|
||||
'CSV row $rowNumber: exp_date must be in MM/YY format',
|
||||
);
|
||||
}
|
||||
final month = int.parse(match.group(1)!);
|
||||
final year = int.parse(match.group(2)!);
|
||||
if (month < 1 || month > 12) {
|
||||
throw FormatException('CSV row $rowNumber: exp_date month must be 1-12');
|
||||
}
|
||||
return _ExpiryDate(month, year);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExpiryDate {
|
||||
final int month;
|
||||
final int year;
|
||||
|
||||
const _ExpiryDate(this.month, this.year);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
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/currency_pair.dart';
|
||||
import 'package:pshared/models/payment/fx/intent.dart';
|
||||
import 'package:pshared/models/payment/fx/side.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:pshared/models/payment/wallet.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
import 'package:pshared/utils/payment/fx_helpers.dart';
|
||||
|
||||
import 'package:pweb/models/multiple_payouts/csv_row.dart';
|
||||
|
||||
@@ -18,14 +17,10 @@ class MultipleIntentBuilder {
|
||||
static const String _currency = 'RUB';
|
||||
|
||||
List<PaymentIntent> buildIntents(
|
||||
WalletsController wallets,
|
||||
Wallet sourceWallet,
|
||||
List<CsvPayoutRow> rows,
|
||||
) {
|
||||
final sourceWallet = wallets.selectedWallet;
|
||||
if (sourceWallet == null) {
|
||||
throw StateError('Select source wallet first');
|
||||
}
|
||||
|
||||
final sourceCurrency = currencyCodeToString(sourceWallet.currency);
|
||||
final hasAsset = (sourceWallet.tokenSymbol ?? '').isNotEmpty;
|
||||
final sourceAsset = hasAsset
|
||||
? PaymentAsset(
|
||||
@@ -34,32 +29,37 @@ class MultipleIntentBuilder {
|
||||
contractAddress: sourceWallet.contractAddress,
|
||||
)
|
||||
: null;
|
||||
final fxIntent = FxIntentHelper.buildSellBaseBuyQuote(
|
||||
baseCurrency: sourceCurrency,
|
||||
quoteCurrency: _currency,
|
||||
);
|
||||
|
||||
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.fixReceived,
|
||||
fx : FxIntent(
|
||||
pair: CurrencyPair(
|
||||
base: 'USDT', // TODO: fix currencies picking
|
||||
quote: 'RUB',
|
||||
(row) {
|
||||
final amount = Money(amount: row.amount, currency: _currency);
|
||||
return PaymentIntent(
|
||||
kind: PaymentKind.payout,
|
||||
source: ManagedWalletPaymentMethod(
|
||||
managedWalletRef: sourceWallet.id,
|
||||
asset: sourceAsset,
|
||||
),
|
||||
side: FxSide.sellBaseBuyQuote,
|
||||
),
|
||||
),
|
||||
destination: CardPaymentMethod(
|
||||
pan: row.pan,
|
||||
firstName: row.firstName,
|
||||
lastName: row.lastName,
|
||||
expMonth: row.expMonth,
|
||||
expYear: row.expYear,
|
||||
),
|
||||
amount: amount,
|
||||
settlementMode: SettlementMode.fixReceived,
|
||||
settlementCurrency: FxIntentHelper.resolveSettlementCurrency(
|
||||
amount: amount,
|
||||
fx: fxIntent,
|
||||
),
|
||||
fx: fxIntent,
|
||||
);
|
||||
},
|
||||
)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user