From fada6f155f43e769666cf7cfa5d2278a531f4dba Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Mon, 3 Aug 2026 01:01:30 +0200 Subject: [PATCH] feat(collection-confirm): add POST /api/v1/order-items/[id]/cost route Co-authored-by: Orca --- .../v1/order-items/[id]/cost/route.test.ts | 65 +++++++++++++++++++ src/app/api/v1/order-items/[id]/cost/route.ts | 26 ++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/app/api/v1/order-items/[id]/cost/route.test.ts create mode 100644 src/app/api/v1/order-items/[id]/cost/route.ts diff --git a/src/app/api/v1/order-items/[id]/cost/route.test.ts b/src/app/api/v1/order-items/[id]/cost/route.test.ts new file mode 100644 index 0000000..48f6d01 --- /dev/null +++ b/src/app/api/v1/order-items/[id]/cost/route.test.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { NextRequest } from "next/server"; + +vi.mock("@/lib/server/collection-confirm-service", () => ({ + updateOrderItemCost: vi.fn(), +})); + +import { POST } from "./route"; +import { updateOrderItemCost } from "@/lib/server/collection-confirm-service"; + +function makePostRequest(id: string, body: unknown) { + return new NextRequest(`http://localhost/api/v1/order-items/${id}/cost`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); +} + +describe("POST /api/v1/order-items/[id]/cost", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("calls updateOrderItemCost with the route id and body, returns the updated row", async () => { + const updatedRow = { id: "row-1", cost_price: 50 }; + vi.mocked(updateOrderItemCost).mockResolvedValue(updatedRow as never); + + const request = makePostRequest("row-1", { + cost_price: 50, + actor: { id: "u-1", name: "sj" }, + }); + const response = await POST(request, { + params: Promise.resolve({ id: "row-1" }), + }); + const body = await response.json(); + + expect(updateOrderItemCost).toHaveBeenCalledWith("row-1", { + cost_price: 50, + actor: { id: "u-1", name: "sj" }, + }); + expect(response.status).toBe(200); + expect(body).toEqual(updatedRow); + }); + + it("returns a 500 with a generic error body and logs the original error", async () => { + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + vi.mocked(updateOrderItemCost).mockRejectedValue(new Error("PB down")); + + const request = makePostRequest("row-1", { + cost_price: 50, + actor: { id: "u-1", name: "sj" }, + }); + const response = await POST(request, { + params: Promise.resolve({ id: "row-1" }), + }); + expect(response.status).toBe(500); + const body = await response.json(); + expect(body).toEqual({ error: "internal error" }); + expect(errorSpy).toHaveBeenCalledWith( + "[POST /api/v1/order-items/[id]/cost]", + expect.any(Error), + ); + errorSpy.mockRestore(); + }); +}); diff --git a/src/app/api/v1/order-items/[id]/cost/route.ts b/src/app/api/v1/order-items/[id]/cost/route.ts new file mode 100644 index 0000000..83073f5 --- /dev/null +++ b/src/app/api/v1/order-items/[id]/cost/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; +import { updateOrderItemCost } from "@/lib/server/collection-confirm-service"; + +/** + * POST /api/v1/order-items/[id]/cost + * + * Thin wrapper around `updateOrderItemCost(id, { cost_price, actor })` + * from the collection-confirm service layer. The service is responsible + * for the prev-vs-new cost comparison and the audit-note append; this + * route just plumbs the HTTP body through and reports failures with a + * generic 500 (the real error is logged server-side). + */ +export async function POST( + request: Request, + { params }: { params: Promise<{ id: string }> }, +): Promise { + try { + const { id } = await params; + const body = await request.json(); + const updated = await updateOrderItemCost(id, body); + return NextResponse.json(updated); + } catch (error) { + console.error("[POST /api/v1/order-items/[id]/cost]", error); + return NextResponse.json({ error: "internal error" }, { status: 500 }); + } +}