← back to Govarbitrage

src/importers/csv.ts

72 lines

import { parse } from "csv-parse/sync";
import type { RawListing } from "./ingest";

// Column aliases → RawListing fields, so operators can paste varied CSVs.
const FIELD_ALIASES: Record<string, keyof RawListing> = {
  source: "source",
  auction: "sourceAuctionId",
  "auction #": "sourceAuctionId",
  auction_id: "sourceAuctionId",
  sourceauctionid: "sourceAuctionId",
  url: "sourceUrl",
  link: "sourceUrl",
  title: "title",
  name: "title",
  description: "description",
  desc: "description",
  category: "category",
  manufacturer: "manufacturer",
  mfr: "manufacturer",
  brand: "manufacturer",
  model: "model",
  condition: "condition",
  quantity: "quantity",
  qty: "quantity",
  weight: "weightLbs",
  weightlbs: "weightLbs",
  dimensions: "dimensions",
  city: "locationCity",
  state: "locationState",
  zip: "locationZip",
  bid: "currentBid",
  currentbid: "currentBid",
  "current bid": "currentBid",
  bidcount: "bidCount",
  bids: "bidCount",
  closing: "closingAt",
  closingat: "closingAt",
  "closing date": "closingAt",
  images: "imageUrls",
  imageurls: "imageUrls",
  terms: "auctionTerms",
};

const NUMERIC: (keyof RawListing)[] = ["quantity", "weightLbs", "currentBid", "bidCount"];

/** Parse a CSV string into normalized RawListing rows (header-driven, alias-aware). */
export function parseListingsCsv(text: string): RawListing[] {
  const records = parse(text, {
    columns: (header: string[]) => header.map((h) => h.trim().toLowerCase()),
    skip_empty_lines: true,
    trim: true,
    relax_column_count: true,
  }) as Record<string, string>[];

  return records.map((rec) => {
    const raw: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(rec)) {
      const field = FIELD_ALIASES[key];
      if (!field || value === "" || value == null) continue;
      if (NUMERIC.includes(field)) {
        const num = Number(String(value).replace(/[$,]/g, ""));
        if (!Number.isNaN(num)) raw[field] = num;
      } else if (field === "imageUrls") {
        raw[field] = String(value).split(/[|;]/).map((s) => s.trim()).filter(Boolean);
      } else {
        raw[field] = value;
      }
    }
    return raw as unknown as RawListing;
  });
}