This commit is contained in:
2026-03-14 23:39:51 -05:00
parent a54e5901f0
commit ff37ad6f06
9 changed files with 190 additions and 3 deletions

View File

@@ -30,6 +30,9 @@ const quoteSchema = z.object({
status: z.enum(salesDocumentStatuses),
issueDate: z.string().datetime(),
expiresAt: z.string().datetime().nullable(),
discountPercent: z.number().min(0).max(100),
taxPercent: z.number().min(0).max(100),
freightAmount: z.number().nonnegative(),
notes: z.string(),
lines: z.array(salesLineSchema),
});
@@ -38,6 +41,9 @@ const orderSchema = z.object({
customerId: z.string().trim().min(1),
status: z.enum(salesDocumentStatuses),
issueDate: z.string().datetime(),
discountPercent: z.number().min(0).max(100),
taxPercent: z.number().min(0).max(100),
freightAmount: z.number().nonnegative(),
notes: z.string(),
lines: z.array(salesLineSchema),
});

View File

@@ -30,6 +30,9 @@ type SalesDocumentRecord = {
status: string;
issueDate: Date;
expiresAt?: Date | null;
discountPercent: number;
taxPercent: number;
freightAmount: number;
notes: string;
createdAt: Date;
updatedAt: Date;
@@ -60,6 +63,31 @@ const documentConfig = {
},
} 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) => ({
@@ -117,6 +145,12 @@ function mapDocument(record: SalesDocumentRecord): SalesDocumentDetailDto {
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,
@@ -125,7 +159,13 @@ function mapDocument(record: SalesDocumentRecord): SalesDocumentDetailDto {
customerName: record.customer.name,
customerEmail: record.customer.email,
status: record.status as SalesDocumentStatus,
subtotal: lines.reduce((sum, line) => sum + line.lineTotal, 0),
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,
@@ -198,6 +238,7 @@ export async function listSalesCustomerOptions(): Promise<SalesCustomerOptionDto
id: true,
name: true,
email: true,
resellerDiscountPercent: true,
},
orderBy: [{ name: "asc" }],
});
@@ -232,6 +273,12 @@ export async function listSalesDocuments(type: SalesDocumentType, filters: { q?:
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,
@@ -274,6 +321,9 @@ export async function createSalesDocument(type: SalesDocumentType, payload: Sale
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,
@@ -317,6 +367,9 @@ export async function updateSalesDocument(type: SalesDocumentType, documentId: s
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: {},
@@ -369,6 +422,9 @@ export async function convertQuoteToSalesOrder(quoteId: string) {
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) => ({