import type { SalesCustomerOptionDto, SalesDocumentDetailDto, SalesDocumentInput, SalesDocumentStatus, SalesDocumentSummaryDto, SalesDocumentType, SalesLineInput, } from "@mrp/shared/dist/sales/types.js"; import { prisma } from "../../lib/prisma.js"; type SalesLineRecord = { id: string; description: string; quantity: number; unitOfMeasure: string; unitPrice: number; position: number; item: { id: string; sku: string; name: string; }; }; type SalesDocumentRecord = { id: string; documentNumber: string; status: string; issueDate: Date; expiresAt?: Date | null; discountPercent: number; taxPercent: number; freightAmount: number; notes: string; createdAt: Date; updatedAt: Date; customer: { id: string; name: string; email: string; }; lines: SalesLineRecord[]; }; const documentConfig = { QUOTE: { prefix: "Q", findMany: (prisma as any).salesQuote.findMany.bind((prisma as any).salesQuote), findUnique: (prisma as any).salesQuote.findUnique.bind((prisma as any).salesQuote), create: (prisma as any).salesQuote.create.bind((prisma as any).salesQuote), update: (prisma as any).salesQuote.update.bind((prisma as any).salesQuote), count: (prisma as any).salesQuote.count.bind((prisma as any).salesQuote), }, ORDER: { prefix: "SO", findMany: (prisma as any).salesOrder.findMany.bind((prisma as any).salesOrder), findUnique: (prisma as any).salesOrder.findUnique.bind((prisma as any).salesOrder), create: (prisma as any).salesOrder.create.bind((prisma as any).salesOrder), update: (prisma as any).salesOrder.update.bind((prisma as any).salesOrder), count: (prisma as any).salesOrder.count.bind((prisma as any).salesOrder), }, } as const; function roundMoney(value: number) { return Math.round(value * 100) / 100; } function calculateTotals(subtotal: number, discountPercent: number, taxPercent: number, freightAmount: number) { const normalizedSubtotal = roundMoney(subtotal); const normalizedDiscountPercent = Number.isFinite(discountPercent) ? discountPercent : 0; const normalizedTaxPercent = Number.isFinite(taxPercent) ? taxPercent : 0; const normalizedFreight = roundMoney(Number.isFinite(freightAmount) ? freightAmount : 0); const discountAmount = roundMoney(normalizedSubtotal * (normalizedDiscountPercent / 100)); const taxableSubtotal = roundMoney(normalizedSubtotal - discountAmount); const taxAmount = roundMoney(taxableSubtotal * (normalizedTaxPercent / 100)); const total = roundMoney(taxableSubtotal + taxAmount + normalizedFreight); return { subtotal: normalizedSubtotal, discountPercent: normalizedDiscountPercent, discountAmount, taxPercent: normalizedTaxPercent, taxAmount, freightAmount: normalizedFreight, total, }; } function normalizeLines(lines: SalesLineInput[]) { return lines .map((line, index) => ({ itemId: line.itemId, description: line.description.trim(), quantity: Number(line.quantity), unitOfMeasure: line.unitOfMeasure, unitPrice: Number(line.unitPrice), position: line.position ?? (index + 1) * 10, })) .filter((line) => line.itemId.trim().length > 0); } async function validateLines(lines: SalesLineInput[]) { 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.unitPrice) || line.unitPrice < 0)) { return { ok: false as const, reason: "Unit price 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 }, }); if (items.length !== itemIds.length) { return { ok: false as const, reason: "One or more sales lines reference an invalid inventory item." }; } return { ok: true as const, lines: normalized }; } function mapDocument(record: SalesDocumentRecord): SalesDocumentDetailDto { 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 SalesDocumentDetailDto["lines"][number]["unitOfMeasure"], unitPrice: line.unitPrice, lineTotal: line.quantity * line.unitPrice, position: line.position, })); const totals = calculateTotals( lines.reduce((sum, line) => sum + line.lineTotal, 0), record.discountPercent, record.taxPercent, record.freightAmount ); return { id: record.id, documentNumber: record.documentNumber, customerId: record.customer.id, customerName: record.customer.name, customerEmail: record.customer.email, status: record.status as SalesDocumentStatus, subtotal: totals.subtotal, discountPercent: totals.discountPercent, discountAmount: totals.discountAmount, taxPercent: totals.taxPercent, taxAmount: totals.taxAmount, freightAmount: totals.freightAmount, total: totals.total, issueDate: record.issueDate.toISOString(), expiresAt: "expiresAt" in record && record.expiresAt ? record.expiresAt.toISOString() : null, notes: record.notes, createdAt: record.createdAt.toISOString(), updatedAt: record.updatedAt.toISOString(), lineCount: lines.length, lines, }; } async function nextDocumentNumber(type: SalesDocumentType) { const next = (await documentConfig[type].count()) + 1; return `${documentConfig[type].prefix}-${String(next).padStart(5, "0")}`; } function buildInclude(type: SalesDocumentType) { const base = { customer: { select: { id: true, name: true, email: true, }, }, }; if (type === "QUOTE") { return { ...base, lines: { include: { item: { select: { id: true, sku: true, name: true, }, }, }, orderBy: [{ position: "asc" }, { createdAt: "asc" }], }, }; } return { ...base, lines: { include: { item: { select: { id: true, sku: true, name: true, }, }, }, orderBy: [{ position: "asc" }, { createdAt: "asc" }], }, }; } export async function listSalesCustomerOptions(): Promise { const customers = await prisma.customer.findMany({ where: { status: { not: "INACTIVE", }, }, select: { id: true, name: true, email: true, resellerDiscountPercent: true, }, orderBy: [{ name: "asc" }], }); return customers; } export async function listSalesOrderOptions() { 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: calculateTotals( order.lines.reduce((sum, line) => sum + line.quantity * line.unitPrice, 0), order.discountPercent, order.taxPercent, order.freightAmount ).total, })); } export async function listSalesDocuments(type: SalesDocumentType, filters: { q?: string; status?: SalesDocumentStatus } = {}) { const query = filters.q?.trim(); const records = await documentConfig[type].findMany({ where: { ...(filters.status ? { status: filters.status } : {}), ...(query ? { OR: [ { documentNumber: { contains: query } }, { customer: { name: { contains: query } } }, ], } : {}), }, include: buildInclude(type), orderBy: [{ issueDate: "desc" }, { createdAt: "desc" }], }); return records.map((record: unknown) => { const detail = mapDocument(record as SalesDocumentRecord); const summary: SalesDocumentSummaryDto = { id: detail.id, documentNumber: detail.documentNumber, customerId: detail.customerId, customerName: detail.customerName, status: detail.status, subtotal: detail.subtotal, discountPercent: detail.discountPercent, discountAmount: detail.discountAmount, 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 getSalesDocumentById(type: SalesDocumentType, documentId: string) { const record = await documentConfig[type].findUnique({ where: { id: documentId }, include: buildInclude(type), }); return record ? mapDocument(record as SalesDocumentRecord) : null; } export async function createSalesDocument(type: SalesDocumentType, payload: SalesDocumentInput) { const validatedLines = await validateLines(payload.lines); if (!validatedLines.ok) { return { ok: false as const, reason: validatedLines.reason }; } const customer = await prisma.customer.findUnique({ where: { id: payload.customerId }, select: { id: true }, }); if (!customer) { return { ok: false as const, reason: "Customer was not found." }; } const documentNumber = await nextDocumentNumber(type); const created = await documentConfig[type].create({ data: { documentNumber, customerId: payload.customerId, status: payload.status, issueDate: new Date(payload.issueDate), ...(type === "QUOTE" ? { expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null } : {}), discountPercent: payload.discountPercent, taxPercent: payload.taxPercent, freightAmount: payload.freightAmount, notes: payload.notes, lines: { create: validatedLines.lines, }, }, select: { id: true }, }); const detail = await getSalesDocumentById(type, created.id); return detail ? { ok: true as const, document: detail } : { ok: false as const, reason: "Unable to load saved document." }; } export async function updateSalesDocument(type: SalesDocumentType, documentId: string, payload: SalesDocumentInput) { const existing = await documentConfig[type].findUnique({ where: { id: documentId }, select: { id: true }, }); if (!existing) { return { ok: false as const, reason: "Sales document was not found." }; } const validatedLines = await validateLines(payload.lines); if (!validatedLines.ok) { return { ok: false as const, reason: validatedLines.reason }; } const customer = await prisma.customer.findUnique({ where: { id: payload.customerId }, select: { id: true }, }); if (!customer) { return { ok: false as const, reason: "Customer was not found." }; } await documentConfig[type].update({ where: { id: documentId }, data: { customerId: payload.customerId, status: payload.status, issueDate: new Date(payload.issueDate), ...(type === "QUOTE" ? { expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null } : {}), discountPercent: payload.discountPercent, taxPercent: payload.taxPercent, freightAmount: payload.freightAmount, notes: payload.notes, lines: { deleteMany: {}, create: validatedLines.lines, }, }, select: { id: true }, }); const detail = await getSalesDocumentById(type, documentId); return detail ? { ok: true as const, document: detail } : { ok: false as const, reason: "Unable to load saved document." }; } export async function updateSalesDocumentStatus(type: SalesDocumentType, documentId: string, status: SalesDocumentStatus) { const existing = await documentConfig[type].findUnique({ where: { id: documentId }, select: { id: true }, }); if (!existing) { return { ok: false as const, reason: "Sales document was not found." }; } await documentConfig[type].update({ where: { id: documentId }, data: { status }, select: { id: true }, }); const detail = await getSalesDocumentById(type, documentId); return detail ? { ok: true as const, document: detail } : { ok: false as const, reason: "Unable to load updated document." }; } export async function convertQuoteToSalesOrder(quoteId: string) { const quote = await documentConfig.QUOTE.findUnique({ where: { id: quoteId }, include: buildInclude("QUOTE"), }); if (!quote) { return { ok: false as const, reason: "Quote was not found." }; } const mappedQuote = mapDocument(quote as SalesDocumentRecord); const nextOrderNumber = await nextDocumentNumber("ORDER"); const created = await documentConfig.ORDER.create({ data: { documentNumber: nextOrderNumber, customerId: mappedQuote.customerId, status: "DRAFT", issueDate: new Date(), discountPercent: mappedQuote.discountPercent, taxPercent: mappedQuote.taxPercent, freightAmount: mappedQuote.freightAmount, notes: mappedQuote.notes ? `Converted from ${mappedQuote.documentNumber}\n\n${mappedQuote.notes}` : `Converted from ${mappedQuote.documentNumber}`, lines: { create: mappedQuote.lines.map((line) => ({ itemId: line.itemId, description: line.description, quantity: line.quantity, unitOfMeasure: line.unitOfMeasure, unitPrice: line.unitPrice, position: line.position, })), }, }, select: { id: true }, }); const order = await getSalesDocumentById("ORDER", created.id); return order ? { ok: true as const, document: order } : { ok: false as const, reason: "Unable to load converted sales order." }; }