feat(orders): add backfill endpoint for duplicate-detection fields
This commit is contained in:
parent
51a53851de
commit
f0c5daa99e
2 changed files with 126 additions and 0 deletions
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
77
src/app/api/v1/orders/backfill-duplicate-checks/route.ts
Normal file
77
src/app/api/v1/orders/backfill-duplicate-checks/route.ts
Normal 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 });
|
||||
}
|
||||
Loading…
Reference in a new issue