Pantry now runs on the v1 API like Inventory/Animals: service layer in src/lib/services/pantry.ts, pure logic + tests in src/lib/pantry/core.ts, thin routes at /api/v1/pantry/* riding the items:* scopes. Pulls write ItemLog type USED with a structured amount (queryable consumption). Bulk-use runs its reads inside the transaction, accumulates repeated pulls, and reports skipped items instead of silently dropping them — the Pull dialog now surfaces those by name. Old /api/pantry routes deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { normalizeTags, planBulkUse, serializePantryItem, type PantryRow } from "./core";
|
|
|
|
describe("normalizeTags", () => {
|
|
it("trims, drops empties, and dedupes", () => {
|
|
expect(normalizeTags([" Poultry ", "Pre-cooked", "", " ", "Poultry"])).toEqual([
|
|
"Poultry",
|
|
"Pre-cooked",
|
|
]);
|
|
});
|
|
|
|
it("keeps distinct casings distinct (labels are case-sensitive)", () => {
|
|
expect(normalizeTags(["poultry", "Poultry"])).toEqual(["poultry", "Poultry"]);
|
|
});
|
|
});
|
|
|
|
describe("serializePantryItem", () => {
|
|
const row: PantryRow = {
|
|
id: "i1",
|
|
name: "Chicken thighs",
|
|
description: "2 lb pack",
|
|
quantity: "3.50", // Prisma Decimal stringifies like this
|
|
unit: "lbs",
|
|
storedAt: new Date("2026-06-01"),
|
|
bestBefore: new Date("2026-09-01"),
|
|
source: "STORE",
|
|
notes: null,
|
|
pantryCategory: "Meat",
|
|
pantrySubcategory: "Poultry",
|
|
pricePaid: "8.99",
|
|
priceUnit: "lb",
|
|
location: { id: "l1", name: "Chest freezer" },
|
|
plant: null,
|
|
labels: [{ label: { name: "Pre-cooked" } }],
|
|
};
|
|
|
|
it("converts Decimals to numbers and flattens relations", () => {
|
|
const dto = serializePantryItem(row);
|
|
expect(dto.quantity).toBe(3.5);
|
|
expect(dto.pricePaid).toBe(8.99);
|
|
expect(dto.locationId).toBe("l1");
|
|
expect(dto.locationName).toBe("Chest freezer");
|
|
expect(dto.packageSize).toBe("2 lb pack");
|
|
expect(dto.tags).toEqual(["Pre-cooked"]);
|
|
});
|
|
|
|
it("handles null price, location, and plant; formats plant with variety", () => {
|
|
const dto = serializePantryItem({
|
|
...row,
|
|
pricePaid: null,
|
|
location: null,
|
|
plant: { id: "p1", commonName: "Tomato", variety: "Roma" },
|
|
});
|
|
expect(dto.pricePaid).toBeNull();
|
|
expect(dto.locationId).toBeNull();
|
|
expect(dto.plantName).toBe("Tomato (Roma)");
|
|
});
|
|
});
|
|
|
|
describe("planBulkUse", () => {
|
|
const items = [
|
|
{ id: "a", name: "Flour", quantity: 10, unit: "lbs" },
|
|
{ id: "b", name: "Eggs", quantity: 12, unit: "each" },
|
|
];
|
|
|
|
it("computes remaining quantities per pull", () => {
|
|
const plan = planBulkUse(items, [
|
|
{ id: "a", amount: 2 },
|
|
{ id: "b", amount: 6 },
|
|
]);
|
|
expect(plan.skipped).toEqual([]);
|
|
expect(plan.updates).toEqual([
|
|
{ id: "a", name: "Flour", unit: "lbs", amount: 2, remaining: 8 },
|
|
{ id: "b", name: "Eggs", unit: "each", amount: 6, remaining: 6 },
|
|
]);
|
|
});
|
|
|
|
it("reports pulls whose item is missing instead of silently dropping them", () => {
|
|
const plan = planBulkUse(items, [
|
|
{ id: "a", amount: 1 },
|
|
{ id: "gone", amount: 5 },
|
|
]);
|
|
expect(plan.skipped).toEqual(["gone"]);
|
|
expect(plan.updates).toHaveLength(1);
|
|
});
|
|
|
|
it("accumulates repeated pulls of the same item", () => {
|
|
const plan = planBulkUse(items, [
|
|
{ id: "b", amount: 4 },
|
|
{ id: "b", amount: 4 },
|
|
]);
|
|
expect(plan.updates).toEqual([
|
|
{ id: "b", name: "Eggs", unit: "each", amount: 8, remaining: 4 },
|
|
]);
|
|
});
|
|
|
|
it("caps at zero — never goes negative, and logs only what was there", () => {
|
|
const plan = planBulkUse(items, [{ id: "a", amount: 25 }]);
|
|
expect(plan.updates).toEqual([
|
|
{ id: "a", name: "Flour", unit: "lbs", amount: 10, remaining: 0 },
|
|
]);
|
|
});
|
|
});
|