diff --git a/src/app/api/v1/stock-purchases/[id]/attachments/[filename]/route.test.ts b/src/app/api/v1/stock-purchases/[id]/attachments/[filename]/route.test.ts new file mode 100644 index 0000000..640180d --- /dev/null +++ b/src/app/api/v1/stock-purchases/[id]/attachments/[filename]/route.test.ts @@ -0,0 +1,63 @@ +// @vitest-environment node +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@/lib/server/stock-purchases-service", () => ({ + removeStockPurchaseAttachment: vi.fn(), +})); + +import { DELETE } from "./route"; +import { removeStockPurchaseAttachment } from "@/lib/server/stock-purchases-service"; + +const rmMock = removeStockPurchaseAttachment as unknown as ReturnType< + typeof vi.fn +>; + +describe("DELETE /api/v1/stock-purchases/[id]/attachments/[filename]", () => { + beforeEach(() => { + rmMock.mockReset(); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + + it("returns 200 + remaining attachments on success", async () => { + rmMock.mockResolvedValueOnce({ attachments: ["keep.pdf"] }); + const res = await DELETE( + new Request("http://localhost/api/v1/stock-purchases/sp-1/attachments/drop.pdf", { + method: "DELETE", + }), + { params: Promise.resolve({ id: "sp-1", filename: "drop.pdf" }) }, + ); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toEqual({ ok: true, attachments: ["keep.pdf"] }); + expect(rmMock).toHaveBeenCalledWith({ + purchaseId: "sp-1", + token: "drop.pdf", + }); + }); + + it("returns 404 when the token is not in the array", async () => { + rmMock.mockRejectedValueOnce(new Error("Attachment not found: nope.pdf")); + const res = await DELETE( + new Request("http://localhost/api/v1/stock-purchases/sp-1/attachments/nope.pdf", { + method: "DELETE", + }), + { params: Promise.resolve({ id: "sp-1", filename: "nope.pdf" }) }, + ); + expect(res.status).toBe(404); + const body = await res.json(); + expect(body.error).toMatch(/not found/i); + }); + + it("returns 500 on other service errors", async () => { + rmMock.mockRejectedValueOnce(new Error("PB unreachable")); + const res = await DELETE( + new Request("http://localhost/api/v1/stock-purchases/sp-1/attachments/drop.pdf", { + method: "DELETE", + }), + { params: Promise.resolve({ id: "sp-1", filename: "drop.pdf" }) }, + ); + expect(res.status).toBe(500); + }); +}); diff --git a/src/app/api/v1/stock-purchases/[id]/attachments/[filename]/route.ts b/src/app/api/v1/stock-purchases/[id]/attachments/[filename]/route.ts new file mode 100644 index 0000000..102a743 --- /dev/null +++ b/src/app/api/v1/stock-purchases/[id]/attachments/[filename]/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from "next/server"; +import { removeStockPurchaseAttachment } from "@/lib/server/stock-purchases-service"; + +/** + * DELETE /api/v1/stock-purchases/[id]/attachments/[filename] + * + * Remove a single additional attachment token from a stock purchase. + * Mirrors `/api/v1/purchase-orders/[id]/attachments/[filename]`. The + * `filename` segment is the PB-stored token (the same string that + * appears in `additional_attachments[]`), not the operator's original + * filename. PB doesn't expose a per-file delete on a multi-file field + * via the SDK, so the underlying helper filters the array client-side + * and writes the rest back; the on-disk file is left in place (same + * limitation as the PO route). + * + * Response: + * 200 — { ok: true, attachments: [remaining tokens] } + * 404 — { error: "Attachment not found: " } + * 500 — { error } on PB or service failure + */ + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +export async function DELETE( + _req: NextRequest, + { params }: { params: Promise<{ id: string; filename: string }> }, +) { + const { id: purchaseId, filename: token } = await params; + try { + const { attachments } = await removeStockPurchaseAttachment({ + purchaseId, + token, + }); + return NextResponse.json({ ok: true, attachments }, { status: 200 }); + } catch (err) { + const msg = err instanceof Error ? err.message : "Delete failed"; + const status = /not found/i.test(msg) ? 404 : 500; + return NextResponse.json({ error: msg }, { status }); + } +}