convert actions
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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." };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user