feat(orders): manual order optionally creates a linked project
Add `create_project` flag (default true) to `CreateOrderSchema` and `CreateOrderData`. When true and `quotation_id` is absent, `createOrder` creates a `projects` row sharing the order's shared-pool number and `order_id` link — mirroring the from-quotation flow. The route audits the new project creation when it occurs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { describe, it, expect, afterEach } from "vitest";
|
import { describe, it, expect, afterEach } from "vitest";
|
||||||
import prisma from "../config/database";
|
import prisma from "../config/database";
|
||||||
import { createProject } from "../services/projects.service";
|
import { createProject } from "../services/projects.service";
|
||||||
|
import { createOrder } from "../services/orders.service";
|
||||||
|
|
||||||
const createdProjectIds: number[] = [];
|
const createdProjectIds: number[] = [];
|
||||||
const createdOrderIds: number[] = [];
|
const createdOrderIds: number[] = [];
|
||||||
@@ -15,6 +16,62 @@ afterEach(async () => {
|
|||||||
await prisma.number_sequences.deleteMany({ where: { type: "shared" } });
|
await prisma.number_sequences.deleteMany({ where: { type: "shared" } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const baseOrder = {
|
||||||
|
status: "prijata" as const,
|
||||||
|
currency: "CZK",
|
||||||
|
language: "cs",
|
||||||
|
vat_rate: 21,
|
||||||
|
apply_vat: true,
|
||||||
|
exchange_rate: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("createOrder (manual, optional linked project)", () => {
|
||||||
|
it("creates an order AND a project sharing one number when create_project=true", async () => {
|
||||||
|
const res = await createOrder({
|
||||||
|
...baseOrder,
|
||||||
|
scope_title: "Ruční zakázka",
|
||||||
|
create_project: true,
|
||||||
|
});
|
||||||
|
expect("id" in res).toBe(true);
|
||||||
|
if (!("id" in res)) return;
|
||||||
|
createdOrderIds.push(res.id);
|
||||||
|
expect(res.project_id).toBeTruthy();
|
||||||
|
const project = await prisma.projects.findUnique({
|
||||||
|
where: { id: res.project_id! },
|
||||||
|
});
|
||||||
|
if (project) createdProjectIds.push(project.id);
|
||||||
|
expect(project?.project_number).toBe(res.order_number);
|
||||||
|
expect(project?.order_id).toBe(res.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates only the order when create_project=false", async () => {
|
||||||
|
const res = await createOrder({ ...baseOrder, create_project: false });
|
||||||
|
expect("id" in res).toBe(true);
|
||||||
|
if (!("id" in res)) return;
|
||||||
|
createdOrderIds.push(res.id);
|
||||||
|
expect(res.project_id).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shared pool coherence (tracking)", () => {
|
||||||
|
it("order → standalone project → order produce three distinct shared numbers", async () => {
|
||||||
|
const o1 = await createOrder({ ...baseOrder, create_project: true });
|
||||||
|
if ("id" in o1) {
|
||||||
|
createdOrderIds.push(o1.id);
|
||||||
|
if (o1.project_id) createdProjectIds.push(o1.project_id);
|
||||||
|
}
|
||||||
|
const p = await createProject({ name: "Mezi-projekt", status: "aktivni" });
|
||||||
|
if ("project_number" in p) createdProjectIds.push(p.id);
|
||||||
|
const o2 = await createOrder({ ...baseOrder, create_project: false });
|
||||||
|
if ("id" in o2) createdOrderIds.push(o2.id);
|
||||||
|
|
||||||
|
const n1 = "id" in o1 ? o1.order_number : null;
|
||||||
|
const np = "project_number" in p ? p.project_number : null;
|
||||||
|
const n2 = "id" in o2 ? o2.order_number : null;
|
||||||
|
expect(new Set([n1, np, n2]).size).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("createProject (manual, standalone)", () => {
|
describe("createProject (manual, standalone)", () => {
|
||||||
it("assigns a shared number and is not linked to an order", async () => {
|
it("assigns a shared number and is not linked to an order", async () => {
|
||||||
const result = await createProject({
|
const result = await createProject({
|
||||||
|
|||||||
@@ -216,6 +216,16 @@ export default async function ordersRoutes(
|
|||||||
entityId: result.id,
|
entityId: result.id,
|
||||||
description: `Vytvořena objednávka ${result.order_number}`,
|
description: `Vytvořena objednávka ${result.order_number}`,
|
||||||
});
|
});
|
||||||
|
if (result.project_id) {
|
||||||
|
await logAudit({
|
||||||
|
request,
|
||||||
|
authData: request.authData,
|
||||||
|
action: "create",
|
||||||
|
entityType: "project",
|
||||||
|
entityId: result.project_id,
|
||||||
|
description: `Vytvořen projekt k objednávce ${result.order_number}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
return success(
|
return success(
|
||||||
reply,
|
reply,
|
||||||
{ id: result.id },
|
{ id: result.id },
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ export const CreateOrderSchema = z.object({
|
|||||||
notes: z.string().nullish(),
|
notes: z.string().nullish(),
|
||||||
items: z.array(OrderItemSchema).optional(),
|
items: z.array(OrderItemSchema).optional(),
|
||||||
sections: z.array(OrderSectionSchema).optional(),
|
sections: z.array(OrderSectionSchema).optional(),
|
||||||
|
create_project: z
|
||||||
|
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
|
||||||
|
.optional()
|
||||||
|
.default(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateOrderSchema = z.object({
|
export const UpdateOrderSchema = z.object({
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ interface CreateOrderData {
|
|||||||
notes?: string | null;
|
notes?: string | null;
|
||||||
items?: OrderItemInput[];
|
items?: OrderItemInput[];
|
||||||
sections?: OrderSectionInput[];
|
sections?: OrderSectionInput[];
|
||||||
|
create_project?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createOrder(body: CreateOrderData) {
|
export async function createOrder(body: CreateOrderData) {
|
||||||
@@ -352,7 +353,27 @@ export async function createOrder(body: CreateOrderData) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { id: order.id, order_number: order.order_number };
|
let projectId: number | undefined;
|
||||||
|
// Only auto-create a project for genuine offer-less orders; share the
|
||||||
|
// order's number (same shared pool) exactly like the from-quotation flow.
|
||||||
|
if (body.create_project !== false && body.quotation_id == null) {
|
||||||
|
const project = await tx.projects.create({
|
||||||
|
data: {
|
||||||
|
project_number: orderNumber,
|
||||||
|
name: body.scope_title || body.customer_order_number || orderNumber,
|
||||||
|
customer_id: order.customer_id,
|
||||||
|
order_id: order.id,
|
||||||
|
status: "aktivni",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
projectId = project.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: order.id,
|
||||||
|
order_number: order.order_number,
|
||||||
|
project_id: projectId,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error && "status" in err) {
|
if (err instanceof Error && "status" in err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user