2026-03-14 23:48:27 -05:00
|
|
|
import { permissions } from "@mrp/shared";
|
|
|
|
|
import { shipmentStatuses } from "@mrp/shared/dist/shipping/types.js";
|
|
|
|
|
import { Router } from "express";
|
|
|
|
|
import { z } from "zod";
|
|
|
|
|
|
|
|
|
|
import { fail, ok } from "../../lib/http.js";
|
|
|
|
|
import { requirePermissions } from "../../lib/rbac.js";
|
2026-03-18 07:27:33 -05:00
|
|
|
import { createShipment, getShipmentById, listShipmentOrderOptions, listShipments, postShipmentPick, updateShipment, updateShipmentStatus } from "./service.js";
|
2026-03-14 23:48:27 -05:00
|
|
|
|
|
|
|
|
const shipmentSchema = z.object({
|
|
|
|
|
salesOrderId: z.string().trim().min(1),
|
|
|
|
|
status: z.enum(shipmentStatuses),
|
|
|
|
|
shipDate: z.string().datetime().nullable(),
|
|
|
|
|
carrier: z.string(),
|
|
|
|
|
serviceLevel: z.string(),
|
|
|
|
|
trackingNumber: z.string(),
|
|
|
|
|
packageCount: z.number().int().positive(),
|
|
|
|
|
notes: z.string(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const shipmentListQuerySchema = z.object({
|
|
|
|
|
q: z.string().optional(),
|
|
|
|
|
status: z.enum(shipmentStatuses).optional(),
|
|
|
|
|
salesOrderId: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const shipmentStatusUpdateSchema = z.object({
|
|
|
|
|
status: z.enum(shipmentStatuses),
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-18 07:27:33 -05:00
|
|
|
const shipmentPickSchema = z.object({
|
|
|
|
|
salesOrderLineId: z.string().trim().min(1),
|
|
|
|
|
warehouseId: z.string().trim().min(1),
|
|
|
|
|
locationId: z.string().trim().min(1),
|
|
|
|
|
quantity: z.number().positive(),
|
|
|
|
|
notes: z.string(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-14 23:48:27 -05:00
|
|
|
function getRouteParam(value: unknown) {
|
|
|
|
|
return typeof value === "string" ? value : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const shippingRouter = Router();
|
|
|
|
|
|
|
|
|
|
shippingRouter.get("/orders/options", requirePermissions([permissions.shippingRead]), async (_request, response) => {
|
|
|
|
|
return ok(response, await listShipmentOrderOptions());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
shippingRouter.get("/shipments", requirePermissions([permissions.shippingRead]), async (request, response) => {
|
|
|
|
|
const parsed = shipmentListQuerySchema.safeParse(request.query);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Shipment filters are invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ok(response, await listShipments(parsed.data));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
shippingRouter.get("/shipments/:shipmentId", requirePermissions([permissions.shippingRead]), async (request, response) => {
|
|
|
|
|
const shipmentId = getRouteParam(request.params.shipmentId);
|
|
|
|
|
if (!shipmentId) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Shipment id is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const shipment = await getShipmentById(shipmentId);
|
|
|
|
|
if (!shipment) {
|
|
|
|
|
return fail(response, 404, "SHIPMENT_NOT_FOUND", "Shipment was not found.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ok(response, shipment);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
shippingRouter.post("/shipments", requirePermissions([permissions.shippingWrite]), async (request, response) => {
|
|
|
|
|
const parsed = shipmentSchema.safeParse(request.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Shipment payload is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 14:57:41 -05:00
|
|
|
const result = await createShipment(parsed.data, request.authUser?.id);
|
2026-03-14 23:48:27 -05:00
|
|
|
if (!result.ok) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", result.reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ok(response, result.shipment, 201);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
shippingRouter.put("/shipments/:shipmentId", requirePermissions([permissions.shippingWrite]), async (request, response) => {
|
|
|
|
|
const shipmentId = getRouteParam(request.params.shipmentId);
|
|
|
|
|
if (!shipmentId) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Shipment id is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsed = shipmentSchema.safeParse(request.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Shipment payload is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 14:57:41 -05:00
|
|
|
const result = await updateShipment(shipmentId, parsed.data, request.authUser?.id);
|
2026-03-14 23:48:27 -05:00
|
|
|
if (!result.ok) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", result.reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ok(response, result.shipment);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
shippingRouter.patch("/shipments/:shipmentId/status", requirePermissions([permissions.shippingWrite]), async (request, response) => {
|
|
|
|
|
const shipmentId = getRouteParam(request.params.shipmentId);
|
|
|
|
|
if (!shipmentId) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Shipment id is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsed = shipmentStatusUpdateSchema.safeParse(request.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Shipment status payload is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 14:57:41 -05:00
|
|
|
const result = await updateShipmentStatus(shipmentId, parsed.data.status, request.authUser?.id);
|
2026-03-14 23:48:27 -05:00
|
|
|
if (!result.ok) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", result.reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ok(response, result.shipment);
|
|
|
|
|
});
|
2026-03-18 07:27:33 -05:00
|
|
|
|
|
|
|
|
shippingRouter.post("/shipments/:shipmentId/picks", requirePermissions([permissions.shippingWrite]), async (request, response) => {
|
|
|
|
|
const shipmentId = getRouteParam(request.params.shipmentId);
|
|
|
|
|
if (!shipmentId) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Shipment id is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsed = shipmentPickSchema.safeParse(request.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Shipment pick payload is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await postShipmentPick(shipmentId, parsed.data, request.authUser?.id);
|
|
|
|
|
if (!result.ok) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", result.reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ok(response, result.shipment, 201);
|
|
|
|
|
});
|