feat(buyer-tasks): markGroupReady with cross-order check

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-07-29 20:00:45 +02:00
parent 98a0bc4143
commit 05242f09f4
2 changed files with 95 additions and 0 deletions

View file

@ -308,4 +308,58 @@ describe("generatePoForGroup", () => {
const { generatePoForGroup } = await import("./buyer-tasks-service");
await expect(generatePoForGroup("order-1", "", [{ id: "item-1", costPrice: 25, quantity: 1 }])).rejects.toThrow(/supplierId/i);
});
});
describe("markGroupReady", () => {
it("updates all items to ready_delivery", async () => {
const getOne = vi.fn().mockResolvedValue({ order: "order-1" });
const updateItem = vi.fn().mockResolvedValue({});
const admin = {
collection: vi.fn().mockReturnValue({ getOne, update: updateItem }),
};
vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never);
const { markGroupReady } = await import("./buyer-tasks-service");
const result = await markGroupReady("order-1", "sup-1", [
{ id: "item-1" },
{ id: "item-2" },
]);
expect(result.updated).toBe(2);
expect(result.failed).toEqual([]);
expect(updateItem).toHaveBeenCalledWith("item-1", { item_status: "ready_delivery" });
expect(updateItem).toHaveBeenCalledWith("item-2", { item_status: "ready_delivery" });
});
it("returns 0 when no items provided", async () => {
const admin = { collection: vi.fn() };
vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never);
const { markGroupReady } = await import("./buyer-tasks-service");
const result = await markGroupReady("order-1", "sup-1", []);
expect(result.updated).toBe(0);
expect(result.failed).toEqual([]);
});
it("collects per-item failures", async () => {
const getOne = vi.fn().mockResolvedValue({ order: "order-1" });
const updateItem = vi.fn().mockImplementation((id: string) => {
if (id === "item-2") throw new Error("PB error");
return Promise.resolve({});
});
const admin = {
collection: vi.fn().mockReturnValue({ getOne, update: updateItem }),
};
vi.mocked(getAdminPocketBase).mockResolvedValue(admin as never);
const { markGroupReady } = await import("./buyer-tasks-service");
const result = await markGroupReady("order-1", "sup-1", [
{ id: "item-1" },
{ id: "item-2" },
]);
expect(result.updated).toBe(1);
expect(result.failed).toEqual([{ id: "item-2", error: "PB error" }]);
});
});

View file

@ -146,3 +146,44 @@ export async function generatePoForGroup(
return po.id;
}
export async function markGroupReady(
orderId: string,
supplierId: string,
items: Array<{ id: string }>
): Promise<{ updated: number; failed: Array<{ id: string; error: string }> }> {
if (!orderId || !supplierId) {
throw new Error("orderId and supplierId are required");
}
if (items.length === 0) return { updated: 0, failed: [] };
const admin = await getAdminPocketBase();
const failed: Array<{ id: string; error: string }> = [];
let updated = 0;
for (const item of items) {
// Cross-order check: verify the item belongs to this order before
// mutating it. Same pattern as updateOrderItemsPrices.
try {
const current = await admin.collection("order_items").getOne(item.id);
if (current.order !== orderId) {
failed.push({
id: item.id,
error: "Item does not belong to this order",
});
continue;
}
await admin.collection("order_items").update(item.id, {
item_status: "ready_delivery",
});
updated += 1;
} catch (err) {
failed.push({
id: item.id,
error: err instanceof Error ? err.message : "Unknown error",
});
}
}
return { updated, failed };
}