import { permissions, purchaseOrderStatuses } from "@mrp/shared"; import { inventoryUnitsOfMeasure } from "@mrp/shared/dist/inventory/types.js"; import { Router } from "express"; import { z } from "zod"; import { fail, ok } from "../../lib/http.js"; import { requirePermissions } from "../../lib/rbac.js"; import { createPurchaseReceipt, createPurchaseOrder, getPurchaseOrderById, listPurchaseOrders, listPurchaseVendorOptions, updatePurchaseOrder, updatePurchaseOrderStatus, } from "./service.js"; const purchaseLineSchema = z.object({ itemId: z.string().trim().min(1), description: z.string(), quantity: z.number().int().positive(), unitOfMeasure: z.enum(inventoryUnitsOfMeasure), unitCost: z.number().nonnegative(), position: z.number().int().nonnegative(), }); const purchaseOrderSchema = z.object({ vendorId: z.string().trim().min(1), status: z.enum(purchaseOrderStatuses), issueDate: z.string().datetime(), taxPercent: z.number().min(0).max(100), freightAmount: z.number().nonnegative(), notes: z.string(), lines: z.array(purchaseLineSchema), }); const purchaseListQuerySchema = z.object({ q: z.string().optional(), status: z.enum(purchaseOrderStatuses).optional(), }); const purchaseStatusUpdateSchema = z.object({ status: z.enum(purchaseOrderStatuses), }); const purchaseReceiptLineSchema = z.object({ purchaseOrderLineId: z.string().trim().min(1), quantity: z.number().int().positive(), }); const purchaseReceiptSchema = z.object({ receivedAt: z.string().datetime(), warehouseId: z.string().trim().min(1), locationId: z.string().trim().min(1), notes: z.string(), lines: z.array(purchaseReceiptLineSchema), }); function getRouteParam(value: unknown) { return typeof value === "string" ? value : null; } export const purchasingRouter = Router(); purchasingRouter.get("/vendors/options", requirePermissions(["purchasing.read"]), async (_request, response) => { return ok(response, await listPurchaseVendorOptions()); }); purchasingRouter.get("/orders", requirePermissions(["purchasing.read"]), async (request, response) => { const parsed = purchaseListQuerySchema.safeParse(request.query); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Purchase order filters are invalid."); } return ok(response, await listPurchaseOrders(parsed.data)); }); purchasingRouter.get("/orders/:orderId", requirePermissions(["purchasing.read"]), async (request, response) => { const orderId = getRouteParam(request.params.orderId); if (!orderId) { return fail(response, 400, "INVALID_INPUT", "Purchase order id is invalid."); } const order = await getPurchaseOrderById(orderId); if (!order) { return fail(response, 404, "PURCHASE_ORDER_NOT_FOUND", "Purchase order was not found."); } return ok(response, order); }); purchasingRouter.post("/orders", requirePermissions(["purchasing.write"]), async (request, response) => { const parsed = purchaseOrderSchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Purchase order payload is invalid."); } const result = await createPurchaseOrder(parsed.data); if (!result.ok) { return fail(response, 400, "INVALID_INPUT", result.reason); } return ok(response, result.document, 201); }); purchasingRouter.put("/orders/:orderId", requirePermissions(["purchasing.write"]), async (request, response) => { const orderId = getRouteParam(request.params.orderId); if (!orderId) { return fail(response, 400, "INVALID_INPUT", "Purchase order id is invalid."); } const parsed = purchaseOrderSchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Purchase order payload is invalid."); } const result = await updatePurchaseOrder(orderId, parsed.data); if (!result.ok) { return fail(response, 400, "INVALID_INPUT", result.reason); } return ok(response, result.document); }); purchasingRouter.patch("/orders/:orderId/status", requirePermissions(["purchasing.write"]), async (request, response) => { const orderId = getRouteParam(request.params.orderId); if (!orderId) { return fail(response, 400, "INVALID_INPUT", "Purchase order id is invalid."); } const parsed = purchaseStatusUpdateSchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Purchase order status payload is invalid."); } const result = await updatePurchaseOrderStatus(orderId, parsed.data.status); if (!result.ok) { return fail(response, 400, "INVALID_INPUT", result.reason); } return ok(response, result.document); }); purchasingRouter.post( "/orders/:orderId/receipts", requirePermissions([permissions.purchasingWrite, permissions.inventoryWrite]), async (request, response) => { const orderId = getRouteParam(request.params.orderId); if (!orderId) { return fail(response, 400, "INVALID_INPUT", "Purchase order id is invalid."); } const parsed = purchaseReceiptSchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Purchase receipt payload is invalid."); } const result = await createPurchaseReceipt(orderId, parsed.data, request.authUser?.id); if (!result.ok) { return fail(response, 400, "INVALID_INPUT", result.reason); } return ok(response, result.document, 201); } );