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 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-10 19:02:19 +02:00
parent 5c9ebddf50
commit ffae1a4e74
2 changed files with 48 additions and 1 deletions

View File

@@ -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)", () => { describe("deleteOrder releases the project number (release regression)", () => {
it("frees the auto-created project's number back to the project sequence", async () => { 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 // The project sequence is reset in afterEach, so the preview before the

View File

@@ -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({ await tx.quotations.updateMany({
where: { order_id: id }, where: { order_id: id },
data: { order_id: null }, data: { order_id: null },