166 lines
5.3 KiB
TypeScript
166 lines
5.3 KiB
TypeScript
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);
|
|
}
|
|
);
|