feat(assign-api): hard-gate on ready_for_collection, drop auto-flip
This commit is contained in:
parent
ee9a0e1001
commit
d78d479091
2 changed files with 69 additions and 10 deletions
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in a new issue