Files
Moonbase/src/lib/pantry/core.test.ts
Bonna Moon 25b1ba9aeb v0.23.0 — Pantry: structured USED pull logs + move to /api/v1 service layer
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>
2026-07-06 00:14:53 -05:00

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 },
]);
});
});