diff --git a/src/lib/server/buyer-tasks-service.test.ts b/src/lib/server/buyer-tasks-service.test.ts index 83cf2a2..22021bc 100644 --- a/src/lib/server/buyer-tasks-service.test.ts +++ b/src/lib/server/buyer-tasks-service.test.ts @@ -57,20 +57,26 @@ const baseItem = (overrides: Record = {}) => ({ ...overrides, }); +// Helper to build a mock admin client. Returns the mock so callers +// can spy on the filter passed to getFullList. +const makeAdmin = (items: Record[]) => { + const getFullList = vi.fn().mockResolvedValue(items); + const admin = { + collection: vi.fn().mockReturnValue({ + getList: vi.fn(), // unused — we now call getFullList + getFullList, + }), + }; + return { admin, getFullList }; +}; + describe("listBuyerTasks", () => { beforeEach(() => { vi.clearAllMocks(); }); it("returns groups for items in needs_po state", async () => { - const admin = { - collection: vi.fn().mockReturnValue({ - getList: vi.fn().mockResolvedValue({ - items: [baseItem()], - totalItems: 1, - }), - }), - }; + const { admin, getFullList } = makeAdmin([baseItem()]); vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never); const groups = await listBuyerTasks("needs_po"); @@ -86,20 +92,29 @@ describe("listBuyerTasks", () => { totalCost: 50, step: "needs_po", }); + expect(getFullList).toHaveBeenCalledTimes(1); + expect(getFullList).not.toHaveBeenCalledWith( + expect.objectContaining({ filter: expect.stringContaining("getList") }), + ); + }); + + it("uses getFullList (not the paginated getList) to avoid silent truncation", async () => { + const { admin, getFullList } = makeAdmin([baseItem()]); + vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never); + const collectionMock = admin.collection as unknown as ReturnType; + + await listBuyerTasks("needs_po"); + + expect(collectionMock).toHaveBeenCalledWith("order_items"); + expect(getFullList).toHaveBeenCalledTimes(1); + expect(collectionMock.mock.results[0].value.getList).not.toHaveBeenCalled(); }); it("groups multiple items from the same (order, supplier) pair", async () => { - const admin = { - collection: vi.fn().mockReturnValue({ - getList: vi.fn().mockResolvedValue({ - items: [ - baseItem(), - baseItem({ id: "item-2", quantity: 3, cost_price: 10 }), - ], - totalItems: 2, - }), - }), - }; + const { admin } = makeAdmin([ + baseItem(), + baseItem({ id: "item-2", quantity: 3, cost_price: 10 }), + ]); vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never); const groups = await listBuyerTasks("needs_po"); @@ -110,17 +125,10 @@ describe("listBuyerTasks", () => { }); it("skips items with no supplier", async () => { - const admin = { - collection: vi.fn().mockReturnValue({ - getList: vi.fn().mockResolvedValue({ - items: [ - baseItem(), - baseItem({ id: "item-2", supplier: "", expand: { order: baseOrder, supplier: undefined, purchase_order: undefined } }), - ], - totalItems: 2, - }), - }), - }; + const { admin } = makeAdmin([ + baseItem(), + baseItem({ id: "item-2", supplier: "", expand: { order: baseOrder, supplier: undefined, purchase_order: undefined } }), + ]); vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never); const groups = await listBuyerTasks("needs_po"); @@ -129,25 +137,28 @@ describe("listBuyerTasks", () => { expect(groups[0].itemCount).toBe(1); }); + it("builds the needs_po filter from processed + empty PO", async () => { + const { admin, getFullList } = makeAdmin([baseItem()]); + vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never); + + await listBuyerTasks("needs_po"); + + const filter = (getFullList.mock.calls[0]?.[0] as { filter: string } | undefined)?.filter; + expect(filter).toBe(`item_status = "processed" && purchase_order = ""`); + }); + it("returns groups filtered by po_draft tab", async () => { - const admin = { - collection: vi.fn().mockReturnValue({ - getList: vi.fn().mockResolvedValue({ - items: [ - baseItem({ - purchase_order: "po-1", - item_status: "po_generated", - expand: { - order: baseOrder, - supplier: baseSupplier, - purchase_order: basePo, - }, - }), - ], - totalItems: 1, - }), + const { admin, getFullList } = makeAdmin([ + baseItem({ + purchase_order: "po-1", + item_status: "po_generated", + expand: { + order: baseOrder, + supplier: baseSupplier, + purchase_order: basePo, + }, }), - }; + ]); vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never); const groups = await listBuyerTasks("po_draft"); @@ -156,27 +167,28 @@ describe("listBuyerTasks", () => { expect(groups[0].step).toBe("po_draft"); expect(groups[0].purchaseOrderId).toBe("po-1"); expect(groups[0].poStatus).toBe("draft"); + + // The po_draft filter must restrict to po_generated items so the + // PB query doesn't pull items in downstream statuses (collection + // pool, delivered, etc.) over the wire. + const filter = (getFullList.mock.calls[0]?.[0] as { filter: string } | undefined)?.filter; + expect(filter).toContain(`item_status = "po_generated"`); + expect(filter).toContain(`purchase_order != ""`); + expect(filter).not.toContain("ready_delivery"); }); it("excludes sent/cancelled POs from po_draft tab", async () => { - const admin = { - collection: vi.fn().mockReturnValue({ - getList: vi.fn().mockResolvedValue({ - items: [ - baseItem({ - purchase_order: "po-1", - item_status: "po_generated", - expand: { - order: baseOrder, - supplier: baseSupplier, - purchase_order: { id: "po-1", status: "sent" }, - }, - }), - ], - totalItems: 1, - }), + const { admin } = makeAdmin([ + baseItem({ + purchase_order: "po-1", + item_status: "po_generated", + expand: { + order: baseOrder, + supplier: baseSupplier, + purchase_order: { id: "po-1", status: "sent" }, + }, }), - }; + ]); vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never); const groups = await listBuyerTasks("po_draft"); @@ -185,24 +197,53 @@ describe("listBuyerTasks", () => { }); it("returns groups for po_sent tab", async () => { - const admin = { - collection: vi.fn().mockReturnValue({ - getList: vi.fn().mockResolvedValue({ - items: [ - baseItem({ - purchase_order: "po-1", - item_status: "po_generated", - expand: { - order: baseOrder, - supplier: baseSupplier, - purchase_order: { id: "po-1", status: "sent" }, - }, - }), - ], - totalItems: 1, - }), + const { admin, getFullList } = makeAdmin([ + baseItem({ + purchase_order: "po-1", + item_status: "po_generated", + expand: { + order: baseOrder, + supplier: baseSupplier, + purchase_order: { id: "po-1", status: "sent" }, + }, }), - }; + ]); + vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never); + + const groups = await listBuyerTasks("po_sent"); + + expect(groups).toHaveLength(1); + expect(groups[0].step).toBe("po_sent"); + + // The po_sent filter must keep the in-flight item statuses and + // explicitly drop downstream ones (delivered, invoiced, cancelled) + // so they don't pollute the wire payload. + const filter = (getFullList.mock.calls[0]?.[0] as { filter: string } | undefined)?.filter; + expect(filter).toContain(`purchase_order != ""`); + expect(filter).toContain(`item_status != "ready_delivery"`); + expect(filter).toContain(`item_status != "invoiced"`); + expect(filter).toContain(`item_status != "delivered"`); + expect(filter).toContain(`item_status != "cancelled"`); + expect(filter).not.toContain(`item_status = "po_generated"`); + }); + + it("does not include a separate ready_delivery exclusion in po_sent post-filter", async () => { + // The ready_delivery exclusion moved to the PB filter (covered by + // the filter-content assertions above). Verify the service still + // accepts a PO-sent item in a non-ready_delivery status, which + // means we don't accidentally re-introduce a redundant + // post-filter that would silently drop legitimate groups. + const { admin } = makeAdmin([ + baseItem({ + purchase_order: "po-1", + item_status: "collection_pool", + expand: { + order: baseOrder, + supplier: baseSupplier, + purchase_order: { id: "po-1", status: "sent" }, + }, + }), + ]); vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never); const groups = await listBuyerTasks("po_sent"); @@ -210,30 +251,4 @@ describe("listBuyerTasks", () => { expect(groups).toHaveLength(1); expect(groups[0].step).toBe("po_sent"); }); - - it("excludes ready_delivery items from po_sent tab", async () => { - const admin = { - collection: vi.fn().mockReturnValue({ - getList: vi.fn().mockResolvedValue({ - items: [ - baseItem({ - purchase_order: "po-1", - item_status: "ready_delivery", - expand: { - order: baseOrder, - supplier: baseSupplier, - purchase_order: { id: "po-1", status: "sent" }, - }, - }), - ], - totalItems: 1, - }), - }), - }; - vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never); - - const groups = await listBuyerTasks("po_sent"); - - expect(groups).toHaveLength(0); - }); -}); +}); \ No newline at end of file diff --git a/src/lib/server/buyer-tasks-service.ts b/src/lib/server/buyer-tasks-service.ts index 7f57eb3..ba5bf56 100644 --- a/src/lib/server/buyer-tasks-service.ts +++ b/src/lib/server/buyer-tasks-service.ts @@ -37,18 +37,26 @@ export async function listBuyerTasks(tab: BuyerTaskTab): Promise(); - for (const item of itemsRes.items) { + for (const item of items) { const orderId = item.order; const supplierId = item.supplier; if (!orderId || !supplierId) continue; @@ -57,11 +65,12 @@ export async function listBuyerTasks(tab: BuyerTaskTab): Promise