From 40993ce8f82448387036644700a0bf2300755994 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 31 Aug 2026 18:36:15 +0200 Subject: [PATCH] refactor: remove check-progress and run-backfill scripts; add DELETE endpoint for quote_items This commit deletes the check-progress.mjs and run-backfill.mjs files, which were previously used for order processing and backfilling tasks. Additionally, it introduces a DELETE endpoint for quote_items, allowing for the deletion of quote items by ID, along with corresponding tests to ensure proper functionality and error handling. --- check-progress.mjs | 35 --------- run-backfill.mjs | 76 ------------------- src/app/api/v1/quote_items/[id]/route.test.ts | 43 ++++++++++- src/app/api/v1/quote_items/[id]/route.ts | 27 ++++++- 4 files changed, 66 insertions(+), 115 deletions(-) delete mode 100644 check-progress.mjs delete mode 100644 run-backfill.mjs diff --git a/check-progress.mjs b/check-progress.mjs deleted file mode 100644 index 4c2b008..0000000 --- a/check-progress.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import { readFileSync } from "node:fs"; -import PocketBase from "pocketbase"; -const envLocal = readFileSync(".env.local", "utf8"); -for (const line of envLocal.split("\n")) { - const m = line.match(/^([A-Z0-9_]+)\s*=\s*(.+?)\s*$/); - if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^["']|["']$/g, ""); -} -const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL); -await pb.admins.authWithPassword(process.env.PB_ADMIN_EMAIL, process.env.PB_ADMIN_PASSWORD); - -let numChecked = 0, orderAttempted = 0, dnAttempted = 0, total = 0; -for (let p = 1; p <= 30; p++) { - const r = await pb.collection("orders").getList(p, 200, { - fields: "id,order_number_duplicate_checked,duplicate_order_matching_attempted,delivery_note_matching_attempted", - sort: "created", - }); - for (const o of r.items) { - total++; - if (o.order_number_duplicate_checked === true) numChecked++; - if (o.duplicate_order_matching_attempted === true) orderAttempted++; - if (o.delivery_note_matching_attempted === true) dnAttempted++; - } - if (r.items.length < 200) break; -} -const hits1 = await pb.collection("orders").getList(1, 100, { - filter: 'order_number_duplicate != "" && order_number_duplicate != null', -}); -const hits2 = await pb.collection("orders").getList(1, 100, { - filter: 'potential_duplicate_order != "" && potential_duplicate_order != null', -}); -const hits3 = await pb.collection("orders").getList(1, 100, { - filter: 'potential_delivery_note != "" && potential_delivery_note != null', -}); -console.log(`total=${total} num=${numChecked} (${(numChecked/total*100).toFixed(1)}%) order=${orderAttempted} (${(orderAttempted/total*100).toFixed(1)}%) dn=${dnAttempted} (${(dnAttempted/total*100).toFixed(1)}%)`); -console.log(`hits: order-num=${hits1.totalItems} item-dup=${hits2.totalItems} dn=${hits3.totalItems}`); diff --git a/run-backfill.mjs b/run-backfill.mjs deleted file mode 100644 index b41f359..0000000 --- a/run-backfill.mjs +++ /dev/null @@ -1,76 +0,0 @@ -// Run the full backfill in 200-order chunks. Logs progress to stdout. -import { readFileSync } from "node:fs"; - -const envLocal = readFileSync(".env.local", "utf8"); -for (const line of envLocal.split("\n")) { - const m = line.match(/^([A-Z0-9_]+)\s*=\s*(.+?)\s*$/); - if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^["']|["']$/g, ""); -} - -const baseUrl = "http://localhost:4001"; -const agentKey = process.env.AGENT_API_KEY; -if (!agentKey) { - console.error("Missing AGENT_API_KEY in .env.local"); - process.exit(1); -} - -const LIMIT = 200; -let skip = 0; -let totalProcessed = 0; -let totalFailures = 0; -const start = Date.now(); -let chunkNum = 0; - -while (true) { - chunkNum++; - const url = `${baseUrl}/api/v1/orders/backfill-duplicate-checks?limit=${LIMIT}&skip=${skip}`; - let body; - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 600_000); // 10 min per chunk - const res = await fetch(url, { - method: "POST", - headers: { authorization: `Bearer ${agentKey}` }, - signal: controller.signal, - }); - clearTimeout(timeoutId); - if (!res.ok) { - console.error(`[fail] HTTP ${res.status} at skip=${skip}`); - const text = await res.text(); - console.error(text.substring(0, 500)); - process.exit(2); - } - body = await res.json(); - } catch (err) { - console.error(`[err] fetch failed at skip=${skip}:`, err.message); - if (err.cause) console.error("cause:", err.cause); - process.exit(2); - } - - totalProcessed += body.ordersProcessed || 0; - totalFailures += body.failures?.length || 0; - const elapsed = Math.round((Date.now() - start) / 1000); - const rate = totalProcessed / (elapsed / 60); - console.log( - `[chunk ${chunkNum} skip=${skip} processed=${body.ordersProcessed} ` + - `failures=${body.failures?.length || 0} total=${totalProcessed} ` + - `nextSkip=${body.nextSkip} done=${body.done} elapsed=${elapsed}s rate=${rate.toFixed(1)}/min]`, - ); - - if (body.failures?.length > 0) { - for (const f of body.failures.slice(0, 3)) { - console.log(` failure: ${f.orderId} -> ${f.error}`); - } - if (body.failures.length > 3) { - console.log(` ... and ${body.failures.length - 3} more`); - } - } - - if (body.done) { - console.log( - `\n[done] total processed: ${totalProcessed}, total failures: ${totalFailures}, elapsed: ${elapsed}s (${(elapsed / 60).toFixed(1)} min)`, - ); - process.exit(0); - } - skip = body.nextSkip; -} diff --git a/src/app/api/v1/quote_items/[id]/route.test.ts b/src/app/api/v1/quote_items/[id]/route.test.ts index f699049..16f7b2b 100644 --- a/src/app/api/v1/quote_items/[id]/route.test.ts +++ b/src/app/api/v1/quote_items/[id]/route.test.ts @@ -3,13 +3,14 @@ import { NextRequest } from "next/server"; const getOne = vi.fn(); const update = vi.fn(); +const del = vi.fn(); vi.mock("@/lib/server/pocketbase-admin", () => ({ getAdminPocketBase: vi.fn().mockResolvedValue({ - collection: () => ({ getOne, update }), + collection: () => ({ getOne, update, delete: del }), }), })); -import { GET, PATCH } from "./route"; +import { GET, PATCH, DELETE } from "./route"; function getURL(id: string, qs: Record = {}) { const url = new URL(`http://localhost/api/v1/quote_items/${id}`); @@ -154,3 +155,41 @@ describe("PATCH /api/v1/quote_items/[id]", () => { expect(res.status).toBe(404); }); }); + +describe("DELETE /api/v1/quote_items/[id]", () => { + beforeEach(() => { + process.env.AGENT_API_KEY = "t"; + del.mockReset(); + del.mockResolvedValue(true); + }); + + it("401s without bearer token", async () => { + const req = new NextRequest("http://localhost/api/v1/quote_items/qi1", { + method: "DELETE", + }); + const res = await DELETE(req, PARAMS("qi1")); + expect(res.status).toBe(401); + expect(del).not.toHaveBeenCalled(); + }); + + it("deletes the row by id", async () => { + const req = new NextRequest("http://localhost/api/v1/quote_items/qi1", { + method: "DELETE", + headers: { authorization: "Bearer t" }, + }); + const res = await DELETE(req, PARAMS("qi1")); + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ success: true, id: "qi1" }); + expect(del).toHaveBeenCalledWith("qi1"); + }); + + it("404s when PB says the id is missing", async () => { + del.mockRejectedValue(new Error("Failed to delete record.")); + const req = new NextRequest("http://localhost/api/v1/quote_items/ghost", { + method: "DELETE", + headers: { authorization: "Bearer t" }, + }); + const res = await DELETE(req, PARAMS("ghost")); + expect(res.status).toBe(404); + }); +}); diff --git a/src/app/api/v1/quote_items/[id]/route.ts b/src/app/api/v1/quote_items/[id]/route.ts index cb16ce1..a969e0d 100644 --- a/src/app/api/v1/quote_items/[id]/route.ts +++ b/src/app/api/v1/quote_items/[id]/route.ts @@ -4,8 +4,9 @@ import { getAdminPocketBase } from "@/lib/server/pocketbase-admin"; import { deriveSellingPriceIfOmitted } from "@/lib/server/quote-items-service"; /** - * GET /api/v1/quote_items/[id] — fetch a single quote_item - * PATCH /api/v1/quote_items/[id] — patch a quote_item (whitelisted fields) + * GET /api/v1/quote_items/[id] — fetch a single quote_item + * PATCH /api/v1/quote_items/[id] — patch a quote_item (whitelisted fields) + * DELETE /api/v1/quote_items/[id] — delete a single quote_item * * Auth: same as the rest of `/api/v1` — `Authorization: Bearer `. * @@ -132,6 +133,28 @@ export async function PATCH( } } +export async function DELETE( + req: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + try { + assertAgentAuth(req); + const { id } = await params; + if (!id) { + return NextResponse.json({ error: "Missing id" }, { status: 400 }); + } + + const pb = await getAdminPocketBase(); + await pb.collection("quote_items").delete(id); + return NextResponse.json({ success: true, id }); + } catch (error) { + const message = errMsg(error, "Failed to delete quote_item"); + const status = + /Failed to delete record|not found/i.test(message) ? 404 : errStatus(error, 500); + return NextResponse.json({ error: message }, { status }); + } +} + function errMsg(error: unknown, fallback: string): string { return error instanceof Error ? error.message : fallback; }