feat(api): add stock-purchase attachments delete route

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-08-06 23:21:25 +02:00
parent a72c5ca0a7
commit ccf04bc39f
2 changed files with 104 additions and 0 deletions

View file

@ -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);
});
});

View file

@ -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: <token>" }
* 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 });
}
}