Files
Moonbase/src/lib/inventory/homebox-import.test.ts
tonym 5f4c482636 v0.16.0 — HomeBox CSV import + sold lifecycle + custom fields
Bring an existing HomeBox inventory straight in, and capture the fields HomeBox
has that we were missing (skipping asset IDs per Tony).

- Item: soldTo/soldPrice/soldDate/soldNotes + flexible customFields (JSON, e.g.
  VIN / oil weight for vehicles); both editable in the item dialog and shown on
  the detail page
- Pure HomeBox CSV parser (src/lib/inventory/homebox-import.ts, +5 tests) —
  RFC4180 quoting (commas/newlines/escaped quotes in fields), HomeBox's 0001-*
  null dates and 0 = empty price, semicolon labels, HB.field.* custom columns,
  nested 'A / B' location paths
- Import service + /api/v1/items/import: find-or-creates the nested locations
  and labels, creates items, recomputes location paths, returns a summary
- Inventory 'Import' button: paste/upload CSV, review every row with checkboxes
  (uncheck to skip), then import. Preview parses client-side (parser is pure)
- Verified end-to-end: nested locations, split labels, sold + custom fields,
  skip, quoted names, null-date handling. tsc + 38 tests + build green

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 01:01:17 -05:00

62 lines
2.5 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { parseCsv, parseHomeBoxCsv, splitLocationPath } from "./homebox-import";
describe("parseCsv", () => {
it("handles quoted fields with embedded commas and quotes", () => {
const rows = parseCsv('a,"b,c","d""e"\n1,2,3');
expect(rows[0]).toEqual(["a", "b,c", 'd"e']);
expect(rows[1]).toEqual(["1", "2", "3"]);
});
it("handles newlines inside quoted fields", () => {
const rows = parseCsv('name,desc\n"Mower","200cc\nrear bag"');
expect(rows[1]).toEqual(["Mower", "200cc\nrear bag"]);
});
});
const HEADER =
"HB.location,HB.labels,HB.name,HB.quantity,HB.description,HB.insured,HB.purchase_price,HB.purchase_from,HB.purchase_time,HB.manufacturer,HB.lifetime_warranty,HB.warranty_expires,HB.sold_to,HB.sold_price,HB.sold_time,HB.field.VIN,HB.field.Oil Weight";
describe("parseHomeBoxCsv", () => {
it("maps columns and applies HomeBox quirks", () => {
const csv = [
HEADER,
'Living Room,Electronics,"Samsung 70""",1,,false,0,,0001-01-01,,false,0001-01-01,,0,0001-01-01,,',
'Vehicles,,2012 Equinox,1,,true,400,Dealer,2024-05-01,Chevy,false,0001-01-01,Alter,525,2024-05-24,1ABC,5W-30',
].join("\n");
const items = parseHomeBoxCsv(csv);
expect(items).toHaveLength(2);
const tv = items[0];
expect(tv.name).toBe('Samsung 70"');
expect(tv.labels).toEqual(["Electronics"]);
expect(tv.value).toBeNull(); // price 0 → null
expect(tv.purchaseDate).toBeNull(); // 0001 → null
expect(tv.insured).toBe(false);
const car = items[1];
expect(car.value).toBe(400);
expect(car.purchaseDate).toBe("2024-05-01");
expect(car.insured).toBe(true);
expect(car.soldTo).toBe("Alter");
expect(car.soldPrice).toBe(525);
expect(car.soldDate).toBe("2024-05-24");
expect(car.customFields).toEqual({ VIN: "1ABC", "Oil Weight": "5W-30" });
});
it("splits semicolon labels and skips nameless rows", () => {
const csv = [HEADER, "Basement,Tools; Woodworking,Planer,1,,false,0,,0001,,false,0001,,0,0001,,",
"Basement,,,1,,false,0,,0001,,false,0001,,0,0001,,"].join("\n");
const items = parseHomeBoxCsv(csv);
expect(items).toHaveLength(1);
expect(items[0].labels).toEqual(["Tools", "Woodworking"]);
});
});
describe("splitLocationPath", () => {
it("splits nested HomeBox paths", () => {
expect(splitLocationPath("Upstairs / Print Room")).toEqual(["Upstairs", "Print Room"]);
expect(splitLocationPath("Vehicles")).toEqual(["Vehicles"]);
});
});