feat(orders): manual order PO attachment + price items (multipart)
Extend createOrder service to accept optional attachmentBuffer/attachmentName parameters and persist them. Restructure the POST / multipart branch in the orders route to dispatch on quotationId presence: from-quotation path is unchanged, new manual-multipart path validates with CreateOrderSchema and forwards the attachment. Add two TDD tests covering price line items and PO file attachment storage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -97,3 +97,46 @@ describe("createProject (manual, standalone)", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("createOrder with price line item + attachment", () => {
|
||||
it("stores a single line item with the entered price", async () => {
|
||||
const res = await createOrder({
|
||||
...baseOrder,
|
||||
create_project: false,
|
||||
items: [
|
||||
{
|
||||
description: "Práce",
|
||||
quantity: 1,
|
||||
unit_price: 12345,
|
||||
is_included_in_total: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect("id" in res).toBe(true);
|
||||
if (!("id" in res)) return;
|
||||
createdOrderIds.push(res.id);
|
||||
const items = await prisma.order_items.findMany({
|
||||
where: { order_id: res.id },
|
||||
});
|
||||
expect(items.length).toBe(1);
|
||||
expect(Number(items[0].unit_price)).toBe(12345);
|
||||
});
|
||||
|
||||
it("stores an uploaded PO attachment", async () => {
|
||||
const buf = Buffer.from("%PDF-1.4 fake");
|
||||
const res = await createOrder(
|
||||
{ ...baseOrder, create_project: false },
|
||||
buf,
|
||||
"po.pdf",
|
||||
);
|
||||
expect("id" in res).toBe(true);
|
||||
if (!("id" in res)) return;
|
||||
createdOrderIds.push(res.id);
|
||||
const order = await prisma.orders.findUnique({
|
||||
where: { id: res.id },
|
||||
select: { attachment_name: true, attachment_data: true },
|
||||
});
|
||||
expect(order?.attachment_name).toBe("po.pdf");
|
||||
expect(order?.attachment_data).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,7 +104,6 @@ export default async function ordersRoutes(
|
||||
request.headers["content-type"]?.includes("multipart");
|
||||
|
||||
if (isMultipart) {
|
||||
// === Order from quotation flow (multipart) ===
|
||||
const fields: Record<string, string> = {};
|
||||
let attachmentBuffer: Buffer | null = null;
|
||||
let attachmentName: string | null = null;
|
||||
@@ -119,37 +118,82 @@ export default async function ordersRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
const quotationId = parseInt(fields.quotationId, 10);
|
||||
const customerOrderNumber = fields.customerOrderNumber || "";
|
||||
|
||||
if (!quotationId || isNaN(quotationId)) {
|
||||
return error(reply, "Chybí ID nabídky", 400);
|
||||
// From-quotation flow (multipart with attachment)
|
||||
if (fields.quotationId) {
|
||||
const quotationId = parseInt(fields.quotationId, 10);
|
||||
const customerOrderNumber = fields.customerOrderNumber || "";
|
||||
if (!quotationId || isNaN(quotationId)) {
|
||||
return error(reply, "Chybí ID nabídky", 400);
|
||||
}
|
||||
const result = await createOrderFromQuotation({
|
||||
quotationId,
|
||||
customerOrderNumber,
|
||||
attachmentBuffer,
|
||||
attachmentName,
|
||||
});
|
||||
if ("error" in result)
|
||||
return error(reply, result.error!, result.status!);
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "create",
|
||||
entityType: "order",
|
||||
entityId: result.data.order_id,
|
||||
description: `Vytvořena objednávka ${result.data.order_number} z nabídky #${result.data.quotationId}`,
|
||||
});
|
||||
return success(
|
||||
reply,
|
||||
{
|
||||
order_id: result.data.order_id,
|
||||
id: result.data.id,
|
||||
order_number: result.data.order_number,
|
||||
},
|
||||
201,
|
||||
"Objednávka byla vytvořena",
|
||||
);
|
||||
}
|
||||
|
||||
const result = await createOrderFromQuotation({
|
||||
quotationId,
|
||||
customerOrderNumber,
|
||||
// Manual order (multipart, with optional PO attachment)
|
||||
const rawManual: Record<string, unknown> = { ...fields };
|
||||
if (typeof fields.items === "string" && fields.items.length > 0) {
|
||||
try {
|
||||
rawManual.items = JSON.parse(fields.items);
|
||||
} catch {
|
||||
return error(reply, "Neplatná data položek", 400);
|
||||
}
|
||||
}
|
||||
const manualParsed = parseBody(CreateOrderSchema, rawManual);
|
||||
if ("error" in manualParsed)
|
||||
return error(reply, manualParsed.error, 400);
|
||||
|
||||
const result = await createOrder(
|
||||
manualParsed.data,
|
||||
attachmentBuffer,
|
||||
attachmentName,
|
||||
});
|
||||
);
|
||||
if ("error" in result)
|
||||
return error(reply, result.error!, result.status!);
|
||||
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "create",
|
||||
entityType: "order",
|
||||
entityId: result.data.order_id,
|
||||
description: `Vytvořena objednávka ${result.data.order_number} z nabídky #${result.data.quotationId}`,
|
||||
entityId: result.id,
|
||||
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(
|
||||
reply,
|
||||
{
|
||||
order_id: result.data.order_id,
|
||||
id: result.data.id,
|
||||
order_number: result.data.order_number,
|
||||
},
|
||||
{ id: result.id },
|
||||
201,
|
||||
"Objednávka byla vytvořena",
|
||||
);
|
||||
|
||||
@@ -291,7 +291,11 @@ interface CreateOrderData {
|
||||
create_project?: boolean;
|
||||
}
|
||||
|
||||
export async function createOrder(body: CreateOrderData) {
|
||||
export async function createOrder(
|
||||
body: CreateOrderData,
|
||||
attachmentBuffer?: Buffer | null,
|
||||
attachmentName?: string | null,
|
||||
) {
|
||||
try {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const orderNumber =
|
||||
@@ -323,6 +327,10 @@ export async function createOrder(body: CreateOrderData) {
|
||||
scope_title: body.scope_title ?? null,
|
||||
scope_description: body.scope_description ?? null,
|
||||
notes: body.notes ?? null,
|
||||
attachment_data: attachmentBuffer
|
||||
? new Uint8Array(attachmentBuffer)
|
||||
: null,
|
||||
attachment_name: attachmentName || null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user