diff --git a/src/app/api/v1/pos/slip-layout/[letterheadId]/route.test.ts b/src/app/api/v1/pos/slip-layout/[letterheadId]/route.test.ts new file mode 100644 index 0000000..cdb9fd7 --- /dev/null +++ b/src/app/api/v1/pos/slip-layout/[letterheadId]/route.test.ts @@ -0,0 +1,72 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { NextRequest } from "next/server"; +import { GET, PUT } from "./route"; +import { getSlipLayout, saveSlipLayout } from "@/lib/server/pos-slip-layout-service"; + +vi.mock("@/lib/server/pos-slip-layout-service", () => ({ + getSlipLayout: vi.fn(), + saveSlipLayout: vi.fn(), +})); + +const POS_API_KEY = "test-pos-key"; + +function req(method: string, body?: unknown, authHeader: string | null = `Bearer ${POS_API_KEY}`): NextRequest { + const headers: Record = { "content-type": "application/json" }; + if (authHeader !== null) headers.authorization = authHeader; + return new NextRequest("http://localhost/api/v1/pos/slip-layout/lh-1", { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + }); +} + +const params = Promise.resolve({ letterheadId: "lh-1" }); + +describe("GET /api/v1/pos/slip-layout/[letterheadId]", () => { + beforeEach(() => { + process.env.POS_API_KEY = POS_API_KEY; + vi.mocked(getSlipLayout).mockReset(); + vi.mocked(saveSlipLayout).mockReset(); + }); + + it("401s without a valid key", async () => { + const res = await GET(req("GET", undefined, null), { params }); + expect(res.status).toBe(401); + }); + + it("returns { layout: null } when nothing is saved yet", async () => { + vi.mocked(getSlipLayout).mockResolvedValue(null); + const res = await GET(req("GET"), { params }); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.layout).toBeNull(); + }); + + it("returns the saved layout", async () => { + vi.mocked(getSlipLayout).mockResolvedValue({ + sale_blocks: [], delivery_blocks: [], cashup_blocks: [], delivery_show_pricing: true, + }); + const res = await GET(req("GET"), { params }); + const body = await res.json(); + expect(body.layout.delivery_show_pricing).toBe(true); + }); +}); + +describe("PUT /api/v1/pos/slip-layout/[letterheadId]", () => { + beforeEach(() => { + process.env.POS_API_KEY = POS_API_KEY; + vi.mocked(saveSlipLayout).mockReset(); + }); + + it("saves and returns the updated layout", async () => { + vi.mocked(saveSlipLayout).mockResolvedValue({ + sale_blocks: [{ id: "logo", enabled: false }], + delivery_blocks: [], cashup_blocks: [], delivery_show_pricing: true, + }); + const res = await PUT(req("PUT", { sale_blocks: [{ id: "logo", enabled: false }] }), { params }); + expect(res.status).toBe(200); + expect(saveSlipLayout).toHaveBeenCalledWith("lh-1", { sale_blocks: [{ id: "logo", enabled: false }] }); + const body = await res.json(); + expect(body.layout.sale_blocks).toEqual([{ id: "logo", enabled: false }]); + }); +}); diff --git a/src/app/api/v1/pos/slip-layout/[letterheadId]/route.ts b/src/app/api/v1/pos/slip-layout/[letterheadId]/route.ts new file mode 100644 index 0000000..d88b866 --- /dev/null +++ b/src/app/api/v1/pos/slip-layout/[letterheadId]/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from "next/server"; +import { assertPosAuth } from "@/lib/server/pos-auth"; +import { + getSlipLayout, + saveSlipLayout, + type PosSlipLayoutInput, +} from "@/lib/server/pos-slip-layout-service"; + +/** + * GET /api/v1/pos/slip-layout/[letterheadId] + * + * Returns `{ layout: null }` when this letterhead has never had a + * layout saved — the till falls back to its own hardcoded defaults + * in that case (see public/pos/slip-layout.js). + * + * Auth: requires `Authorization: Bearer ${POS_API_KEY}`. + */ +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ letterheadId: string }> }, +) { + try { + assertPosAuth(req); + } catch (err) { + const message = err instanceof Error ? err.message : "Unauthorized"; + return NextResponse.json({ error: message }, { status: 401 }); + } + const { letterheadId } = await params; + const layout = await getSlipLayout(letterheadId); + return NextResponse.json({ layout }); +} + +/** + * PUT /api/v1/pos/slip-layout/[letterheadId] + * + * Body: any subset of `{ sale_blocks, delivery_blocks, cashup_blocks, + * delivery_show_pricing }` — only the fields given are written. + * Creates the row on first save. + * + * Auth: requires `Authorization: Bearer ${POS_API_KEY}`. + */ +export async function PUT( + req: NextRequest, + { params }: { params: Promise<{ letterheadId: string }> }, +) { + try { + assertPosAuth(req); + } catch (err) { + const message = err instanceof Error ? err.message : "Unauthorized"; + return NextResponse.json({ error: message }, { status: 401 }); + } + const { letterheadId } = await params; + const body = (await req.json()) as PosSlipLayoutInput; + const layout = await saveSlipLayout(letterheadId, body); + return NextResponse.json({ layout }); +}