feat(orders): add backfill endpoint for duplicate-detection fields

This commit is contained in:
Dave 2026-08-30 22:28:00 +02:00
parent 51a53851de
commit f0c5daa99e
2 changed files with 126 additions and 0 deletions

View file

@ -0,0 +1,49 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextRequest } from "next/server";
import { POST } from "./route";
vi.mock("@/lib/server/order-number-duplicate-link-service", () => ({
suggestOrderNumberDuplicate: vi.fn().mockResolvedValue({ duplicateOrderId: null }),
}));
vi.mock("@/lib/server/order-order-duplicate-link-service", () => ({
suggestOrderDuplicate: vi
.fn()
.mockResolvedValue({ duplicateOrderId: null, method: "no_match" }),
}));
vi.mock("@/lib/server/order-delivery-note-link-service", () => ({
suggestDeliveryNoteForOrder: vi
.fn()
.mockResolvedValue({ best: null, method: "no_match" }),
}));
vi.mock("@/lib/server/pocketbase-admin", () => ({
getAdminPocketBase: vi.fn().mockResolvedValue({
autoCancellation: vi.fn(),
collection: () => ({
getFullList: vi
.fn()
.mockResolvedValue([{ id: "order-1" }, { id: "order-2" }]),
}),
}),
}));
const AGENT_API_KEY = "test-agent-key";
function backfillRequest(): NextRequest {
return new NextRequest("http://localhost/api/v1/orders/backfill-duplicate-checks", {
method: "POST",
headers: { authorization: `Bearer ${AGENT_API_KEY}` },
});
}
describe("POST /api/v1/orders/backfill-duplicate-checks", () => {
beforeEach(() => {
process.env.AGENT_API_KEY = AGENT_API_KEY;
});
it("runs all three matchers for every order and reports counts", async () => {
const res = await POST(backfillRequest());
expect(res.status).toBe(200);
const body = await res.json();
expect(body.ordersProcessed).toBe(2);
});
});

View file

@ -0,0 +1,77 @@
/**
* POST /api/v1/orders/backfill-duplicate-checks
*
* Operator-triggered backfill: re-runs all three duplicate-detection
* matchers (order-number, item-duplicate, delivery-note-repeat) over
* EVERY existing order. The matchers themselves only run automatically
* on new order creation (see orders-service.ts createOrder) — this
* endpoint is what gets historical orders flagged too.
*
* Intentionally sequential per order (not Promise.all) — this can run
* over the full orders table, and PocketBase's admin client doesn't
* love thousands of concurrent requests. This is a background/admin
* operation, not a user-facing latency path.
*/
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
// Backfills over a full orders table can easily exceed the default 60s
// timeout — at ~100ms per order the 4,800-row history takes ~8 minutes.
// Lift the cap to match the sibling sweep route so Next.js doesn't kill
// the response before we finish.
export const maxDuration = 300;
import { NextRequest, NextResponse } from "next/server";
import { getAdminPocketBase } from "@/lib/server/pocketbase-admin";
import { suggestOrderNumberDuplicate } from "@/lib/server/order-number-duplicate-link-service";
import { suggestOrderDuplicate } from "@/lib/server/order-order-duplicate-link-service";
import { suggestDeliveryNoteForOrder } from "@/lib/server/order-delivery-note-link-service";
import { assertAgentAuth } from "@/lib/server/agent-auth";
export async function POST(req: NextRequest) {
try {
assertAgentAuth(req);
} catch (err) {
const message = err instanceof Error ? err.message : "Unauthorized";
return NextResponse.json({ error: message }, { status: 401 });
}
const pb = await getAdminPocketBase();
pb.autoCancellation(false);
const orders = await pb
.collection("orders")
.getFullList<{ id: string }>({ fields: "id", batch: 500 });
let ordersProcessed = 0;
const failures: Array<{ orderId: string; error: string }> = [];
for (const order of orders) {
try {
await suggestOrderNumberDuplicate(order.id);
} catch (err) {
failures.push({
orderId: order.id,
error: `order_number: ${err instanceof Error ? err.message : String(err)}`,
});
}
try {
await suggestOrderDuplicate(order.id);
} catch (err) {
failures.push({
orderId: order.id,
error: `item_duplicate: ${err instanceof Error ? err.message : String(err)}`,
});
}
try {
await suggestDeliveryNoteForOrder(order.id);
} catch (err) {
failures.push({
orderId: order.id,
error: `delivery_note: ${err instanceof Error ? err.message : String(err)}`,
});
}
ordersProcessed++;
}
return NextResponse.json({ ordersProcessed, failures });
}