feat(assign-api): hard-gate on ready_for_collection, drop auto-flip

This commit is contained in:
SJCoetz 2026-07-12 00:33:00 +02:00
parent ee9a0e1001
commit d78d479091
2 changed files with 69 additions and 10 deletions

View file

@ -37,7 +37,7 @@ describe("PATCH /api/v1/collection-map/items/[id]/assign", () => {
});
it("writes trimmed notes to the item when ready_notes is provided", async () => {
getOneMock.mockResolvedValueOnce({ id: "item-1" });
getOneMock.mockResolvedValueOnce({ id: "item-1", ready_for_collection: true });
updateMock.mockResolvedValueOnce({ id: "item-1" });
const res = await PATCH(
@ -115,4 +115,56 @@ describe("PATCH /api/v1/collection-map/items/[id]/assign", () => {
expect(payload.item_status).toBe("collection_pool");
expect(payload.drv_status).toBe("unassigned");
});
it("returns 409 when assigning a driver to an unready row", async () => {
getOneMock.mockResolvedValueOnce({ id: "item-1", ready_for_collection: false });
const res = await PATCH(
makeReq({ driverId: "drv-1", date: "2026-07-12" }),
{ params: Promise.resolve({ id: "item-1" }) },
);
expect(res.status).toBe(409);
const body = (await res.json()) as { error?: string };
expect(body.error).toMatch(/ready for collection/i);
expect(updateMock).not.toHaveBeenCalled();
});
it("succeeds and does NOT auto-flip ready_for_collection when assigning a ready row", async () => {
getOneMock.mockResolvedValueOnce({ id: "item-1", ready_for_collection: true });
updateMock.mockResolvedValueOnce({ id: "item-1" });
const res = await PATCH(
makeReq({ driverId: "drv-1", date: "2026-07-12" }),
{ params: Promise.resolve({ id: "item-1" }) },
);
expect(res.status).toBe(200);
const payload = updateMock.mock.calls[0][1] as Record<string, unknown>;
expect(payload.drv_id).toBe("drv-1");
expect(payload.item_status).toBe("assigned");
expect(payload.drv_status).toBe("assigned");
// Auto-flip is gone: patch must not include ready_for_collection=true.
expect(payload.ready_for_collection).toBeUndefined();
});
it("allows unassign from an unready row (supplier gate doesn't trap pool returns)", async () => {
getOneMock.mockResolvedValueOnce({
id: "item-1",
ready_for_collection: false,
assigned_at: "2026-07-09T00:00:00.000Z",
});
updateMock.mockResolvedValueOnce({ id: "item-1" });
const res = await PATCH(
makeReq({ date: "" }),
{ params: Promise.resolve({ id: "item-1" }) },
);
expect(res.status).toBe(200);
const payload = updateMock.mock.calls[0][1] as Record<string, unknown>;
expect(payload.assigned_at).toBe("");
expect(payload.item_status).toBe("collection_pool");
expect(payload.drv_status).toBe("unassigned");
});
});

View file

@ -77,15 +77,6 @@ export async function PATCH(
drv_id: typeof driverId === "string" ? driverId : "",
assigned_at: assignedAt,
});
// Assigning a driver means the supplier has been confirmed by
// the operator — flip `ready_for_collection` to true so the
// driver app's `getMyAssignedItems` filter surfaces the row.
// Un-assigning leaves the ready flag alone (the supplier-side
// gate doesn't change just because we're swapping which driver
// gets the pickup slot).
if (isAssigning) {
patch.ready_for_collection = true;
}
// Standalone ready-for-collection flip from the pool/map pages
// (operator marks a row ready without assigning a driver). When
// `ready_for_collection` is explicitly provided it wins over any
@ -112,6 +103,22 @@ export async function PATCH(
if (!current) {
return NextResponse.json({ error: `Order item ${id} not found` }, { status: 404 });
}
// Supplier-side gate: a row must be confirmed ready_for_collection
// before a driver can be assigned. Unassign always works (the
// operator must be able to return a row to the pool mid-flip).
// `ready_for_collection` is a boolean in PB; treat `false`,
// `null`, and `undefined` as not-ready. Auto-flip on assign has
// been intentionally removed — the row reaches ready via the
// mark-ready flow on /collection-pool, not via the assign path.
if (isAssigning && !current.ready_for_collection) {
return NextResponse.json(
{
error:
"Supplier hasn't confirmed this item is ready for collection. Mark it ready in /collection-pool before assigning a driver.",
},
{ status: 409 },
);
}
const updated = await pb.collection(getOrderItemsCollectionName()).update(id, patch);
return NextResponse.json({