diff --git a/src/lib/server/orders-service-review.test.ts b/src/lib/server/orders-service-review.test.ts index 9a3d81f..b2dfd2e 100644 --- a/src/lib/server/orders-service-review.test.ts +++ b/src/lib/server/orders-service-review.test.ts @@ -188,6 +188,88 @@ describe("updateOrderReviewFields", () => { expect(result.order_number).toBe("ORD-2026-07-001"); expect(result.customer).toBe("cust-1"); }); + + it("escapes embedded quotes in order_number before building the collision filter", async () => { + // An order_number containing `"` would break the filter + // expression unless we escape embedded quotes before + // interpolating. Spy on getList to capture the filter + // the service actually sends. + const order = await pbMock.collection("orders").create({ + order_number: "ORD-2026-07-001", + customer: "cust-1", + }); + + const originalCollection = pbMock.collection; + const captured: string[] = []; + pbMock.collection = (name: string) => { + const col = originalCollection(name); + if (name === "orders") { + const originalGetList = col.getList; + col.getList = vi.fn(async (...args: unknown[]) => { + const opts = (args[2] as { filter?: string } | undefined) || {}; + if (opts.filter) captured.push(opts.filter); + return originalGetList(...(args as Parameters)); + }); + } + return col; + }; + + try { + // Pick an order_number that contains a literal quote. The + // unescaped filter would be `order_number = "ORD "foo" && id != "..."`, + // which would either fail to parse or match the wrong rows. + await updateOrderReviewFields(order.id, { + order_number: 'ORD "weird"', + }); + + expect(captured.length).toBeGreaterThan(0); + // The filter MUST escape the `"` inside the literal — look for + // the backslash-escaped quote in the captured string. + const filter = captured[captured.length - 1]; + expect(filter).toContain('ORD \\"weird\\"'); + } finally { + pbMock.collection = originalCollection; + } + }); + + it("maps a PB unique-constraint write error back to OrderNumberCollisionError", async () => { + // Two callers race past the pre-write check; the loser's update + // then throws on the unique index. The service should catch that + // and surface a structured collision error (with the colliding + // row's id) rather than letting it bubble up as a 500. + const order = await pbMock.collection("orders").create({ + order_number: "ORD-2026-07-001", + customer: "cust-1", + }); + await pbMock.collection("orders").create({ + order_number: "ORD-2026-07-002", + customer: "cust-2", + }); + + const originalCollection = pbMock.collection; + pbMock.collection = (name: string) => { + const col = originalCollection(name); + if (name === "orders") { + col.update = vi.fn(async () => { + const err = new Error( + "UNIQUE constraint failed: orders.order_number", + ); + throw err; + }); + } + return col; + }; + + try { + await expect( + updateOrderReviewFields(order.id, { + order_number: "ORD-2026-07-002", + }), + ).rejects.toBeInstanceOf(OrderNumberCollisionError); + } finally { + pbMock.collection = originalCollection; + } + }); }); describe("updateOrderItemsPrices", () => { @@ -394,4 +476,49 @@ describe("updateOrderItemsPrices", () => { // markup_percent must mirror markup — the two fields stay in lock-step. expect(item.markup_percent).toBe(140); }); + + it("rejects items that belong to a different order", async () => { + // Two orders, each with its own item. + const orderA = await pbMock.collection("orders").create({ + order_number: "ORD-2026-07-001", + }); + const orderB = await pbMock.collection("orders").create({ + order_number: "ORD-2026-07-002", + }); + await pbMock.collection("order_items").create({ + id: "item-a", + order: orderA.id, + description: "A's drill", + quantity: 2, + selling_price: 50, + cost_price: 25, + markup: 100, + }); + await pbMock.collection("order_items").create({ + id: "item-b", + order: orderB.id, + description: "B's bit", + quantity: 1, + selling_price: 10, + cost_price: 5, + markup: 100, + }); + + // Caller targets order A but slips in order B's item — the cross- + // order check must catch it and not modify item-b. + const result = await updateOrderItemsPrices(orderA.id, [ + { id: "item-a", quantity: 7 }, + { id: "item-b", quantity: 99 }, + ]); + + expect(result.updated).toBe(1); + expect(result.failed).toEqual([ + { id: "item-b", error: "Item does not belong to this order" }, + ]); + + const itemA = await pbMock.collection("order_items").getOne("item-a"); + expect(itemA.quantity).toBe(7); + const itemB = await pbMock.collection("order_items").getOne("item-b"); + expect(itemB.quantity).toBe(1); // unchanged + }); });