From ffae1a4e74c249dcdafb394eacfffcc970e8c24d Mon Sep 17 00:00:00 2001 From: BOHA Date: Wed, 10 Jun 2026 19:02:19 +0200 Subject: [PATCH] fix(offers): deleting an order returns the source offer to active deleteOrder only nulled quotations.order_id and left status "ordered" - a dead end (ordered only transitions to invalidated), so the offer could never be ordered again and the "Vytvorit objednavku" button stayed hidden. The delete transaction now reverts ordered -> active together with the back-reference clear; regression test included. Co-Authored-By: Claude Fable 5 --- src/__tests__/manual-create.test.ts | 38 +++++++++++++++++++++++++++++ src/services/orders.service.ts | 11 ++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/__tests__/manual-create.test.ts b/src/__tests__/manual-create.test.ts index 2e1e505..839ffc4 100644 --- a/src/__tests__/manual-create.test.ts +++ b/src/__tests__/manual-create.test.ts @@ -184,6 +184,44 @@ describe("createOrderFromQuotation (auto-project gets its OWN number)", () => { }); }); +describe("deleteOrder reverts the source offer (stuck-ordered regression)", () => { + it("returns the linked quotation to active with order_id cleared", async () => { + const quotation = await prisma.quotations.create({ + data: { + quotation_number: `Q-REVERT-${Date.now()}`, + status: "active", + currency: "CZK", + language: "cs", + }, + }); + createdQuotationIds.push(quotation.id); + + const res = await createOrderFromQuotation({ quotationId: quotation.id }); + if (!("data" in res) || !res.data) failResult(res); + const project = await prisma.projects.findFirst({ + where: { order_id: res.data.order_id }, + }); + if (project) createdProjectIds.push(project.id); + + const afterCreate = await prisma.quotations.findUnique({ + where: { id: quotation.id }, + }); + expect(afterCreate!.status).toBe("ordered"); + expect(afterCreate!.order_id).toBe(res.data.order_id); + + const del = await deleteOrder(res.data.order_id); + if ("error" in del) failResult(del); + + // The offer must be orderable again — `ordered` with no order_id is a + // dead end (the transition table only allows ordered -> invalidated). + const afterDelete = await prisma.quotations.findUnique({ + where: { id: quotation.id }, + }); + expect(afterDelete!.order_id).toBeNull(); + expect(afterDelete!.status).toBe("active"); + }); +}); + describe("deleteOrder releases the project number (release regression)", () => { it("frees the auto-created project's number back to the project sequence", async () => { // The project sequence is reset in afterEach, so the preview before the diff --git a/src/services/orders.service.ts b/src/services/orders.service.ts index 73aba4c..27d09c1 100644 --- a/src/services/orders.service.ts +++ b/src/services/orders.service.ts @@ -761,7 +761,16 @@ export async function deleteOrder(id: number, deleteFiles = false) { } } - // Clear quotation back-reference (matching PHP) + // Clear the quotation back-reference AND revert its status: an + // `ordered` offer with no order is a dead end (the transition table + // only allows ordered -> invalidated), so it goes back to `active`, + // making it orderable again. + await tx.quotations.updateMany({ + where: { order_id: id, status: "ordered" }, + data: { order_id: null, status: "active", modified_at: new Date() }, + }); + // Backstop for rows in any other status (shouldn't exist — only an + // ordered offer can hold an order_id). await tx.quotations.updateMany({ where: { order_id: id }, data: { order_id: null },