import type { PurchaseLineInput, PurchaseOrderDetailDto, PurchaseOrderInput, PurchaseOrderStatus, PurchaseOrderSummaryDto, PurchaseVendorOptionDto, } from "@mrp/shared"; import { prisma } from "../../lib/prisma.js"; const purchaseOrderModel = (prisma as any).purchaseOrder; type PurchaseLineRecord = { id: string; description: string; quantity: number; unitOfMeasure: string; unitCost: number; position: number; item: { id: string; sku: string; name: string; }; }; type PurchaseOrderRecord = { id: string; documentNumber: string; status: string; issueDate: Date; taxPercent: number; freightAmount: number; notes: string; createdAt: Date; updatedAt: Date; vendor: { id: string; name: string; email: string; paymentTerms: string | null; currencyCode: string | null; }; lines: PurchaseLineRecord[]; }; function roundMoney(value: number) { return Math.round(value * 100) / 100; } function calculateTotals(subtotal: number, taxPercent: number, freightAmount: number) { const normalizedSubtotal = roundMoney(subtotal); const normalizedTaxPercent = Number.isFinite(taxPercent) ? taxPercent : 0; const normalizedFreight = roundMoney(Number.isFinite(freightAmount) ? freightAmount : 0); const taxAmount = roundMoney(normalizedSubtotal * (normalizedTaxPercent / 100)); const total = roundMoney(normalizedSubtotal + taxAmount + normalizedFreight); return { subtotal: normalizedSubtotal, taxPercent: normalizedTaxPercent, taxAmount, freightAmount: normalizedFreight, total, }; } function normalizeLines(lines: PurchaseLineInput[]) { return lines .map((line, index) => ({ itemId: line.itemId, description: line.description.trim(), quantity: Number(line.quantity), unitOfMeasure: line.unitOfMeasure, unitCost: Number(line.unitCost), position: line.position ?? (index + 1) * 10, })) .filter((line) => line.itemId.trim().length > 0); } async function validateLines(lines: PurchaseLineInput[]) { const normalized = normalizeLines(lines); if (normalized.length === 0) { return { ok: false as const, reason: "At least one line item is required." }; } if (normalized.some((line) => !Number.isInteger(line.quantity) || line.quantity <= 0)) { return { ok: false as const, reason: "Line quantity must be a whole number greater than zero." }; } if (normalized.some((line) => Number.isNaN(line.unitCost) || line.unitCost < 0)) { return { ok: false as const, reason: "Unit cost must be zero or greater." }; } const itemIds = [...new Set(normalized.map((line) => line.itemId))]; const items = await prisma.inventoryItem.findMany({ where: { id: { in: itemIds } }, select: { id: true, isPurchasable: true }, }); if (items.length !== itemIds.length) { return { ok: false as const, reason: "One or more purchase lines reference an invalid inventory item." }; } if (items.some((item) => !item.isPurchasable)) { return { ok: false as const, reason: "Purchase orders can only include purchasable inventory items." }; } return { ok: true as const, lines: normalized }; } function mapPurchaseOrder(record: PurchaseOrderRecord): PurchaseOrderDetailDto { const lines = record.lines .slice() .sort((left, right) => left.position - right.position) .map((line) => ({ id: line.id, itemId: line.item.id, itemSku: line.item.sku, itemName: line.item.name, description: line.description, quantity: line.quantity, unitOfMeasure: line.unitOfMeasure as PurchaseOrderDetailDto["lines"][number]["unitOfMeasure"], unitCost: line.unitCost, lineTotal: line.quantity * line.unitCost, position: line.position, })); const totals = calculateTotals( lines.reduce((sum, line) => sum + line.lineTotal, 0), record.taxPercent, record.freightAmount ); return { id: record.id, documentNumber: record.documentNumber, vendorId: record.vendor.id, vendorName: record.vendor.name, vendorEmail: record.vendor.email, paymentTerms: record.vendor.paymentTerms, currencyCode: record.vendor.currencyCode, status: record.status as PurchaseOrderStatus, subtotal: totals.subtotal, taxPercent: totals.taxPercent, taxAmount: totals.taxAmount, freightAmount: totals.freightAmount, total: totals.total, issueDate: record.issueDate.toISOString(), notes: record.notes, createdAt: record.createdAt.toISOString(), updatedAt: record.updatedAt.toISOString(), lineCount: lines.length, lines, }; } async function nextDocumentNumber() { const next = (await purchaseOrderModel.count()) + 1; return `PO-${String(next).padStart(5, "0")}`; } function buildInclude() { return { vendor: { select: { id: true, name: true, email: true, paymentTerms: true, currencyCode: true, }, }, lines: { include: { item: { select: { id: true, sku: true, name: true, }, }, }, orderBy: [{ position: "asc" }, { createdAt: "asc" }], }, }; } export async function listPurchaseVendorOptions(): Promise { const vendors = await prisma.vendor.findMany({ where: { status: { not: "INACTIVE", }, }, select: { id: true, name: true, email: true, paymentTerms: true, currencyCode: true, }, orderBy: [{ name: "asc" }], }); return vendors; } export async function listPurchaseOrders(filters: { q?: string; status?: PurchaseOrderStatus } = {}) { const query = filters.q?.trim(); const records = await purchaseOrderModel.findMany({ where: { ...(filters.status ? { status: filters.status } : {}), ...(query ? { OR: [ { documentNumber: { contains: query } }, { vendor: { name: { contains: query } } }, ], } : {}), }, include: buildInclude(), orderBy: [{ issueDate: "desc" }, { createdAt: "desc" }], }); return records.map((record: unknown) => { const detail = mapPurchaseOrder(record as PurchaseOrderRecord); const summary: PurchaseOrderSummaryDto = { id: detail.id, documentNumber: detail.documentNumber, vendorId: detail.vendorId, vendorName: detail.vendorName, status: detail.status, subtotal: detail.subtotal, taxPercent: detail.taxPercent, taxAmount: detail.taxAmount, freightAmount: detail.freightAmount, total: detail.total, issueDate: detail.issueDate, updatedAt: detail.updatedAt, lineCount: detail.lineCount, }; return summary; }); } export async function getPurchaseOrderById(documentId: string) { const record = await purchaseOrderModel.findUnique({ where: { id: documentId }, include: buildInclude(), }); return record ? mapPurchaseOrder(record as PurchaseOrderRecord) : null; } export async function createPurchaseOrder(payload: PurchaseOrderInput) { const validatedLines = await validateLines(payload.lines); if (!validatedLines.ok) { return { ok: false as const, reason: validatedLines.reason }; } const vendor = await prisma.vendor.findUnique({ where: { id: payload.vendorId }, select: { id: true }, }); if (!vendor) { return { ok: false as const, reason: "Vendor was not found." }; } const documentNumber = await nextDocumentNumber(); const created = await purchaseOrderModel.create({ data: { documentNumber, vendorId: payload.vendorId, status: payload.status, issueDate: new Date(payload.issueDate), taxPercent: payload.taxPercent, freightAmount: payload.freightAmount, notes: payload.notes, lines: { create: validatedLines.lines, }, }, select: { id: true }, }); const detail = await getPurchaseOrderById(created.id); return detail ? { ok: true as const, document: detail } : { ok: false as const, reason: "Unable to load saved purchase order." }; } export async function updatePurchaseOrder(documentId: string, payload: PurchaseOrderInput) { const existing = await purchaseOrderModel.findUnique({ where: { id: documentId }, select: { id: true }, }); if (!existing) { return { ok: false as const, reason: "Purchase order was not found." }; } const validatedLines = await validateLines(payload.lines); if (!validatedLines.ok) { return { ok: false as const, reason: validatedLines.reason }; } const vendor = await prisma.vendor.findUnique({ where: { id: payload.vendorId }, select: { id: true }, }); if (!vendor) { return { ok: false as const, reason: "Vendor was not found." }; } await purchaseOrderModel.update({ where: { id: documentId }, data: { vendorId: payload.vendorId, status: payload.status, issueDate: new Date(payload.issueDate), taxPercent: payload.taxPercent, freightAmount: payload.freightAmount, notes: payload.notes, lines: { deleteMany: {}, create: validatedLines.lines, }, }, select: { id: true }, }); const detail = await getPurchaseOrderById(documentId); return detail ? { ok: true as const, document: detail } : { ok: false as const, reason: "Unable to load saved purchase order." }; } export async function updatePurchaseOrderStatus(documentId: string, status: PurchaseOrderStatus) { const existing = await purchaseOrderModel.findUnique({ where: { id: documentId }, select: { id: true }, }); if (!existing) { return { ok: false as const, reason: "Purchase order was not found." }; } await purchaseOrderModel.update({ where: { id: documentId }, data: { status }, select: { id: true }, }); const detail = await getPurchaseOrderById(documentId); return detail ? { ok: true as const, document: detail } : { ok: false as const, reason: "Unable to load updated purchase order." }; }