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:
parent
e0480878a6
commit
40993ce8f8
4 changed files with 66 additions and 115 deletions
|
|
@ -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}`);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue