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.
This commit is contained in:
Dave 2026-08-31 18:36:15 +02:00
parent e0480878a6
commit 40993ce8f8
4 changed files with 66 additions and 115 deletions

View file

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

View file

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

View file

@ -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<string, string> = {}) {
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);
});
});

View file

@ -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 <AGENT_API_KEY>`.
*
@ -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;
}