diff --git a/src/lib/server/pos-service.test.ts b/src/lib/server/pos-service.test.ts new file mode 100644 index 0000000..3a63655 --- /dev/null +++ b/src/lib/server/pos-service.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect, vi } from "vitest"; +import { listActiveStockCatalog } from "./pos-service"; + +function stubPb(items: unknown[]) { + return { + collection: () => ({ + getFullList: vi.fn().mockResolvedValue(items), + getList: vi.fn(), + getOne: vi.fn(), + create: vi.fn(), + update: vi.fn(), + }), + }; +} + +describe("listActiveStockCatalog", () => { + it("flattens stock_items rows, pulling the expanded category name", async () => { + const pb = stubPb([ + { + id: "si1", + sku: "SKU-1", + name: "Widget", + description: "A widget", + unit: "ea", + current_qty: 12, + selling_price: 99.5, + cost_price: 50, + expand: { category: { name: "Hardware" } }, + }, + { + id: "si2", + sku: "SKU-2", + name: "Uncategorised thing", + description: "", + unit: "ea", + current_qty: 0, + selling_price: 10, + cost_price: 5, + }, + ]); + + const result = await listActiveStockCatalog(pb); + + expect(result).toEqual([ + { + id: "si1", + sku: "SKU-1", + name: "Widget", + description: "A widget", + unit: "ea", + category: "Hardware", + current_qty: 12, + selling_price: 99.5, + cost_price: 50, + }, + { + id: "si2", + sku: "SKU-2", + name: "Uncategorised thing", + description: "", + unit: "ea", + category: "", + current_qty: 0, + selling_price: 10, + cost_price: 5, + }, + ]); + }); +}); diff --git a/src/lib/server/pos-service.ts b/src/lib/server/pos-service.ts new file mode 100644 index 0000000..6e8deab --- /dev/null +++ b/src/lib/server/pos-service.ts @@ -0,0 +1,50 @@ +import type { PosCatalogItem, PosSale, PosSaleItem, StockItem } from "@/lib/types"; + +/** + * PB client type kept loose so both the real `PocketBase` instance + * and test stubs satisfy the contract — same approach as + * `stock-items-service.ts`'s `StockItemsPb`. + */ +export type PosPb = { + collection: (name: string) => { + getFullList: (opts?: Record) => Promise; + getList: ( + page: number, + perPage: number, + opts?: Record, + ) => Promise<{ items: T[] }>; + getOne: (id: string, opts?: Record) => Promise; + create: (data: Record) => Promise; + update: (id: string, data: Record) => Promise; + }; +}; + +// ─── listActiveStockCatalog ──────────────────────────────────────── + +type StockItemWithCategory = StockItem & { + expand?: { category?: { name?: string } }; +}; + +/** + * Catalog source for the POS: active stock_items only, flattened so + * the frontend doesn't need to know about PB's relation/expand shape. + */ +export async function listActiveStockCatalog(pb: PosPb): Promise { + const items = await pb.collection("stock_items").getFullList({ + filter: "is_active = true", + sort: "name", + expand: "category", + }); + + return items.map((it) => ({ + id: it.id, + sku: it.sku, + name: it.name, + description: it.description, + unit: it.unit, + category: it.expand?.category?.name || "", + current_qty: Number(it.current_qty ?? 0), + selling_price: Number(it.selling_price ?? 0), + cost_price: Number(it.cost_price ?? 0), + })); +}