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>
62 lines
2.5 KiB
TypeScript
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"]);
|
|
});
|
|
});
|