feat(projects/orders): project status machine + bidirectional project-order sync; order reopen
- projects.service: VALID_TRANSITIONS (aktivni->dokonceny/zruseny, terminal->aktivni reopen), tolerant of legacy statuses; getProject returns valid_transitions; updateProject validates transitions (invalid_transition token -> Czech 400 in the route). - NEW project->order cascade, transactional with the project update: finishing/ cancelling a project completes/cancels its linked received order — only while the order is still open (never resurrects storno, never cancels completed); reopen never cascades. Direct tx write, no service recursion; cascaded order change gets its own audit row. - orders.service: reopen edges (dokoncena/stornovana -> v_realizaci); syncProjectStatus is reopen-aware (reopen skips project sync) and returns the cascaded projects for per-project audit rows in the route. - orders.schema: fix phantom 'zrusena' enum token -> canonical 'stornovana' (every storno PUT through the route 400ed with a raw English Zod message; latent until now because no UI sent it) + route-level regression test. - NEW project-order-status-sync.test.ts: 15 tests — both cascade directions, guards, reopen-no-cascade, legacy tolerance, forward-sync regression. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,12 @@ export default async function projectsRoutes(
|
||||
const result = await updateProject(id, parsed.data);
|
||||
if (!result) return error(reply, "Projekt nenalezen", 404);
|
||||
if ("error" in result) {
|
||||
if (result.error === "invalid_transition" && "currentStatus" in result)
|
||||
return error(
|
||||
reply,
|
||||
`Neplatný přechod stavu z "${result.currentStatus}" na "${result.newStatus}"`,
|
||||
400,
|
||||
);
|
||||
return error(
|
||||
reply,
|
||||
result.error,
|
||||
@@ -138,6 +144,7 @@ export default async function projectsRoutes(
|
||||
);
|
||||
}
|
||||
|
||||
const statusChanged = result.old_status !== result.status;
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
@@ -145,7 +152,26 @@ export default async function projectsRoutes(
|
||||
entityType: "project",
|
||||
entityId: id,
|
||||
description: `Upraven projekt ${result.name}`,
|
||||
oldValues: statusChanged ? { status: result.old_status } : undefined,
|
||||
newValues: statusChanged ? { status: result.status } : undefined,
|
||||
});
|
||||
// The project→order cascade updated the linked order too — give the
|
||||
// order its own audit trail entry.
|
||||
if (result.synced_order) {
|
||||
const so = result.synced_order;
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "update",
|
||||
entityType: "order",
|
||||
entityId: so.id,
|
||||
description: `Objednávka ${so.order_number ?? `#${so.id}`} automaticky ${
|
||||
so.to === "dokoncena" ? "dokončena" : "stornována"
|
||||
} (synchronizace s projektem)`,
|
||||
oldValues: { status: so.from },
|
||||
newValues: { status: so.to },
|
||||
});
|
||||
}
|
||||
return success(reply, { id }, 200, "Projekt byl uložen");
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user