Files
mrp/server/src/modules/shipping/service.ts
2026-03-14 23:50:41 -05:00

310 lines
8.2 KiB
TypeScript

import type {
ShipmentDetailDto,
ShipmentInput,
ShipmentOrderOptionDto,
ShipmentStatus,
ShipmentSummaryDto,
} from "@mrp/shared/dist/shipping/types.js";
import { prisma } from "../../lib/prisma.js";
export interface ShipmentPackingSlipData {
shipmentNumber: string;
status: ShipmentStatus;
shipDate: string | null;
carrier: string;
serviceLevel: string;
trackingNumber: string;
packageCount: number;
notes: string;
salesOrderNumber: string;
customer: {
name: string;
addressLine1: string;
addressLine2: string;
city: string;
state: string;
postalCode: string;
country: string;
};
lines: Array<{
itemSku: string;
itemName: string;
description: string;
quantity: number;
unitOfMeasure: string;
}>;
}
type ShipmentRecord = {
id: string;
shipmentNumber: string;
status: string;
shipDate: Date | null;
carrier: string;
serviceLevel: string;
trackingNumber: string;
packageCount: number;
notes: string;
createdAt: Date;
updatedAt: Date;
salesOrder: {
id: string;
documentNumber: string;
customer: {
name: string;
};
};
};
function mapShipment(record: ShipmentRecord): ShipmentDetailDto {
return {
id: record.id,
shipmentNumber: record.shipmentNumber,
salesOrderId: record.salesOrder.id,
salesOrderNumber: record.salesOrder.documentNumber,
customerName: record.salesOrder.customer.name,
status: record.status as ShipmentStatus,
carrier: record.carrier,
serviceLevel: record.serviceLevel,
trackingNumber: record.trackingNumber,
packageCount: record.packageCount,
shipDate: record.shipDate ? record.shipDate.toISOString() : null,
notes: record.notes,
createdAt: record.createdAt.toISOString(),
updatedAt: record.updatedAt.toISOString(),
};
}
async function nextShipmentNumber() {
const next = (await prisma.shipment.count()) + 1;
return `SHP-${String(next).padStart(5, "0")}`;
}
export async function listShipmentOrderOptions(): Promise<ShipmentOrderOptionDto[]> {
const orders = await prisma.salesOrder.findMany({
include: {
customer: {
select: {
name: true,
},
},
lines: {
select: {
quantity: true,
unitPrice: true,
},
},
},
orderBy: [{ issueDate: "desc" }, { createdAt: "desc" }],
});
return orders.map((order) => ({
id: order.id,
documentNumber: order.documentNumber,
customerName: order.customer.name,
status: order.status,
total: order.lines.reduce((sum, line) => sum + line.quantity * line.unitPrice, 0),
}));
}
export async function listShipments(filters: { q?: string; status?: ShipmentStatus; salesOrderId?: string } = {}) {
const query = filters.q?.trim();
const shipments = await prisma.shipment.findMany({
where: {
...(filters.status ? { status: filters.status } : {}),
...(filters.salesOrderId ? { salesOrderId: filters.salesOrderId } : {}),
...(query
? {
OR: [
{ shipmentNumber: { contains: query } },
{ trackingNumber: { contains: query } },
{ carrier: { contains: query } },
{ salesOrder: { documentNumber: { contains: query } } },
{ salesOrder: { customer: { name: { contains: query } } } },
],
}
: {}),
},
include: {
salesOrder: {
include: {
customer: {
select: {
name: true,
},
},
},
},
},
orderBy: [{ createdAt: "desc" }],
});
return shipments.map((shipment) => mapShipment(shipment));
}
export async function getShipmentById(shipmentId: string) {
const shipment = await prisma.shipment.findUnique({
where: { id: shipmentId },
include: {
salesOrder: {
include: {
customer: {
select: {
name: true,
},
},
},
},
},
});
return shipment ? mapShipment(shipment) : null;
}
export async function createShipment(payload: ShipmentInput) {
const order = await prisma.salesOrder.findUnique({
where: { id: payload.salesOrderId },
select: { id: true },
});
if (!order) {
return { ok: false as const, reason: "Sales order was not found." };
}
const shipmentNumber = await nextShipmentNumber();
const shipment = await prisma.shipment.create({
data: {
shipmentNumber,
salesOrderId: payload.salesOrderId,
status: payload.status,
shipDate: payload.shipDate ? new Date(payload.shipDate) : null,
carrier: payload.carrier.trim(),
serviceLevel: payload.serviceLevel.trim(),
trackingNumber: payload.trackingNumber.trim(),
packageCount: payload.packageCount,
notes: payload.notes,
},
select: { id: true },
});
const detail = await getShipmentById(shipment.id);
return detail ? { ok: true as const, shipment: detail } : { ok: false as const, reason: "Unable to load saved shipment." };
}
export async function updateShipment(shipmentId: string, payload: ShipmentInput) {
const existing = await prisma.shipment.findUnique({
where: { id: shipmentId },
select: { id: true },
});
if (!existing) {
return { ok: false as const, reason: "Shipment was not found." };
}
const order = await prisma.salesOrder.findUnique({
where: { id: payload.salesOrderId },
select: { id: true },
});
if (!order) {
return { ok: false as const, reason: "Sales order was not found." };
}
await prisma.shipment.update({
where: { id: shipmentId },
data: {
salesOrderId: payload.salesOrderId,
status: payload.status,
shipDate: payload.shipDate ? new Date(payload.shipDate) : null,
carrier: payload.carrier.trim(),
serviceLevel: payload.serviceLevel.trim(),
trackingNumber: payload.trackingNumber.trim(),
packageCount: payload.packageCount,
notes: payload.notes,
},
select: { id: true },
});
const detail = await getShipmentById(shipmentId);
return detail ? { ok: true as const, shipment: detail } : { ok: false as const, reason: "Unable to load saved shipment." };
}
export async function updateShipmentStatus(shipmentId: string, status: ShipmentStatus) {
const existing = await prisma.shipment.findUnique({
where: { id: shipmentId },
select: { id: true },
});
if (!existing) {
return { ok: false as const, reason: "Shipment was not found." };
}
await prisma.shipment.update({
where: { id: shipmentId },
data: { status },
select: { id: true },
});
const detail = await getShipmentById(shipmentId);
return detail ? { ok: true as const, shipment: detail } : { ok: false as const, reason: "Unable to load updated shipment." };
}
export async function getShipmentPackingSlipData(shipmentId: string): Promise<ShipmentPackingSlipData | null> {
const shipment = await prisma.shipment.findUnique({
where: { id: shipmentId },
include: {
salesOrder: {
include: {
customer: {
select: {
name: true,
addressLine1: true,
addressLine2: true,
city: true,
state: true,
postalCode: true,
country: true,
},
},
lines: {
include: {
item: {
select: {
sku: true,
name: true,
},
},
},
orderBy: [{ position: "asc" }, { createdAt: "asc" }],
},
},
},
},
});
if (!shipment) {
return null;
}
return {
shipmentNumber: shipment.shipmentNumber,
status: shipment.status as ShipmentStatus,
shipDate: shipment.shipDate ? shipment.shipDate.toISOString() : null,
carrier: shipment.carrier,
serviceLevel: shipment.serviceLevel,
trackingNumber: shipment.trackingNumber,
packageCount: shipment.packageCount,
notes: shipment.notes,
salesOrderNumber: shipment.salesOrder.documentNumber,
customer: shipment.salesOrder.customer,
lines: shipment.salesOrder.lines.map((line) => ({
itemSku: line.item.sku,
itemName: line.item.name,
description: line.description,
quantity: line.quantity,
unitOfMeasure: line.unitOfMeasure,
})),
};
}