convert actions

This commit is contained in:
2026-03-14 23:16:42 -05:00
parent d44d97e47b
commit 9d233a0c3d
4 changed files with 232 additions and 18 deletions

View File

@@ -7,10 +7,12 @@ import { fail, ok } from "../../lib/http.js";
import { requirePermissions } from "../../lib/rbac.js";
import { inventoryUnitsOfMeasure } from "@mrp/shared/dist/inventory/types.js";
import {
convertQuoteToSalesOrder,
createSalesDocument,
getSalesDocumentById,
listSalesCustomerOptions,
listSalesDocuments,
updateSalesDocumentStatus,
updateSalesDocument,
} from "./service.js";
@@ -45,6 +47,10 @@ const salesListQuerySchema = z.object({
status: z.enum(salesDocumentStatuses).optional(),
});
const salesStatusUpdateSchema = z.object({
status: z.enum(salesDocumentStatuses),
});
function getRouteParam(value: unknown) {
return typeof value === "string" ? value : null;
}
@@ -111,6 +117,39 @@ salesRouter.put("/quotes/:quoteId", requirePermissions([permissions.salesWrite])
return ok(response, result.document);
});
salesRouter.patch("/quotes/:quoteId/status", requirePermissions([permissions.salesWrite]), async (request, response) => {
const quoteId = getRouteParam(request.params.quoteId);
if (!quoteId) {
return fail(response, 400, "INVALID_INPUT", "Quote id is invalid.");
}
const parsed = salesStatusUpdateSchema.safeParse(request.body);
if (!parsed.success) {
return fail(response, 400, "INVALID_INPUT", "Quote status payload is invalid.");
}
const result = await updateSalesDocumentStatus("QUOTE", quoteId, parsed.data.status);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
return ok(response, result.document);
});
salesRouter.post("/quotes/:quoteId/convert", requirePermissions([permissions.salesWrite]), async (request, response) => {
const quoteId = getRouteParam(request.params.quoteId);
if (!quoteId) {
return fail(response, 400, "INVALID_INPUT", "Quote id is invalid.");
}
const result = await convertQuoteToSalesOrder(quoteId);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
return ok(response, result.document, 201);
});
salesRouter.get("/orders", requirePermissions([permissions.salesRead]), async (request, response) => {
const parsed = salesListQuerySchema.safeParse(request.query);
if (!parsed.success) {
@@ -172,3 +211,22 @@ salesRouter.put("/orders/:orderId", requirePermissions([permissions.salesWrite])
return ok(response, result.document);
});
salesRouter.patch("/orders/:orderId/status", requirePermissions([permissions.salesWrite]), async (request, response) => {
const orderId = getRouteParam(request.params.orderId);
if (!orderId) {
return fail(response, 400, "INVALID_INPUT", "Sales order id is invalid.");
}
const parsed = salesStatusUpdateSchema.safeParse(request.body);
if (!parsed.success) {
return fail(response, 400, "INVALID_INPUT", "Sales order status payload is invalid.");
}
const result = await updateSalesDocumentStatus("ORDER", orderId, parsed.data.status);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
return ok(response, result.document);
});

View File

@@ -329,3 +329,61 @@ export async function updateSalesDocument(type: SalesDocumentType, documentId: s
const detail = await getSalesDocumentById(type, documentId);
return detail ? { ok: true as const, document: detail } : { ok: false as const, reason: "Unable to load saved document." };
}
export async function updateSalesDocumentStatus(type: SalesDocumentType, documentId: string, status: SalesDocumentStatus) {
const existing = await documentConfig[type].findUnique({
where: { id: documentId },
select: { id: true },
});
if (!existing) {
return { ok: false as const, reason: "Sales document was not found." };
}
await documentConfig[type].update({
where: { id: documentId },
data: { status },
select: { id: true },
});
const detail = await getSalesDocumentById(type, documentId);
return detail ? { ok: true as const, document: detail } : { ok: false as const, reason: "Unable to load updated document." };
}
export async function convertQuoteToSalesOrder(quoteId: string) {
const quote = await documentConfig.QUOTE.findUnique({
where: { id: quoteId },
include: buildInclude("QUOTE"),
});
if (!quote) {
return { ok: false as const, reason: "Quote was not found." };
}
const mappedQuote = mapDocument(quote as SalesDocumentRecord);
const nextOrderNumber = await nextDocumentNumber("ORDER");
const created = await documentConfig.ORDER.create({
data: {
documentNumber: nextOrderNumber,
customerId: mappedQuote.customerId,
status: "DRAFT",
issueDate: new Date(),
notes: mappedQuote.notes ? `Converted from ${mappedQuote.documentNumber}\n\n${mappedQuote.notes}` : `Converted from ${mappedQuote.documentNumber}`,
lines: {
create: mappedQuote.lines.map((line) => ({
itemId: line.itemId,
description: line.description,
quantity: line.quantity,
unitOfMeasure: line.unitOfMeasure,
unitPrice: line.unitPrice,
position: line.position,
})),
},
},
select: { id: true },
});
const order = await getSalesDocumentById("ORDER", created.id);
return order ? { ok: true as const, document: order } : { ok: false as const, reason: "Unable to load converted sales order." };
}