From 275a47e28f70ffd0fb477fd5d9b803f584964de0 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Wed, 29 Jul 2026 20:02:04 +0200 Subject: [PATCH] feat(buyer-tasks): GET /api/v1/buyer-tasks route Co-authored-by: Orca --- src/app/api/v1/buyer-tasks/route.test.ts | 63 ++++++++++++++++++++++++ src/app/api/v1/buyer-tasks/route.ts | 29 +++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/app/api/v1/buyer-tasks/route.test.ts create mode 100644 src/app/api/v1/buyer-tasks/route.ts diff --git a/src/app/api/v1/buyer-tasks/route.test.ts b/src/app/api/v1/buyer-tasks/route.test.ts new file mode 100644 index 0000000..b45c4a6 --- /dev/null +++ b/src/app/api/v1/buyer-tasks/route.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; + +vi.mock("@/lib/server/buyer-tasks-service", () => ({ + listBuyerTasks: vi.fn(), +})); + +import { GET } from "./route"; +import { listBuyerTasks } from "@/lib/server/buyer-tasks-service"; + +function makeRequest(tab: string | null) { + const url = tab ? `http://test/api/v1/buyer-tasks?tab=${tab}` : "http://test/api/v1/buyer-tasks"; + return new Request(url); +} + +describe("GET /api/v1/buyer-tasks", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns groups for valid tab", async () => { + vi.mocked(listBuyerTasks).mockResolvedValue([ + { + groupId: "order-1::sup-1", + orderId: "order-1", + orderNumber: "ORD-001", + supplierId: "sup-1", + supplierName: "Steel Co", + customerName: "J. Smith", + companyName: "Acme", + items: [], + itemCount: 1, + totalCost: 50, + purchaseOrderId: null, + poStatus: null, + step: "needs_po", + }, + ]); + + const res = await GET(makeRequest("needs_po") as never); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.groups).toHaveLength(1); + expect(body.groups[0].supplierName).toBe("Steel Co"); + }); + + it("returns 400 on invalid tab", async () => { + const res = await GET(makeRequest("invalid") as never); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/tab/i); + }); + + it("returns 400 when tab is missing", async () => { + const res = await GET(makeRequest(null) as never); + expect(res.status).toBe(400); + }); + + it("returns 500 on service error", async () => { + vi.mocked(listBuyerTasks).mockRejectedValue(new Error("PB down")); + const res = await GET(makeRequest("needs_po") as never); + expect(res.status).toBe(500); + }); +}); \ No newline at end of file diff --git a/src/app/api/v1/buyer-tasks/route.ts b/src/app/api/v1/buyer-tasks/route.ts new file mode 100644 index 0000000..4c0a1d4 --- /dev/null +++ b/src/app/api/v1/buyer-tasks/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from "next/server"; +import { listBuyerTasks, type BuyerTaskTab } from "@/lib/server/buyer-tasks-service"; + +const VALID_TABS: BuyerTaskTab[] = ["needs_po", "po_draft", "po_sent"]; + +export async function GET(req: NextRequest | Request) { + // NextRequest exposes a parsed `nextUrl`; a plain Request (used in + // unit tests with `new Request(url)`) doesn't. Fall back to parsing + // `req.url` so both shapes work. + const tab = (req as NextRequest).nextUrl?.searchParams?.get("tab") + ?? new URL(req.url).searchParams.get("tab"); + + if (!tab || !VALID_TABS.includes(tab as BuyerTaskTab)) { + return NextResponse.json( + { error: "Invalid or missing 'tab' parameter" }, + { status: 400 } + ); + } + + try { + const groups = await listBuyerTasks(tab as BuyerTaskTab); + return NextResponse.json({ groups }); + } catch (err) { + return NextResponse.json( + { error: err instanceof Error ? err.message : "Failed to load buyer tasks" }, + { status: 500 } + ); + } +} \ No newline at end of file