sales
This commit is contained in:
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user