From e0d214c94f2c3d734557efbbb23b331ee09f68a6 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 27 Aug 2026 00:02:25 +0200 Subject: [PATCH] test(quotes): cover spec test cases #3 and #5 for no-quote Closes the two IMPORTANT test-coverage gaps from the final implementation review. Pure additions; no production code changes. - Spec test #3: submitQuote({partial:true}) accepts zero-priced items when is_no_quote=true (status flips to partial_approval_pending) - Spec test #5: strengthen the no-supplier + no-quote assertion to check the missing array directly, not just the failure.code Co-Authored-By: Claude --- src/lib/server/quotes-service.test.ts | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/lib/server/quotes-service.test.ts b/src/lib/server/quotes-service.test.ts index 7dd442e..dc90b67 100644 --- a/src/lib/server/quotes-service.test.ts +++ b/src/lib/server/quotes-service.test.ts @@ -1255,6 +1255,18 @@ describe("diagnosePdfReadiness with is_no_quote", () => { expect(result.failure?.details?.issues?.[0]?.missing).toContain( "no supplier" ); + // Stronger assertion (spec test case #5): confirm the `details.issues` + // array directly contains an object whose `missing` list includes + // "no supplier". Guards against future refactors that drop the + // missing-array shape (e.g. moving the supplier check out of the + // per-issue structure). + expect(result.failure?.details?.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + missing: expect.arrayContaining(["no supplier"]), + }), + ]), + ); // And the pricing/processed checks must NOT appear in the missing list // (since the operator marked this as a decline). const missing = result.failure?.details?.issues?.[0]?.missing ?? []; @@ -1909,6 +1921,41 @@ describe("submitQuote with is_no_quote", () => { expect(stored.status).toBe("approval_pending"); }); + it("flips a new no-quote quote to partial_approval_pending when partial:true even with zero-priced items", async () => { + // Spec test case #3: `submitQuote` accepts zero-priced items when + // `is_no_quote` true AND `partial: true`. Mirrors the previous test + // (spec #1) but flips on the partial flag and asserts the + // `partial_approval_pending` target status — not the full-approval + // path. + await pbMock.collection("quotes").create({ + id: "q-test", + quote_number: "Q-2026-08-004", + status: "new", + is_no_quote: true, + }); + await pbMock.collection("quote_items").create({ + id: "qi-zero", + quote: "q-test", + description: "M16 bolt", + selling_price: 0, + }); + await pbMock.collection("quote_items").create({ + id: "qi-priced", + quote: "q-test", + description: "Hex nut", + selling_price: 100, + }); + + const result = await submitQuote("q-test", undefined, { partial: true }); + expect(result.statusChanged).toBe(true); + expect(result.quote.status).toBe("partial_approval_pending"); + expect(result.targetStatus).toBe("partial_approval_pending"); + + // PB row updated in place. + const stored = await pbMock.collection("quotes").getOne("q-test"); + expect(stored.status).toBe("partial_approval_pending"); + }); + it("still throws unpriced_items for a regular (non-no-quote) quote with zero-priced items", async () => { await pbMock.collection("quotes").create({ id: "q-test",