feat(orders): add since=YYYY-MM-DD to backfill endpoint for scoped re-scans
Lets the operator scope a backfill to recent orders so off-peak sanity passes don't disturb the active workflow. e.g.: POST /api/v1/orders/backfill-duplicate-checks?limit=200&since=2026-08-24 rescans the last 7 days up to 200 rows. Combines with limit/skip; rejects malformed dates with 400 instead of silently no-op'ing.
This commit is contained in:
parent
b4855a9c9f
commit
8ae4592108
2 changed files with 58 additions and 0 deletions
|
|
@ -120,6 +120,37 @@ describe("POST /api/v1/orders/backfill-duplicate-checks", () => {
|
|||
expect((await res.json()).nextSkip).toBe(9);
|
||||
});
|
||||
|
||||
it("applies a since filter to scope the backfill to recent orders", async () => {
|
||||
const { getAdminPocketBase } = await import("@/lib/server/pocketbase-admin");
|
||||
const getListSpy = vi.fn().mockResolvedValue({
|
||||
items: [{ id: "recent-1" }],
|
||||
totalItems: 1,
|
||||
page: 1,
|
||||
perPage: 500,
|
||||
});
|
||||
vi.mocked(getAdminPocketBase).mockResolvedValueOnce({
|
||||
autoCancellation: vi.fn(),
|
||||
collection: () => ({ getList: getListSpy }),
|
||||
} as unknown as Awaited<ReturnType<typeof getAdminPocketBase>>);
|
||||
|
||||
const res = await POST(backfillRequest("?limit=500&since=2026-08-24"));
|
||||
expect(res.status).toBe(200);
|
||||
// The since param translates to a PB `created >= "<date> 00:00:00.000Z"`
|
||||
// filter so the backfill only re-scans orders from that day forward.
|
||||
expect(getListSpy).toHaveBeenCalledWith(
|
||||
1,
|
||||
500,
|
||||
expect.objectContaining({ filter: 'created >= "2026-08-24 00:00:00.000Z"' }),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns 400 when since is malformed", async () => {
|
||||
const res = await POST(backfillRequest("?since=not-a-date"));
|
||||
expect(res.status).toBe(400);
|
||||
const body = await res.json();
|
||||
expect(body.error).toMatch(/since must be YYYY-MM-DD/);
|
||||
});
|
||||
|
||||
it("returns 401 when AGENT_API_KEY is missing", async () => {
|
||||
delete process.env.AGENT_API_KEY;
|
||||
const res = await POST(backfillRequest());
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@
|
|||
* POST /api/v1/orders/backfill-duplicate-checks?limit=500&skip=$skip
|
||||
* done
|
||||
*
|
||||
* Scoped backfill (recent orders only): pass `?since=YYYY-MM-DD`
|
||||
* (ISO date) to filter to orders created on or after that date.
|
||||
* Combines with `limit`/`skip` — e.g.
|
||||
* `?since=2026-08-24&limit=200` re-scans the last 7 days of orders
|
||||
* (or however far back `since` reaches) up to 200 rows. Useful for
|
||||
* off-peak sanity passes without disturbing the operator workflow.
|
||||
*
|
||||
* Why not a single call? At ~25 orders/min on a busy PB instance the
|
||||
* matcher is N+1 on candidates — a 5,000-order table is ~3 hours of
|
||||
* sequential work. Chunking keeps each response under Next.js's
|
||||
|
|
@ -77,6 +84,7 @@ export async function POST(req: NextRequest) {
|
|||
const { searchParams } = new URL(req.url);
|
||||
const limitRaw = Number(searchParams.get("limit"));
|
||||
const skipRaw = Number(searchParams.get("skip"));
|
||||
const sinceRaw = searchParams.get("since");
|
||||
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : DEFAULT_LIMIT;
|
||||
const skip = Number.isFinite(skipRaw) && skipRaw >= 0 ? Math.floor(skipRaw) : 0;
|
||||
if (limit > MAX_LIMIT) {
|
||||
|
|
@ -86,6 +94,21 @@ export async function POST(req: NextRequest) {
|
|||
);
|
||||
}
|
||||
|
||||
// `since` accepts an ISO date (YYYY-MM-DD). PB's date filter expects
|
||||
// the same wire format the matcher writes — `YYYY-MM-DD HH:MM:SS.sssZ`,
|
||||
// so we anchor `since` at the start of that day in UTC. We reject
|
||||
// malformed dates with a 400 rather than silently no-op'ing.
|
||||
let sinceFilter: string | null = null;
|
||||
if (sinceRaw) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(sinceRaw)) {
|
||||
return NextResponse.json(
|
||||
{ error: `since must be YYYY-MM-DD, got "${sinceRaw}"` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
sinceFilter = `created >= "${sinceRaw} 00:00:00.000Z"`;
|
||||
}
|
||||
|
||||
const pb = await getAdminPocketBase();
|
||||
pb.autoCancellation(false);
|
||||
|
||||
|
|
@ -102,9 +125,13 @@ export async function POST(req: NextRequest) {
|
|||
{
|
||||
fields: "id",
|
||||
sort: "created",
|
||||
...(sinceFilter ? { filter: sinceFilter } : {}),
|
||||
},
|
||||
);
|
||||
const slice = orders.items;
|
||||
// `hasMore` accounts for the date filter: PB's `totalItems` is the
|
||||
// count of rows matching the filter, not the full collection size.
|
||||
// So `nextSkip + slice.length < filteredTotal` is the right bound.
|
||||
const hasMore = skip + slice.length < orders.totalItems;
|
||||
|
||||
let ordersProcessed = 0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue