sales documents

This commit is contained in:
2026-03-15 11:44:14 -05:00
parent e2254d020e
commit a9d31730f8
15 changed files with 628 additions and 115 deletions

View File

@@ -7,9 +7,11 @@ import { fail, ok } from "../../lib/http.js";
import { requirePermissions } from "../../lib/rbac.js";
import { inventoryUnitsOfMeasure } from "@mrp/shared/dist/inventory/types.js";
import {
approveSalesDocument,
convertQuoteToSalesOrder,
createSalesDocument,
getSalesDocumentById,
listSalesDocumentRevisions,
listSalesCustomerOptions,
listSalesDocuments,
listSalesOrderOptions,
@@ -36,6 +38,7 @@ const quoteSchema = z.object({
freightAmount: z.number().nonnegative(),
notes: z.string(),
lines: z.array(salesLineSchema),
revisionReason: z.string().optional(),
});
const orderSchema = z.object({
@@ -47,6 +50,7 @@ const orderSchema = z.object({
freightAmount: z.number().nonnegative(),
notes: z.string(),
lines: z.array(salesLineSchema),
revisionReason: z.string().optional(),
});
const salesListQuerySchema = z.object({
@@ -101,7 +105,7 @@ salesRouter.post("/quotes", requirePermissions([permissions.salesWrite]), async
return fail(response, 400, "INVALID_INPUT", "Quote payload is invalid.");
}
const result = await createSalesDocument("QUOTE", parsed.data);
const result = await createSalesDocument("QUOTE", parsed.data, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
@@ -120,7 +124,7 @@ salesRouter.put("/quotes/:quoteId", requirePermissions([permissions.salesWrite])
return fail(response, 400, "INVALID_INPUT", "Quote payload is invalid.");
}
const result = await updateSalesDocument("QUOTE", quoteId, parsed.data);
const result = await updateSalesDocument("QUOTE", quoteId, parsed.data, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
@@ -139,7 +143,7 @@ salesRouter.patch("/quotes/:quoteId/status", requirePermissions([permissions.sal
return fail(response, 400, "INVALID_INPUT", "Quote status payload is invalid.");
}
const result = await updateSalesDocumentStatus("QUOTE", quoteId, parsed.data.status);
const result = await updateSalesDocumentStatus("QUOTE", quoteId, parsed.data.status, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
@@ -147,13 +151,41 @@ salesRouter.patch("/quotes/:quoteId/status", requirePermissions([permissions.sal
return ok(response, result.document);
});
salesRouter.post("/quotes/:quoteId/approve", requirePermissions([permissions.salesWrite]), async (request, response) => {
const quoteId = getRouteParam(request.params.quoteId);
if (!quoteId) {
return fail(response, 400, "INVALID_INPUT", "Quote id is invalid.");
}
const result = await approveSalesDocument("QUOTE", quoteId, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
return ok(response, result.document);
});
salesRouter.get("/quotes/:quoteId/revisions", requirePermissions([permissions.salesRead]), async (request, response) => {
const quoteId = getRouteParam(request.params.quoteId);
if (!quoteId) {
return fail(response, 400, "INVALID_INPUT", "Quote id is invalid.");
}
const quote = await getSalesDocumentById("QUOTE", quoteId);
if (!quote) {
return fail(response, 404, "QUOTE_NOT_FOUND", "Quote was not found.");
}
return ok(response, await listSalesDocumentRevisions("QUOTE", quoteId));
});
salesRouter.post("/quotes/:quoteId/convert", requirePermissions([permissions.salesWrite]), async (request, response) => {
const quoteId = getRouteParam(request.params.quoteId);
if (!quoteId) {
return fail(response, 400, "INVALID_INPUT", "Quote id is invalid.");
}
const result = await convertQuoteToSalesOrder(quoteId);
const result = await convertQuoteToSalesOrder(quoteId, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
@@ -193,7 +225,7 @@ salesRouter.post("/orders", requirePermissions([permissions.salesWrite]), async
const result = await createSalesDocument("ORDER", {
...parsed.data,
expiresAt: null,
});
}, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
@@ -215,7 +247,7 @@ salesRouter.put("/orders/:orderId", requirePermissions([permissions.salesWrite])
const result = await updateSalesDocument("ORDER", orderId, {
...parsed.data,
expiresAt: null,
});
}, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
@@ -234,10 +266,38 @@ salesRouter.patch("/orders/:orderId/status", requirePermissions([permissions.sal
return fail(response, 400, "INVALID_INPUT", "Sales order status payload is invalid.");
}
const result = await updateSalesDocumentStatus("ORDER", orderId, parsed.data.status);
const result = await updateSalesDocumentStatus("ORDER", orderId, parsed.data.status, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
return ok(response, result.document);
});
salesRouter.post("/orders/:orderId/approve", requirePermissions([permissions.salesWrite]), async (request, response) => {
const orderId = getRouteParam(request.params.orderId);
if (!orderId) {
return fail(response, 400, "INVALID_INPUT", "Sales order id is invalid.");
}
const result = await approveSalesDocument("ORDER", orderId, request.authUser?.id);
if (!result.ok) {
return fail(response, 400, "INVALID_INPUT", result.reason);
}
return ok(response, result.document);
});
salesRouter.get("/orders/:orderId/revisions", requirePermissions([permissions.salesRead]), async (request, response) => {
const orderId = getRouteParam(request.params.orderId);
if (!orderId) {
return fail(response, 400, "INVALID_INPUT", "Sales order id is invalid.");
}
const order = await getSalesDocumentById("ORDER", orderId);
if (!order) {
return fail(response, 404, "SALES_ORDER_NOT_FOUND", "Sales order was not found.");
}
return ok(response, await listSalesDocumentRevisions("ORDER", orderId));
});

View File

@@ -2,6 +2,7 @@ import type {
SalesCustomerOptionDto,
SalesDocumentDetailDto,
SalesDocumentInput,
SalesDocumentRevisionDto,
SalesDocumentStatus,
SalesDocumentSummaryDto,
SalesDocumentType,
@@ -60,12 +61,24 @@ type SalesLineRecord = {
};
};
type RevisionRecord = {
id: string;
revisionNumber: number;
reason: string;
createdAt: Date;
createdBy: {
firstName: string;
lastName: string;
} | null;
};
type SalesDocumentRecord = {
id: string;
documentNumber: string;
status: string;
issueDate: Date;
expiresAt?: Date | null;
approvedAt: Date | null;
discountPercent: number;
taxPercent: number;
freightAmount: number;
@@ -77,10 +90,28 @@ type SalesDocumentRecord = {
name: string;
email: string;
};
approvedBy: {
firstName: string;
lastName: string;
} | null;
revisions: RevisionRecord[];
lines: SalesLineRecord[];
};
const documentConfig = {
type DocumentConfig = {
prefix: string;
findMany: typeof prisma.salesQuote.findMany;
findUnique: typeof prisma.salesQuote.findUnique;
create: typeof prisma.salesQuote.create;
update: typeof prisma.salesQuote.update;
count: typeof prisma.salesQuote.count;
revisionFindMany: typeof prisma.salesQuoteRevision.findMany;
revisionAggregate: typeof prisma.salesQuoteRevision.aggregate;
revisionCreate: typeof prisma.salesQuoteRevision.create;
revisionDocumentField: "quoteId" | "orderId";
};
const documentConfig: Record<SalesDocumentType, DocumentConfig> = {
QUOTE: {
prefix: "Q",
findMany: (prisma as any).salesQuote.findMany.bind((prisma as any).salesQuote),
@@ -88,6 +119,10 @@ const documentConfig = {
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),
revisionFindMany: (prisma as any).salesQuoteRevision.findMany.bind((prisma as any).salesQuoteRevision),
revisionAggregate: (prisma as any).salesQuoteRevision.aggregate.bind((prisma as any).salesQuoteRevision),
revisionCreate: (prisma as any).salesQuoteRevision.create.bind((prisma as any).salesQuoteRevision),
revisionDocumentField: "quoteId",
},
ORDER: {
prefix: "SO",
@@ -96,8 +131,12 @@ const documentConfig = {
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),
revisionFindMany: (prisma as any).salesOrderRevision.findMany.bind((prisma as any).salesOrderRevision),
revisionAggregate: (prisma as any).salesOrderRevision.aggregate.bind((prisma as any).salesOrderRevision),
revisionCreate: (prisma as any).salesOrderRevision.create.bind((prisma as any).salesOrderRevision),
revisionDocumentField: "orderId",
},
} as const;
};
function roundMoney(value: number) {
return Math.round(value * 100) / 100;
@@ -124,6 +163,24 @@ function calculateTotals(subtotal: number, discountPercent: number, taxPercent:
};
}
function getUserDisplayName(user: { firstName: string; lastName: string } | null) {
if (!user) {
return null;
}
return `${user.firstName} ${user.lastName}`.trim();
}
function mapRevision(record: RevisionRecord): SalesDocumentRevisionDto {
return {
id: record.id,
revisionNumber: record.revisionNumber,
reason: record.reason,
createdAt: record.createdAt.toISOString(),
createdByName: getUserDisplayName(record.createdBy),
};
}
function normalizeLines(lines: SalesLineInput[]) {
return lines
.map((line, index) => ({
@@ -187,6 +244,10 @@ function mapDocument(record: SalesDocumentRecord): SalesDocumentDetailDto {
record.taxPercent,
record.freightAmount
);
const revisions = record.revisions
.slice()
.sort((left, right) => right.revisionNumber - left.revisionNumber)
.map(mapRevision);
return {
id: record.id,
@@ -195,6 +256,9 @@ function mapDocument(record: SalesDocumentRecord): SalesDocumentDetailDto {
customerName: record.customer.name,
customerEmail: record.customer.email,
status: record.status as SalesDocumentStatus,
approvedAt: record.approvedAt ? record.approvedAt.toISOString() : null,
approvedByName: getUserDisplayName(record.approvedBy),
currentRevisionNumber: revisions[0]?.revisionNumber ?? 0,
subtotal: totals.subtotal,
discountPercent: totals.discountPercent,
discountAmount: totals.discountAmount,
@@ -209,16 +273,95 @@ function mapDocument(record: SalesDocumentRecord): SalesDocumentDetailDto {
updatedAt: record.updatedAt.toISOString(),
lineCount: lines.length,
lines,
revisions,
};
}
function buildRevisionSnapshot(document: SalesDocumentDetailDto) {
return JSON.stringify({
documentNumber: document.documentNumber,
customerId: document.customerId,
customerName: document.customerName,
status: document.status,
approvedAt: document.approvedAt,
approvedByName: document.approvedByName,
issueDate: document.issueDate,
expiresAt: document.expiresAt,
discountPercent: document.discountPercent,
discountAmount: document.discountAmount,
taxPercent: document.taxPercent,
taxAmount: document.taxAmount,
freightAmount: document.freightAmount,
subtotal: document.subtotal,
total: document.total,
notes: document.notes,
lines: document.lines.map((line) => ({
itemId: line.itemId,
itemSku: line.itemSku,
itemName: line.itemName,
description: line.description,
quantity: line.quantity,
unitOfMeasure: line.unitOfMeasure,
unitPrice: line.unitPrice,
lineTotal: line.lineTotal,
position: line.position,
})),
});
}
async function createRevision(
type: SalesDocumentType,
documentId: string,
detail: SalesDocumentDetailDto,
reason: string,
userId?: string
) {
const aggregate = await documentConfig[type].revisionAggregate({
where: { [documentConfig[type].revisionDocumentField]: documentId },
_max: { revisionNumber: true },
});
const nextRevisionNumber = (aggregate._max.revisionNumber ?? 0) + 1;
if (type === "QUOTE") {
await prisma.salesQuoteRevision.create({
data: {
quoteId: documentId,
revisionNumber: nextRevisionNumber,
reason,
snapshot: buildRevisionSnapshot(detail),
createdById: userId ?? null,
},
});
return;
}
await prisma.salesOrderRevision.create({
data: {
orderId: documentId,
revisionNumber: nextRevisionNumber,
reason,
snapshot: buildRevisionSnapshot(detail),
createdById: userId ?? null,
},
});
}
async function getDocumentDetailOrNull(type: SalesDocumentType, documentId: string) {
const record = await documentConfig[type].findUnique({
where: { id: documentId },
include: buildInclude(),
});
return record ? mapDocument(record as SalesDocumentRecord) : null;
}
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 = {
function buildInclude() {
return {
customer: {
select: {
id: true,
@@ -226,28 +369,23 @@ function buildInclude(type: SalesDocumentType) {
email: true,
},
},
};
if (type === "QUOTE") {
return {
...base,
lines: {
include: {
item: {
select: {
id: true,
sku: true,
name: true,
},
approvedBy: {
select: {
firstName: true,
lastName: true,
},
},
revisions: {
include: {
createdBy: {
select: {
firstName: true,
lastName: true,
},
},
orderBy: [{ position: "asc" }, { createdAt: "asc" }],
},
};
}
return {
...base,
orderBy: [{ revisionNumber: "desc" as const }],
},
lines: {
include: {
item: {
@@ -258,7 +396,7 @@ function buildInclude(type: SalesDocumentType) {
},
},
},
orderBy: [{ position: "asc" }, { createdAt: "asc" }],
orderBy: [{ position: "asc" as const }, { createdAt: "asc" as const }],
},
};
}
@@ -328,7 +466,7 @@ export async function listSalesDocuments(type: SalesDocumentType, filters: { q?:
}
: {}),
},
include: buildInclude(type),
include: buildInclude(),
orderBy: [{ issueDate: "desc" }, { createdAt: "desc" }],
});
@@ -340,6 +478,9 @@ export async function listSalesDocuments(type: SalesDocumentType, filters: { q?:
customerId: detail.customerId,
customerName: detail.customerName,
status: detail.status,
approvedAt: detail.approvedAt,
approvedByName: detail.approvedByName,
currentRevisionNumber: detail.currentRevisionNumber,
subtotal: detail.subtotal,
discountPercent: detail.discountPercent,
discountAmount: detail.discountAmount,
@@ -357,15 +498,27 @@ export async function listSalesDocuments(type: SalesDocumentType, filters: { q?:
}
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;
return getDocumentDetailOrNull(type, documentId);
}
export async function createSalesDocument(type: SalesDocumentType, payload: SalesDocumentInput) {
export async function listSalesDocumentRevisions(type: SalesDocumentType, documentId: string) {
const revisions = await documentConfig[type].revisionFindMany({
where: { [documentConfig[type].revisionDocumentField]: documentId },
include: {
createdBy: {
select: {
firstName: true,
lastName: true,
},
},
},
orderBy: [{ revisionNumber: "desc" }],
});
return revisions.map((revision: RevisionRecord) => mapRevision(revision));
}
export async function createSalesDocument(type: SalesDocumentType, payload: SalesDocumentInput, userId?: string) {
const validatedLines = await validateLines(payload.lines);
if (!validatedLines.ok) {
return { ok: false as const, reason: validatedLines.reason };
@@ -382,32 +535,60 @@ export async function createSalesDocument(type: SalesDocumentType, payload: Sale
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 createdId = await prisma.$transaction(async (tx) => {
const created =
type === "QUOTE"
? await tx.salesQuote.create({
data: {
documentNumber,
customerId: payload.customerId,
status: payload.status,
issueDate: new Date(payload.issueDate),
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 },
})
: await tx.salesOrder.create({
data: {
documentNumber,
customerId: payload.customerId,
status: payload.status,
issueDate: new Date(payload.issueDate),
discountPercent: payload.discountPercent,
taxPercent: payload.taxPercent,
freightAmount: payload.freightAmount,
notes: payload.notes,
lines: {
create: validatedLines.lines,
},
},
select: { id: true },
});
return created.id;
});
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." };
const detail = await getDocumentDetailOrNull(type, createdId);
if (!detail) {
return { ok: false as const, reason: "Unable to load saved document." };
}
await createRevision(type, createdId, detail, payload.revisionReason?.trim() || "Initial issue", userId);
const refreshed = await getDocumentDetailOrNull(type, createdId);
return refreshed ? { ok: true as const, document: refreshed } : { ok: false as const, reason: "Unable to load saved document." };
}
export async function updateSalesDocument(type: SalesDocumentType, documentId: string, payload: SalesDocumentInput) {
export async function updateSalesDocument(type: SalesDocumentType, documentId: string, payload: SalesDocumentInput, userId?: string) {
const existing = await documentConfig[type].findUnique({
where: { id: documentId },
select: { id: true },
select: { id: true, approvedAt: true },
});
if (!existing) {
@@ -439,6 +620,8 @@ export async function updateSalesDocument(type: SalesDocumentType, documentId: s
taxPercent: payload.taxPercent,
freightAmount: payload.freightAmount,
notes: payload.notes,
approvedAt: payload.status === "APPROVED" ? existing.approvedAt ?? new Date() : null,
approvedById: payload.status === "APPROVED" ? userId ?? null : null,
lines: {
deleteMany: {},
create: validatedLines.lines,
@@ -447,14 +630,20 @@ export async function updateSalesDocument(type: SalesDocumentType, documentId: s
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." };
const detail = await getDocumentDetailOrNull(type, documentId);
if (!detail) {
return { ok: false as const, reason: "Unable to load saved document." };
}
await createRevision(type, documentId, detail, payload.revisionReason?.trim() || "Document edited", userId);
const refreshed = await getDocumentDetailOrNull(type, documentId);
return refreshed ? { ok: true as const, document: refreshed } : { ok: false as const, reason: "Unable to load saved document." };
}
export async function updateSalesDocumentStatus(type: SalesDocumentType, documentId: string, status: SalesDocumentStatus) {
export async function updateSalesDocumentStatus(type: SalesDocumentType, documentId: string, status: SalesDocumentStatus, userId?: string) {
const existing = await documentConfig[type].findUnique({
where: { id: documentId },
select: { id: true },
select: { id: true, status: true, approvedAt: true },
});
if (!existing) {
@@ -463,18 +652,62 @@ export async function updateSalesDocumentStatus(type: SalesDocumentType, documen
await documentConfig[type].update({
where: { id: documentId },
data: { status },
data: {
status,
approvedAt: status === "APPROVED" ? existing.approvedAt ?? new Date() : null,
approvedById: status === "APPROVED" ? userId ?? null : null,
},
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." };
const detail = await getDocumentDetailOrNull(type, documentId);
if (!detail) {
return { ok: false as const, reason: "Unable to load updated document." };
}
await createRevision(type, documentId, detail, `Status changed to ${status}`, userId);
const refreshed = await getDocumentDetailOrNull(type, documentId);
return refreshed ? { ok: true as const, document: refreshed } : { ok: false as const, reason: "Unable to load updated document." };
}
export async function convertQuoteToSalesOrder(quoteId: string) {
export async function approveSalesDocument(type: SalesDocumentType, documentId: string, userId?: string) {
const existing = await documentConfig[type].findUnique({
where: { id: documentId },
select: { id: true, status: true, approvedAt: true },
});
if (!existing) {
return { ok: false as const, reason: "Sales document was not found." };
}
if (existing.status === "CLOSED") {
return { ok: false as const, reason: "Closed sales documents cannot be approved." };
}
await documentConfig[type].update({
where: { id: documentId },
data: {
status: "APPROVED",
approvedAt: existing.approvedAt ?? new Date(),
approvedById: userId ?? null,
},
select: { id: true },
});
const detail = await getDocumentDetailOrNull(type, documentId);
if (!detail) {
return { ok: false as const, reason: "Unable to load approved document." };
}
await createRevision(type, documentId, detail, "Document approved", userId);
const refreshed = await getDocumentDetailOrNull(type, documentId);
return refreshed ? { ok: true as const, document: refreshed } : { ok: false as const, reason: "Unable to load approved document." };
}
export async function convertQuoteToSalesOrder(quoteId: string, userId?: string) {
const quote = await documentConfig.QUOTE.findUnique({
where: { id: quoteId },
include: buildInclude("QUOTE"),
include: buildInclude(),
});
if (!quote) {
@@ -484,32 +717,42 @@ export async function convertQuoteToSalesOrder(quoteId: string) {
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,
})),
const createdId = await prisma.$transaction(async (tx) => {
const created = await tx.salesOrder.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 },
select: { id: true },
});
return created.id;
});
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." };
const order = await getDocumentDetailOrNull("ORDER", createdId);
if (!order) {
return { ok: false as const, reason: "Unable to load converted sales order." };
}
await createRevision("ORDER", createdId, order, `Converted from quote ${mappedQuote.documentNumber}`, userId);
const refreshed = await getDocumentDetailOrNull("ORDER", createdId);
return refreshed ? { ok: true as const, document: refreshed } : { ok: false as const, reason: "Unable to load converted sales order." };
}
export async function getSalesDocumentPdfData(type: SalesDocumentType, documentId: string): Promise<SalesDocumentPdfData | null> {