diff --git a/src/app/api/invoicing/print-bundle/route.test.ts b/src/app/api/invoicing/print-bundle/route.test.ts index b8c9959..afa0dd7 100644 --- a/src/app/api/invoicing/print-bundle/route.test.ts +++ b/src/app/api/invoicing/print-bundle/route.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, beforeEach, vi } from "vitest"; -const { fetchMock, getFullList, buildPrintBundle, pdfMock, pickingSlipPdfMock } = vi.hoisted(() => { +const { fetchMock, getFullList, buildPrintBundle, pdfMock, pickingSlipPdfMock, filesGetUrl, authStore } = vi.hoisted(() => { const fetchMock = vi.fn(); const getFullList = vi.fn(); const buildPrintBundle = vi.fn(); @@ -8,7 +8,24 @@ const { fetchMock, getFullList, buildPrintBundle, pdfMock, pickingSlipPdfMock } toString: vi.fn().mockResolvedValue("SLIP_PDF"), })); const pickingSlipPdfMock = vi.fn((_props: unknown) => "PICKING_SLIP_DOC"); - return { fetchMock, getFullList, buildPrintBundle, pdfMock, pickingSlipPdfMock }; + // Mirror the PB SDK's files.getURL: the caller passes + // { collectionId, id } and a filename, the SDK returns + // `${base}/api/files/${collectionId}/${recordId}/${filename}`. + // We control the URL via the mock so the test is environment-independent. + const filesGetUrl = vi.fn( + (record: { collectionId: string; id: string }, filename: string) => + `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${filename}`, + ); + const authStore = { token: "TEST_ADMIN_TOKEN" }; + return { + fetchMock, + getFullList, + buildPrintBundle, + pdfMock, + pickingSlipPdfMock, + filesGetUrl, + authStore, + }; }); vi.stubGlobal("fetch", fetchMock); @@ -16,6 +33,8 @@ vi.stubGlobal("fetch", fetchMock); vi.mock("@/lib/server/pocketbase-admin", () => ({ getAdminPocketBase: vi.fn().mockResolvedValue({ collection: () => ({ getFullList }), + files: { getURL: filesGetUrl }, + authStore, }), })); @@ -55,6 +74,7 @@ describe("GET /api/invoicing/print-bundle", () => { buildPrintBundle.mockReset(); pdfMock.mockClear(); pickingSlipPdfMock.mockReset(); + filesGetUrl.mockClear(); }); it("returns 400 when invoiceNumber is missing", async () => { @@ -148,7 +168,10 @@ describe("GET /api/invoicing/print-bundle", () => { ); expect(fetchMock).toHaveBeenCalledWith( - "https://test.local/api/v1/files/order_items/item-1/inv.pdf", + "http://127.0.0.1:8090/api/files/order_items/item-1/inv.pdf", + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: "TEST_ADMIN_TOKEN" }), + }), ); expect(pickingSlipPdfMock).toHaveBeenCalledTimes(1); @@ -195,6 +218,11 @@ describe("GET /api/invoicing/print-bundle", () => { const res = await GET(makeGetRequest("INV-1") as any); expect(res.status).toBe(200); - expect(fetchMock).toHaveBeenCalledWith("https://test.local/api/v1/files/order_items/item-1/inv.pdf"); + expect(fetchMock).toHaveBeenCalledWith( + "http://127.0.0.1:8090/api/files/order_items/item-1/inv.pdf", + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: "TEST_ADMIN_TOKEN" }), + }), + ); }); }); diff --git a/src/app/api/invoicing/print-bundle/route.ts b/src/app/api/invoicing/print-bundle/route.ts index a447ea3..4c4aadb 100644 --- a/src/app/api/invoicing/print-bundle/route.ts +++ b/src/app/api/invoicing/print-bundle/route.ts @@ -75,7 +75,7 @@ function asNumber(value: unknown): number { return typeof value === "number" && Number.isFinite(value) ? value : 0; } -async function handlePrintBundle(input: { invoiceNumber: string; origin: string }) { +async function handlePrintBundle(input: { invoiceNumber: string }) { const invoiceNumber = input.invoiceNumber.trim(); if (!invoiceNumber) { return NextResponse.json({ error: "Missing invoiceNumber" }, { status: 400 }); @@ -103,8 +103,23 @@ async function handlePrintBundle(input: { invoiceNumber: string; origin: string return NextResponse.json({ error: "Invoice PDF not available" }, { status: 409 }); } - const proxyUrl = `${input.origin}/api/v1/files/order_items/${sourceItem.id}/${encodeURIComponent(fileToken)}`; - const invoiceRes = await fetch(proxyUrl); + // Fetch the invoice PDF directly from PocketBase with admin auth. + // We deliberately do NOT self-fetch through `/api/v1/files/...` here: + // that approach breaks on production because the Next.js server + // can't reach its own public hostname from inside the container + // (DNS resolves to an external IP that doesn't route back, the + // outbound TCP is blocked, or the TLS handshake fails). The + // `/api/v1/files/...` proxy uses the same pattern internally + // (see src/app/api/v1/files/[collection]/[recordId]/[filename]/route.ts), + // so this is just inlining that one step to avoid the self-loop. + const adminToken = pb.authStore?.token ?? ""; + const fileUrl = pb.files.getURL( + { collectionId: sourceItem.collectionId ?? "order_items", id: sourceItem.id }, + fileToken, + ); + const invoiceRes = await fetch(fileUrl, { + headers: adminToken ? { Authorization: adminToken } : {}, + }); if (invoiceRes.status === 404) { return NextResponse.json({ error: "Invoice PDF not found" }, { status: 404 }); } @@ -163,7 +178,7 @@ async function handlePrintBundle(input: { invoiceNumber: string; origin: string export async function GET(request: NextRequest) { const url = new URL(request.url); const invoiceNumber = url.searchParams.get("invoiceNumber") ?? ""; - return handlePrintBundle({ invoiceNumber, origin: url.origin }); + return handlePrintBundle({ invoiceNumber }); } export async function POST(request: NextRequest) { @@ -176,5 +191,5 @@ export async function POST(request: NextRequest) { const invoiceNumber = body && typeof body === "object" ? String((body as { invoiceNumber?: unknown }).invoiceNumber ?? "") : ""; - return handlePrintBundle({ invoiceNumber, origin: new URL(request.url).origin }); + return handlePrintBundle({ invoiceNumber }); }