From a6deda1c0c3b7c9234792ac8b2e906d7ed287ddb Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Wed, 29 Oct 2025 13:25:58 +0330 Subject: [PATCH] fix : same values for report and chart --- src/modules/order/order.repository.ts | 324 ++++++++++++++------------ src/modules/order/order.service.ts | 2 +- 2 files changed, 179 insertions(+), 147 deletions(-) diff --git a/src/modules/order/order.repository.ts b/src/modules/order/order.repository.ts index d8c09bc..3c9fb73 100644 --- a/src/modules/order/order.repository.ts +++ b/src/modules/order/order.repository.ts @@ -512,143 +512,6 @@ class OrderRepository extends BaseRepository { }, ]); } - - async getOrderChart(granularity: Gran = "daily", from?: string, to?: string) { - // تبدیل/نرمالایز تاریخ‌ها - const MS_PER_DAY = 24 * 60 * 60 * 1000; - const endDate = to ? new Date(to) : new Date(); - endDate.setHours(23, 59, 59, 999); - - const defaultStart = new Date(Date.now() - 30 * MS_PER_DAY); - const startDate = from ? new Date(from) : defaultStart; - startDate.setHours(0, 0, 0, 0); - - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { - throw new Error("Invalid date format. Expected YYYY-MM-DD or ISO date."); - } - - if (startDate > endDate) { - // swap - const tmp = new Date(startDate); - (startDate as any) = new Date(endDate); - (endDate as any) = tmp; - } - - // validate granularity - const allowed: Record = { - daily: "day", - weekly: "week", - monthly: "month", - }; - const unit = allowed[granularity] ?? allowed["daily"]; - - const excludedOrderItemStatuses = [ - OrderItemsStatus.cancelled_shop, - OrderItemsStatus.cancelled_system, - OrderItemsStatus.cancelled_user, - OrderItemsStatus.Returned, - ]; - - const pipeline: any[] = [ - { - $match: { - createdAt: { $gte: startDate, $lte: endDate }, - orderStatus: { - $nin: [OrdersStatus.wait_payment, OrdersStatus.cancelled_system, OrdersStatus.Cancelled], - }, - }, - }, - { - $lookup: { - from: "orderitems", - localField: "_id", - foreignField: "order", - as: "orderItems", - }, - }, - { - $addFields: { - orderItems: { - $filter: { - input: "$orderItems", - as: "item", - cond: { - $not: [{ $in: ["$$item.status", excludedOrderItemStatuses] }], - }, - }, - }, - }, - }, - { $unwind: "$orderItems" }, - { - $addFields: { - lineTotal: { $ifNull: ["$orderItems.totalPaymentPrice", 0] }, - }, - }, - { - $group: { - _id: { - period: { - $dateTrunc: { - date: "$createdAt", - unit: unit, - binSize: 1, - }, - }, - }, - periodTotal: { $sum: "$lineTotal" }, - }, - }, - { $sort: { "_id.period": 1 } }, - ]; - - const aggResults: Array<{ _id: { period: Date }; periodTotal: number }> = await this.model.aggregate(pipeline).exec(); - - const map = new Map(); - for (const r of aggResults) { - const d: Date = r._id.period; - const key = unit === "month" ? d.toISOString().slice(0, 7) : d.toISOString().slice(0, 10); - map.set(key, r.periodTotal ?? 0); - } - - // helper برای قدم زدن در دوره‌ها - function formatKey(d: Date) { - return unit === "month" ? d.toISOString().slice(0, 7) : d.toISOString().slice(0, 10); - } - function addPeriod(d: Date) { - const nd = new Date(d); - if (unit === "day") nd.setDate(nd.getDate() + 1); - else if (unit === "week") nd.setDate(nd.getDate() + 7); - else nd.setMonth(nd.getMonth() + 1); - return nd; - } - - const xAxis: string[] = []; - const yAxis: number[] = []; - - let cursor = new Date(startDate); - if (unit === "month") cursor.setDate(1); - - while (cursor <= endDate) { - const key = formatKey(cursor); - xAxis.push(key); - yAxis.push(map.get(key) ?? 0); - cursor = addPeriod(cursor); - } - - const totalSales = yAxis.reduce((s, v) => s + v, 0); - - return { - chartType: "line", - title: "نمودار کل فروش", - unit: "تومان", - granularity, - xAxis, - yAxis, - totalSales, - _debug: { aggregatedPeriods: aggResults.length }, - }; - } } class OrderItemRepo extends BaseRepository { @@ -2405,13 +2268,32 @@ class OrderItemRepo extends BaseRepository { return salesData; } + // Shared constants (place near top of file) async getMonthlySalesReport() { + const EXCLUDED_ORDER_ITEM_STATUSES = [ + OrderItemsStatus.cancelled_shop, + OrderItemsStatus.cancelled_system, + OrderItemsStatus.cancelled_user, + OrderItemsStatus.Returned, + ]; + + const EXCLUDED_ORDER_STATUSES = [OrdersStatus.wait_payment, OrdersStatus.cancelled_system, OrdersStatus.Cancelled]; + const today = new Date(); const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 1); const salesData = await this.model.aggregate([ + // filter items by month & item status first + { + $match: { + createdAt: { $gte: startOfMonth, $lt: endOfMonth }, + status: { $nin: EXCLUDED_ORDER_ITEM_STATUSES }, + }, + }, + + // join the parent order { $lookup: { from: "orders", @@ -2420,31 +2302,181 @@ class OrderItemRepo extends BaseRepository { as: "order", }, }, + { $unwind: "$order" }, + + // filter by parent order status { $match: { - createdAt: { - $gte: startOfMonth, - $lt: endOfMonth, - }, - status: { - $nin: ["cancelled_shop", "cancelled_system", "Returned", OrdersStatus.wait_payment], - }, + "order.orderStatus": { $nin: EXCLUDED_ORDER_STATUSES }, }, }, + + // aggregate totals and collect distinct order ids { $group: { - _id: null, // No need for grouping by date since it's filtered for the entire month + _id: null, totalSales: { $sum: "$totalSellingPrice" }, totalNetSales: { $sum: "$totalRetailPrice" }, itemsSold: { $sum: "$itemsCount" }, - totalOrders: { $sum: 1 }, + orderIds: { $addToSet: "$order._id" }, }, }, + + // convert orderIds into a number + { + $addFields: { + totalOrders: { $size: "$orderIds" }, + }, + }, + + // optionally remove the array from results + { + $project: { orderIds: 0 }, + }, ]); return salesData.length > 0 ? salesData[0] : null; } + async getOrderChart(granularity: Gran = "daily", from?: string, to?: string) { + const EXCLUDED_ORDER_ITEM_STATUSES = [ + OrderItemsStatus.cancelled_shop, + OrderItemsStatus.cancelled_system, + OrderItemsStatus.cancelled_user, + OrderItemsStatus.Returned, + ]; + + const EXCLUDED_ORDER_STATUSES = [OrdersStatus.wait_payment, OrdersStatus.cancelled_system, OrdersStatus.Cancelled]; + + const DATE_TRUNC_TIMEZONE = "Asia/Tehran"; + const MS_PER_DAY = 24 * 60 * 60 * 1000; + const today = new Date(); + + // if user asked for monthly and did not give from/to, use calendar month + let startDate: Date; + let endDate: Date; + + if (granularity === "monthly" && !from && !to) { + startDate = new Date(today.getFullYear(), today.getMonth(), 1); + endDate = new Date(today.getFullYear(), today.getMonth() + 1, 1); + } else { + endDate = to ? new Date(to) : new Date(); + endDate.setHours(23, 59, 59, 999); + const defaultStart = new Date(Date.now() - 30 * MS_PER_DAY); + startDate = from ? new Date(from) : defaultStart; + startDate.setHours(0, 0, 0, 0); + } + + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + throw new Error("Invalid date format. Expected YYYY-MM-DD or ISO date."); + } + + if (startDate > endDate) { + const tmp = startDate; + startDate = endDate; + endDate = tmp; + } + + const allowed: Record = { + daily: "day", + weekly: "week", + monthly: "month", + }; + const unit = allowed[granularity] ?? "day"; + + const pipeline: any[] = [ + // 1. filter order items by date and item-status + { + $match: { + createdAt: { $gte: startDate, $lte: endDate }, + status: { $nin: EXCLUDED_ORDER_ITEM_STATUSES }, + }, + }, + + // 2. join parent order and filter by order status + { + $lookup: { + from: "orders", + localField: "order", + foreignField: "_id", + as: "order", + }, + }, + { $unwind: "$order" }, + { + $match: { + "order.orderStatus": { $nin: EXCLUDED_ORDER_STATUSES }, + }, + }, + + // 3. group by truncated period (include timezone) + { + $group: { + _id: { + period: { + $dateTrunc: { + date: "$createdAt", + unit: unit, + binSize: 1, + timezone: DATE_TRUNC_TIMEZONE, + }, + }, + }, + periodTotal: { $sum: "$totalSellingPrice" }, + }, + }, + + { $sort: { "_id.period": 1 } }, + ]; + + const aggResults: Array<{ _id: { period: Date }; periodTotal: number }> = await this.model.aggregate(pipeline).exec(); + + // build map and x/y arrays (same logic as you had) + const map = new Map(); + for (const r of aggResults) { + const d: Date = r._id.period; + const key = unit === "month" ? d.toISOString().slice(0, 7) : d.toISOString().slice(0, 10); + map.set(key, r.periodTotal ?? 0); + } + + function formatKey(d: Date) { + return unit === "month" ? d.toISOString().slice(0, 7) : d.toISOString().slice(0, 10); + } + function addPeriod(d: Date) { + const nd = new Date(d); + if (unit === "day") nd.setDate(nd.getDate() + 1); + else if (unit === "week") nd.setDate(nd.getDate() + 7); + else nd.setMonth(nd.getMonth() + 1); + return nd; + } + + const xAxis: string[] = []; + const yAxis: number[] = []; + + let cursor = new Date(startDate); + if (unit === "month") cursor.setDate(1); + + while (cursor <= endDate) { + const key = formatKey(cursor); + xAxis.push(key); + yAxis.push(map.get(key) ?? 0); + cursor = addPeriod(cursor); + } + + const totalSales = yAxis.reduce((s, v) => s + v, 0); + + return { + chartType: "line", + title: "نمودار کل فروش", + unit: "تومان", + granularity, + xAxis, + yAxis, + totalSales, + _debug: { aggregatedPeriods: aggResults.length }, + }; + } + async getAnnualSalesReport() { const today = new Date(); const startOfYear = new Date(today.getFullYear(), 0, 1); diff --git a/src/modules/order/order.service.ts b/src/modules/order/order.service.ts index 3f8c40f..a3f3b64 100644 --- a/src/modules/order/order.service.ts +++ b/src/modules/order/order.service.ts @@ -433,7 +433,7 @@ class OrderService { //####################################### //####################################### async getSaleOrderChart(granularity?: Gran, from?: string, to?: string) { - return this.orderRepo.getOrderChart(granularity, from, to); + return this.orderItemRepo.getOrderChart(granularity, from, to); } //#######################################