From 2f99e27f0fe9b240d6a45905377a662e17c3b204 Mon Sep 17 00:00:00 2001 From: zhuotianyuan Date: Sat, 27 Dec 2025 16:06:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor(shopyy):=20=E9=87=8D=E6=9E=84=E8=AE=A2?= =?UTF-8?q?=E5=8D=95DTO=E5=92=8C=E6=9C=8D=E5=8A=A1=E9=80=BB=E8=BE=91=20fix?= =?UTF-8?q?:=20=E4=BF=AE=E5=A4=8D=E6=9C=AC=E5=9C=B0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E9=85=8D=E7=BD=AE=E7=AB=AF=E5=8F=A3=E5=92=8C=E5=AF=86?= =?UTF-8?q?=E7=A0=81=20feat(statistics):=20=E6=94=AF=E6=8C=81=E6=8C=89?= =?UTF-8?q?=E6=97=A5/=E5=91=A8/=E6=9C=88=E5=88=86=E7=BB=84=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E8=AE=A2=E5=8D=95=E6=95=B0=E6=8D=AE=20feat(order):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AE=A2=E5=8D=95=E5=AF=BC=E5=87=BACSV?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20style:=20=E6=B8=85=E7=90=86=E6=97=A0?= =?UTF-8?q?=E7=94=A8=E4=BB=A3=E7=A0=81=E5=92=8C=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/adapter/shopyy.adapter.ts | 42 ++- src/dto/shopyy.dto.ts | 15 +- src/dto/site-api.dto.ts | 62 +++-- src/dto/statistics.dto.ts | 5 + src/service/order.service.ts | 250 +++++++++++++++--- src/service/shopyy.service.ts | 16 +- src/service/statistics.service.ts | 419 +++++++++++++++++++++++++++++- 7 files changed, 732 insertions(+), 77 deletions(-) diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index f0332b9..1d1ece5 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -25,7 +25,9 @@ import { ShopyyVariant, ShopyyWebhook, } from '../dto/shopyy.dto'; - +import { + OrderStatus, +} from '../enums/base.enum'; export class ShopyyAdapter implements ISiteAdapter { constructor(private site: any, private shopyyService: ShopyyService) { this.mapCustomer = this.mapCustomer.bind(this); @@ -129,6 +131,13 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + shopyyOrderAutoNextStatusMap = {//订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; + [100]: OrderStatus.PENDING, // 100 未完成 转为 pending + [110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing + [180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed + [190]: OrderStatus.CANCEL // 190 取消 转为 cancelled + } + private mapOrder(item: ShopyyOrder): UnifiedOrderDTO { // 提取账单和送货地址 如果不存在则为空对象 const billing = (item as any).billing_address || {}; @@ -202,9 +211,27 @@ export class ShopyyAdapter implements ISiteAdapter { quantity: p.quantity, total: String(p.price ?? ''), sku: p.sku || p.sku_code || '', + price: String(p.price ?? ''), }) ); + const currencySymbols: Record = { + 'EUR': '€', + 'USD': '$', + 'GBP': '£', + 'JPY': '¥', + 'AUD': 'A$', + 'CAD': 'C$', + 'CHF': 'CHF', + 'CNY': '¥', + 'HKD': 'HK$', + 'NZD': 'NZ$', + 'SGD': 'S$' + // 可以根据需要添加更多货币代码和符号 + }; + const originStatus = item.status; + item.status = this.shopyyOrderAutoNextStatusMap[originStatus]; + return { id: item.id || item.order_id, number: item.order_number || item.order_sn, @@ -222,10 +249,15 @@ export class ShopyyAdapter implements ISiteAdapter { billing_full_address: formatAddress(billingObj), shipping_full_address: formatAddress(shippingObj), payment_method: item.payment_method, - shipping_lines: item.shipping_lines, - fee_lines: item.fee_lines, - coupon_lines: item.coupon_lines, + shipping_lines: item.fulfillments || [], + fee_lines: item.fee_lines || [], + coupon_lines: item.coupon_lines || [], + date_paid: typeof item.pay_at === 'number' + ? item.pay_at === 0 ? null : new Date(item.pay_at * 1000).toISOString() + : null, + refunds: [], + currency_symbol: (currencySymbols[item.currency] || '$') || '', date_created: typeof item.created_at === 'number' ? new Date(item.created_at * 1000).toISOString() @@ -241,7 +273,7 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - + private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { // 处理多地址结构 diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index 3a7f4ad..f95fba3 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -246,17 +246,18 @@ export interface ShopyyOrder { updated_at?: number | string; date_updated?: string; last_modified?: string; - + // 支付时间 + pay_at?: number ; // 配送方式 - shipping_lines?: Array; + shipping_lines?: Array; // 费用项 - fee_lines?: Array; + fee_lines?: Array; // 优惠券项 - coupon_lines?: Array; + coupon_lines?: Array; } -export class UnifiedShippingLineDTO { +export class ShopyyShippingLineDTO { // 配送方式DTO用于承载统一配送方式数据 id?: string | number; @@ -274,7 +275,7 @@ export class UnifiedShippingLineDTO { } -export class UnifiedFeeLineDTO { +export class ShopyyFeeLineDTO { // 费用项DTO用于承载统一费用项数据 id?: string | number; @@ -293,7 +294,7 @@ export class UnifiedFeeLineDTO { meta_data?: any[]; } -export class UnifiedCouponLineDTO { +export class ShopyyCouponLineDTO { // 优惠券项DTO用于承载统一优惠券项数据 id?: string | number; code?: string; diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts index 8376402..6d6c167 100644 --- a/src/dto/site-api.dto.ts +++ b/src/dto/site-api.dto.ts @@ -16,6 +16,12 @@ export class UnifiedPaginationDTO { @ApiProperty({ description: '总页数', example: 5 }) totalPages: number; + + @ApiProperty({ description: '分页后的数据', required: false }) + after?: string; + + @ApiProperty({ description: '分页前的数据', required: false }) + before?: string; } export class UnifiedTagDTO { // 标签DTO用于承载统一标签数据 @@ -258,6 +264,9 @@ export class UnifiedOrderDTO { @ApiProperty({ description: '货币' }) currency: string; + @ApiProperty({ description: '货币符号' }) + currency_symbol?: string; + @ApiProperty({ description: '总金额' }) total: string; @@ -312,6 +321,9 @@ export class UnifiedOrderDTO { @ApiProperty({ description: '优惠券项', type: () => [UnifiedCouponLineDTO], required: false }) coupon_lines?: UnifiedCouponLineDTO[]; + + @ApiProperty({ description: '支付时间', required: false }) + date_paid?: string ; } export class UnifiedShippingLineDTO { @@ -324,7 +336,7 @@ export class UnifiedShippingLineDTO { @ApiProperty({ description: '配送方式实例ID' }) method_id?: string; - + @ApiProperty({ description: '配送方式金额' }) total?: string; @@ -336,7 +348,7 @@ export class UnifiedShippingLineDTO { @ApiProperty({ description: '配送方式元数据' }) meta_data?: any[]; - + } export class UnifiedFeeLineDTO { @@ -347,23 +359,23 @@ export class UnifiedFeeLineDTO { @ApiProperty({ description: '费用项名称' }) name?: string; - @ApiProperty({ description: '税率类' }) - tax_class?: string; + @ApiProperty({ description: '税率类' }) + tax_class?: string; - @ApiProperty({ description: '税率状态' }) - tax_status?: string; + @ApiProperty({ description: '税率状态' }) + tax_status?: string; - @ApiProperty({ description: '总金额' }) - total?: string; + @ApiProperty({ description: '总金额' }) + total?: string; - @ApiProperty({ description: '总税额' }) - total_tax?: string; + @ApiProperty({ description: '总税额' }) + total_tax?: string; - @ApiProperty({ description: '税额详情' }) - taxes?: any[]; + @ApiProperty({ description: '税额详情' }) + taxes?: any[]; - @ApiProperty({ description: '元数据' }) - meta_data?: any[]; + @ApiProperty({ description: '元数据' }) + meta_data?: any[]; } export class UnifiedCouponLineDTO { @@ -371,17 +383,17 @@ export class UnifiedCouponLineDTO { @ApiProperty({ description: '优惠券项ID' }) id?: string | number; - @ApiProperty({ description: '优惠券项代码' }) - code?: string; + @ApiProperty({ description: '优惠券项代码' }) + code?: string; - @ApiProperty({ description: '优惠券项折扣' }) - discount?: string; + @ApiProperty({ description: '优惠券项折扣' }) + discount?: string; - @ApiProperty({ description: '优惠券项税额' }) - discount_tax?: string; + @ApiProperty({ description: '优惠券项税额' }) + discount_tax?: string; - @ApiProperty({ description: '优惠券项元数据' }) - meta_data?: any[]; + @ApiProperty({ description: '优惠券项元数据' }) + meta_data?: any[]; } @@ -619,6 +631,12 @@ export class UnifiedSearchParamsDTO> { @ApiProperty({ description: '过滤条件对象', type: 'object', required: false }) where?: Where; + + @ApiProperty({ description: '创建时间后', required: false }) + after?: string; + + @ApiProperty({ description: '创建时间前', required: false }) + before?: string; @ApiProperty({ description: '排序对象,例如 { "sku": "desc" }', diff --git a/src/dto/statistics.dto.ts b/src/dto/statistics.dto.ts index 31e10a8..2bd6250 100644 --- a/src/dto/statistics.dto.ts +++ b/src/dto/statistics.dto.ts @@ -33,4 +33,9 @@ export class OrderStatisticsParams { @ApiProperty({ enum: ['all', 'zyn', 'yoone', 'zolt'], default: 'all' }) @Rule(RuleType.string().valid('all', 'zyn', 'yoone', 'zolt')) brand: string; + + @ApiProperty({ enum: ['day', 'week', 'month'], default: 'day' }) + @Rule(RuleType.string().valid('day', 'week', 'month')) + grouping: string; + } diff --git a/src/service/order.service.ts b/src/service/order.service.ts index f5e1503..01a22a3 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -31,7 +31,9 @@ import { UpdateStockDTO } from '../dto/stock.dto'; import { StockService } from './stock.service'; import { OrderItemOriginal } from '../entity/order_item_original.entity'; import { SiteApiService } from './site-api.service'; - +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; @Provide() export class OrderService { @@ -96,8 +98,22 @@ export class OrderService { siteApiService: SiteApiService; async syncOrders(siteId: number, params: Record = {}) { + const daysRange = 7; + +// 获取当前时间和7天前时间 + const now = new Date(); + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(now.getDate() - daysRange); + +// 格式化时间为ISO 8601 + const after = sevenDaysAgo.toISOString(); + const before = now.toISOString(); // 调用 WooCommerce API 获取订单 - const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders(params); + const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders({ + ...params, + after, + before, + }); let successCount = 0; let failureCount = 0; @@ -135,12 +151,7 @@ export class OrderService { [OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded } - shopyyOrderAutoNextStatusMap = {//订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; - [100]: OrderStatus.PENDING, // 100 未完成 转为 pending - [110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing - [180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed - [190]: OrderStatus.CANCEL // 190 取消 转为 cancelled - } + // 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换 async autoUpdateOrderStatus(siteId: number, order: any) { console.log('更新订单状态', order) @@ -188,28 +199,6 @@ export class OrderService { await this.orderModel.update({ externalOrderId: order.id, siteId: siteId }, { orderStatus: order.status, }) - }else if(site.type === 'shopyy'){ - const originStatus = orderData.status; - orderData.status= this.shopyyOrderAutoNextStatusMap[originStatus]; - // 根据currency_code获取对应货币符号 - const currencySymbols: Record = { - 'EUR': '€', - 'USD': '$', - 'GBP': '£', - 'JPY': '¥', - 'AUD': 'A$', - 'CAD': 'C$', - 'CHF': 'CHF', - 'CNY': '¥', - 'HKD': 'HK$', - 'NZD': 'NZ$', - 'SGD': 'S$' - // 可以根据需要添加更多货币代码和符号 - }; - if (orderData.currency) { - const currencyCode = orderData.currency.toUpperCase(); - orderData.currency_symbol = currencySymbols[currencyCode] || '$'; - } } const externalOrderId = order.id; if ( @@ -1733,4 +1722,205 @@ export class OrderService { // } } + + async exportOrder(ids: number[]) { + // 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号 + interface ExportData { + '日期': string; + '订单号': string; + '姓名地址': string; + '邮箱': string; + '号码': string; + '订单内容': string; + '盒数': number; + '换盒数': number; + '换货内容': string; + '快递号': string; + } + + try { + // 空值检查 + const dataSource = this.dataSourceManager.getDataSource('default'); + + // 优化事务使用 + return await dataSource.transaction(async manager => { + // 准备查询条件 + const whereCondition: any = {}; + if (ids && ids.length > 0) { + whereCondition.id = In(ids); + } + + // 获取订单、订单项和物流信息 + const orders = await manager.getRepository(Order).find({ + where: whereCondition, + relations: ['shipment'] + }); + + if (orders.length === 0) { + throw new Error('未找到匹配的订单'); + } + + // 获取所有订单ID + const orderIds = orders.map(order => order.id); + + // 获取所有订单项 + const orderItems = await manager.getRepository(OrderItem).find({ + where: { + orderId: In(orderIds) + } + }); + + // 按订单ID分组订单项 + const orderItemsByOrderId = orderItems.reduce((acc, item) => { + if (!acc[item.orderId]) { + acc[item.orderId] = []; + } + acc[item.orderId].push(item); + return acc; + }, {} as Record); + + // 构建导出数据 + const exportDataList: ExportData[] = orders.map(order => { + // 获取订单的订单项 + const items = orderItemsByOrderId[order.id] || []; + + // 计算总盒数 + const boxCount = items.reduce((total, item) => total + item.quantity, 0); + + // 构建订单内容 + const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; '); + + // 构建姓名地址 + const shipping = order.shipping; + const billing = order.billing; + const firstName = shipping?.first_name || billing?.first_name || ''; + const lastName = shipping?.last_name || billing?.last_name || ''; + const name = `${firstName} ${lastName}`.trim() || ''; + const address = shipping?.address_1 || billing?.address_1 || ''; + const address2 = shipping?.address_2 || billing?.address_2 || ''; + const city = shipping?.city || billing?.city || ''; + const state = shipping?.state || billing?.state || ''; + const postcode = shipping?.postcode || billing?.postcode || ''; + const country = shipping?.country || billing?.country || ''; + const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`; + + // 获取电话号码 + const phone = shipping?.phone || billing?.phone || ''; + + // 获取快递号 + const trackingNumber = order.shipment?.tracking_id || ''; + + // 暂时没有换货相关数据,默认为0和空字符串 + const exchangeBoxCount = 0; + const exchangeContent = ''; + + return { + '日期': order.date_created?.toISOString().split('T')[0] || '', + '订单号': order.externalOrderId || '', + '姓名地址': nameAddress, + '邮箱': order.customer_email || '', + '号码': phone, + '订单内容': orderContent, + '盒数': boxCount, + '换盒数': exchangeBoxCount, + '换货内容': exchangeContent, + '快递号': trackingNumber + }; + }); + + // 返回CSV字符串内容给前端 + const csvContent = await this.exportToCsv(exportDataList, { type: 'string' }); + return csvContent; + }); + } catch (error) { + throw new Error(`导出订单失败:${error.message}`); + } + } + + + /** + * 导出数据为CSV格式 + * @param {any[]} data 数据数组 + * @param {Object} options 配置选项 + * @param {string} [options.type='string'] 输出类型:'string' | 'buffer' + * @param {string} [options.fileName] 文件名(仅当需要写入文件时使用) + * @param {boolean} [options.writeFile=false] 是否写入文件 + * @returns {string|Buffer} 根据type返回字符串或Buffer + */ +async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise { + try { + // 检查数据是否为空 + if (!data || data.length === 0) { + throw new Error('导出数据不能为空'); + } + + const { type = 'string', fileName, writeFile = false } = options; + + // 生成表头 + const headers = Object.keys(data[0]); + let csvContent = headers.join(',') + '\n'; + + // 处理数据行 + data.forEach(item => { + const row = headers.map(key => { + const value = item[key as keyof any]; + // 处理特殊字符 + if (typeof value === 'string') { + // 转义双引号,将"替换为"" + const escapedValue = value.replace(/"/g, '""'); + // 如果包含逗号或换行符,需要用双引号包裹 + if (escapedValue.includes(',') || escapedValue.includes('\n')) { + return `"${escapedValue}"`; + } + return escapedValue; + } + // 处理日期类型 + if (value instanceof Date) { + return value.toISOString(); + } + // 处理undefined和null + if (value === undefined || value === null) { + return ''; + } + return String(value); + }).join(','); + csvContent += row + '\n'; + }); + + // 如果需要写入文件 + if (writeFile && fileName) { + // 获取当前用户目录 + const userHomeDir = os.homedir(); + + // 构建目标路径(下载目录) + const downloadsDir = path.join(userHomeDir, 'Downloads'); + + // 确保下载目录存在 + if (!fs.existsSync(downloadsDir)) { + fs.mkdirSync(downloadsDir, { recursive: true }); + } + + const filePath = path.join(downloadsDir, fileName); + + // 写入文件 + fs.writeFileSync(filePath, csvContent, 'utf8'); + + console.log(`数据已成功导出至 ${filePath}`); + return filePath; + } + + // 根据类型返回不同结果 + if (type === 'buffer') { + return Buffer.from(csvContent, 'utf8'); + } + + return csvContent; + } catch (error) { + console.error('导出CSV时出错:', error); + throw new Error(`导出CSV文件失败: ${error.message}`); + } +} + + + } diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index 55bfe25..926cd64 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -7,6 +7,7 @@ import { Site } from '../entity/site.entity'; import { UnifiedReviewDTO } from '../dto/site-api.dto'; import { ShopyyReview } from '../dto/shopyy.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; +import { UnifiedSearchParamsDTO } from '../dto/site-api.dto'; /** * ShopYY平台服务实现 */ @@ -286,6 +287,14 @@ export class ShopyyService { const response = await this.request(site, `products/${productId}/variations/${variationId}`, 'GET'); return response.data; } + mapOrderSearchParams(params: UnifiedSearchParamsDTO){ + const { after, before, ...restParams } = params; + return { + ...restParams, + ...(after ? { created_at_min: after } : {}), + ...(before ? { created_at_max: before } : {}), + } + } /** * 获取ShopYY订单列表 @@ -294,14 +303,15 @@ export class ShopyyService { * @param pageSize 每页数量 * @returns 分页订单列表 */ - async getOrders(site: any | number, page: number = 1, pageSize: number = 100): Promise { + async getOrders(site: any | number, page: number = 1, pageSize: number = 100, params: UnifiedSearchParamsDTO = {}): Promise { // 如果传入的是站点ID,则获取站点配置 const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site; // ShopYY API: GET /orders const response = await this.request(siteConfig, 'orders', 'GET', null, { page, - page_size: pageSize + limit: pageSize, + ...params }); return { @@ -313,7 +323,7 @@ export class ShopyyService { }; } - async getAllOrders(site: any | number, params: Record = {}, maxPages: number = 10, concurrencyLimit: number = 100): Promise { + async getAllOrders(site: any | number, params: Record = {}, maxPages: number = 100, concurrencyLimit: number = 100): Promise { const firstPage = await this.getOrders(site, 1, 100); const { items: firstPageItems, totalPages} = firstPage; diff --git a/src/service/statistics.service.ts b/src/service/statistics.service.ts index 71804ce..688ed3d 100644 --- a/src/service/statistics.service.ts +++ b/src/service/statistics.service.ts @@ -15,11 +15,13 @@ export class StatisticsService { orderItemRepository: Repository; async getOrderStatistics(params: OrderStatisticsParams) { - const { startDate, endDate, siteId } = params; + const { startDate, endDate, siteId ,grouping} = params; // const keywords = keyword ? keyword.split(' ').filter(Boolean) : []; const start = dayjs(startDate).format('YYYY-MM-DD'); const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD'); - let sql = ` + let sql = ''; + if (!grouping || grouping === 'day') { + sql = ` WITH first_order AS ( SELECT customer_email, MIN(date_paid) AS first_purchase_date FROM \`order\` @@ -214,6 +216,393 @@ export class StatisticsService { dt.can_total_orders ORDER BY d.order_date DESC; `; + }else if (grouping === 'week') { + sql = `WITH first_order AS ( + SELECT customer_email, MIN(date_paid) AS first_purchase_date + FROM \`order\` + GROUP BY customer_email + ), + weekly_orders AS ( + SELECT + o.id AS order_id, + DATE_FORMAT(o.date_paid, '%Y-%u') AS order_date, + o.customer_email, + o.total, + o.source_type, + o.utm_source, + o.siteId, + CASE + WHEN o.date_paid = f.first_purchase_date THEN 'first_purchase' + ELSE 'repeat_purchase' + END AS purchase_type, + CASE + WHEN o.source_type = 'utm' AND o.utm_source = 'google' THEN 'cpc' + ELSE 'non_cpc' + END AS order_type, + MAX(CASE WHEN oi.name LIKE '%zyn%' THEN 'zyn' ELSE 'non_zyn' END) AS zyn_type, + MAX(CASE WHEN oi.name LIKE '%yoone%' THEN 'yoone' ELSE 'non_yoone' END) AS yoone_type, + MAX(CASE WHEN oi.name LIKE '%zex%' THEN 'zex' ELSE 'non_zex' END) AS zex_type + FROM \`order\` o + LEFT JOIN first_order f ON o.customer_email = f.customer_email + LEFT JOIN order_item oi ON o.id = oi.orderId + WHERE o.date_paid IS NOT NULL + AND o.date_paid >= '${start}' AND o.date_paid < '${end}' + AND o.status IN ('processing','completed') + GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source + ), + order_sales_summary AS ( + SELECT + orderId, + SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity, + SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity, + SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity + FROM order_sale + GROUP BY orderId + ), + order_items_summary AS ( + SELECT + orderId, + SUM(CASE WHEN name LIKE '%zyn%' THEN total + total_tax ELSE 0 END) AS zyn_amount, + SUM(CASE WHEN name LIKE '%yoone%' THEN total + total_tax ELSE 0 END) AS yoone_amount, + SUM(CASE WHEN name LIKE '%zex%' THEN total + total_tax ELSE 0 END) AS zex_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%package%' THEN total + total_tax ELSE 0 END) AS yoone_G_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name NOT LIKE '%package%' THEN total + total_tax ELSE 0 END) AS yoone_S_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN total + total_tax ELSE 0 END) AS yoone_3_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN total + total_tax ELSE 0 END) AS yoone_6_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN total + total_tax ELSE 0 END) AS yoone_9_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN total + total_tax ELSE 0 END) AS yoone_12_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN total + total_tax ELSE 0 END) AS yoone_15_amount + FROM order_item + GROUP BY orderId + ), + weekly_totals AS ( + SELECT order_date, SUM(total) AS total_amount, + SUM(CASE WHEN siteId = 1 THEN total ELSE 0 END) AS togo_total_amount, + SUM(CASE WHEN siteId = 2 THEN total ELSE 0 END) AS can_total_amount, + COUNT(DISTINCT order_id) AS total_orders, + COUNT(DISTINCT CASE WHEN siteId = 1 THEN order_id END) AS togo_total_orders, + COUNT(DISTINCT CASE WHEN siteId = 2 THEN order_id END) AS can_total_orders, + SUM(CASE WHEN purchase_type = 'first_purchase' THEN total ELSE 0 END) AS first_purchase_total, + SUM(CASE WHEN purchase_type = 'repeat_purchase' THEN total ELSE 0 END) AS repeat_purchase_total, + SUM(CASE WHEN order_type = 'cpc' THEN total ELSE 0 END) AS cpc_total, + SUM(CASE WHEN order_type = 'non_cpc' THEN total ELSE 0 END) AS non_cpc_total, + SUM(CASE WHEN zyn_type = 'zyn' AND order_type = 'cpc' THEN total ELSE 0 END) AS zyn_total, + SUM(CASE WHEN zyn_type = 'zyn' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_zyn_total, + SUM(CASE WHEN yoone_type = 'yoone' AND order_type = 'cpc' THEN total ELSE 0 END) AS yoone_total, + SUM(CASE WHEN yoone_type = 'yoone' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_yoone_total, + SUM(CASE WHEN zex_type = 'zex' AND order_type = 'cpc' THEN total ELSE 0 END) AS zex_total, + SUM(CASE WHEN zex_type = 'zex' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_zex_total, + SUM(CASE WHEN source_type = 'typein' THEN total ELSE 0 END) AS direct_total, + SUM(CASE WHEN source_type = 'organic' THEN total ELSE 0 END) AS organic_total + FROM weekly_orders + GROUP BY order_date + ) + SELECT + wt.order_date, + COUNT(DISTINCT CASE WHEN wo.purchase_type = 'first_purchase' THEN wo.order_id END) AS first_purchase_orders, + COUNT(DISTINCT CASE WHEN wo.purchase_type = 'repeat_purchase' THEN wo.order_id END) AS repeat_purchase_orders, + COUNT(DISTINCT CASE WHEN wo.order_type = 'cpc' THEN wo.order_id END) AS cpc_orders, + COUNT(DISTINCT CASE WHEN wo.order_type = 'cpc' AND wo.siteId = 1 THEN wo.order_id END) AS togo_cpc_orders, + COUNT(DISTINCT CASE WHEN wo.order_type = 'cpc' AND wo.siteId = 2 THEN wo.order_id END) AS can_cpc_orders, + COUNT(DISTINCT CASE WHEN wo.order_type = 'non_cpc' THEN wo.order_id END) AS non_cpc_orders, + COUNT(DISTINCT CASE WHEN wo.order_type = 'non_cpc' AND wo.siteId = 1 THEN wo.order_id END) AS non_togo_cpc_orders, + COUNT(DISTINCT CASE WHEN wo.order_type = 'non_cpc' AND wo.siteId = 2 THEN wo.order_id END) AS non_can_cpc_orders, + COUNT(DISTINCT CASE WHEN wo.zyn_type = 'zyn' AND wo.order_type = 'cpc' THEN wo.order_id END) AS zyn_orders, + COUNT(DISTINCT CASE WHEN wo.zyn_type = 'zyn' AND wo.order_type = 'non_cpc' THEN wo.order_id END) AS non_zyn_orders, + COUNT(DISTINCT CASE WHEN wo.yoone_type = 'yoone' AND wo.order_type = 'cpc' THEN wo.order_id END) AS yoone_orders, + COUNT(DISTINCT CASE WHEN wo.yoone_type = 'yoone' AND wo.order_type = 'non_cpc' THEN wo.order_id END) AS non_yoone_orders, + COUNT(DISTINCT CASE WHEN wo.zex_type = 'zex' AND wo.order_type = 'cpc' THEN wo.order_id END) AS zex_orders, + COUNT(DISTINCT CASE WHEN wo.zex_type = 'zex' AND wo.order_type = 'non_cpc' THEN wo.order_id END) AS non_zex_orders, + COUNT(DISTINCT CASE WHEN wo.source_type = 'typein' THEN wo.order_id END) AS direct_orders, + COUNT(DISTINCT CASE WHEN wo.source_type = 'organic' THEN wo.order_id END) AS organic_orders, + wt.total_orders, + wt.togo_total_orders, + wt.can_total_orders, + wt.total_amount, + wt.togo_total_amount, + wt.can_total_amount, + wt.first_purchase_total, + wt.repeat_purchase_total, + wt.cpc_total, + wt.non_cpc_total, + wt.zyn_total, + wt.non_zyn_total, + wt.yoone_total, + wt.non_yoone_total, + wt.zex_total, + wt.non_zex_total, + wt.direct_total, + wt.organic_total, + COALESCE(SUM(os.zyn_quantity), 0) AS zyn_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.zyn_quantity ELSE 0 END) AS cpc_zyn_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.zyn_quantity ELSE 0 END) AS non_cpc_zyn_quantity, + COALESCE(SUM(os.yoone_quantity), 0) AS yoone_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_quantity ELSE 0 END) AS cpc_yoone_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_quantity ELSE 0 END) AS non_cpc_yoone_quantity, + COALESCE(SUM(os.yoone_G_quantity), 0) AS yoone_G_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_G_quantity ELSE 0 END) AS cpc_yoone_G_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_G_quantity ELSE 0 END) AS non_cpc_yoone_G_quantity, + COALESCE(SUM(os.yoone_S_quantity), 0) AS yoone_S_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_S_quantity ELSE 0 END) AS cpc_yoone_S_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_S_quantity ELSE 0 END) AS non_cpc_yoone_S_quantity, + COALESCE(SUM(os.yoone_3_quantity), 0) AS yoone_3_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_3_quantity ELSE 0 END) AS cpc_yoone_3_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_3_quantity ELSE 0 END) AS non_cpc_yoone_3_quantity, + COALESCE(SUM(os.yoone_6_quantity), 0) AS yoone_6_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_6_quantity ELSE 0 END) AS cpc_yoone_6_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_6_quantity ELSE 0 END) AS non_cpc_yoone_6_quantity, + COALESCE(SUM(os.yoone_9_quantity), 0) AS yoone_9_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_9_quantity ELSE 0 END) AS cpc_yoone_9_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_9_quantity ELSE 0 END) AS non_cpc_yoone_9_quantity, + COALESCE(SUM(os.yoone_12_quantity), 0) AS yoone_12_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_12_quantity ELSE 0 END) AS cpc_yoone_12_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_12_quantity ELSE 0 END) AS non_cpc_yoone_12_quantity, + COALESCE(SUM(os.yoone_15_quantity), 0) AS yoone_15_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_15_quantity ELSE 0 END) AS cpc_yoone_15_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_15_quantity ELSE 0 END) AS non_cpc_yoone_15_quantity, + COALESCE(SUM(os.zex_quantity), 0) AS zex_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.zex_quantity ELSE 0 END) AS cpc_zex_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.zex_quantity ELSE 0 END) AS non_cpc_zex_quantity, + COALESCE(SUM(oi.zyn_amount), 0) AS zyn_amount, + COALESCE(SUM(oi.yoone_amount), 0) AS yoone_amount, + COALESCE(SUM(oi.zex_amount), 0) AS zex_amount, + COALESCE(SUM(oi.yoone_G_amount), 0) AS yoone_G_amount, + COALESCE(SUM(oi.yoone_S_amount), 0) AS yoone_S_amount, + COALESCE(SUM(oi.yoone_3_amount), 0) AS yoone_3_amount, + COALESCE(SUM(oi.yoone_6_amount), 0) AS yoone_6_amount, + COALESCE(SUM(oi.yoone_9_amount), 0) AS yoone_9_amount, + COALESCE(SUM(oi.yoone_12_amount), 0) AS yoone_12_amount, + COALESCE(SUM(oi.yoone_15_amount), 0) AS yoone_15_amount, + ROUND(COALESCE(wt.total_amount / wt.total_orders, 0), 2) AS avg_total_amount, + ROUND(COALESCE(wt.togo_total_amount / wt.togo_total_orders, 0), 2) AS avg_togo_total_amount, + ROUND(COALESCE(wt.can_total_amount / wt.can_total_orders, 0), 2) AS avg_can_total_amount + FROM weekly_orders wo + LEFT JOIN weekly_totals wt ON wo.order_date = wt.order_date + LEFT JOIN order_sales_summary os ON wo.order_id = os.orderId + LEFT JOIN order_items_summary oi ON wo.order_id = oi.orderId + GROUP BY + wt.order_date, + wt.total_amount, + wt.togo_total_amount, + wt.can_total_amount, + wt.first_purchase_total, + wt.repeat_purchase_total, + wt.cpc_total, + wt.non_cpc_total, + wt.zyn_total, + wt.non_zyn_total, + wt.yoone_total, + wt.non_yoone_total, + wt.zex_total, + wt.non_zex_total, + wt.direct_total, + wt.organic_total, + wt.total_orders, + wt.togo_total_orders, + wt.can_total_orders + ORDER BY wt.order_date DESC; + `; + }else if (grouping === 'month') { + sql = `WITH first_order AS ( + SELECT customer_email, MIN(date_paid) AS first_purchase_date + FROM \`order\` + GROUP BY customer_email + ), + monthly_orders AS ( + SELECT + o.id AS order_id, + DATE_FORMAT(o.date_paid, '%Y-%m') AS order_date, + o.customer_email, + o.total, + o.source_type, + o.utm_source, + o.siteId, + CASE + WHEN o.date_paid = f.first_purchase_date THEN 'first_purchase' + ELSE 'repeat_purchase' + END AS purchase_type, + CASE + WHEN o.source_type = 'utm' AND o.utm_source = 'google' THEN 'cpc' + ELSE 'non_cpc' + END AS order_type, + MAX(CASE WHEN oi.name LIKE '%zyn%' THEN 'zyn' ELSE 'non_zyn' END) AS zyn_type, + MAX(CASE WHEN oi.name LIKE '%yoone%' THEN 'yoone' ELSE 'non_yoone' END) AS yoone_type, + MAX(CASE WHEN oi.name LIKE '%zex%' THEN 'zex' ELSE 'non_zex' END) AS zex_type + FROM \`order\` o + LEFT JOIN first_order f ON o.customer_email = f.customer_email + LEFT JOIN order_item oi ON o.id = oi.orderId + WHERE o.date_paid IS NOT NULL + AND o.date_paid >= '${start}' AND o.date_paid < '${end}' + AND o.status IN ('processing','completed') + GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source + ), + order_sales_summary AS ( + SELECT + orderId, + SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity, + SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity, + SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity + FROM order_sale + GROUP BY orderId + ), + order_items_summary AS ( + SELECT + orderId, + SUM(CASE WHEN name LIKE '%zyn%' THEN total + total_tax ELSE 0 END) AS zyn_amount, + SUM(CASE WHEN name LIKE '%yoone%' THEN total + total_tax ELSE 0 END) AS yoone_amount, + SUM(CASE WHEN name LIKE '%zex%' THEN total + total_tax ELSE 0 END) AS zex_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%package%' THEN total + total_tax ELSE 0 END) AS yoone_G_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name NOT LIKE '%package%' THEN total + total_tax ELSE 0 END) AS yoone_S_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN total + total_tax ELSE 0 END) AS yoone_3_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN total + total_tax ELSE 0 END) AS yoone_6_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN total + total_tax ELSE 0 END) AS yoone_9_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN total + total_tax ELSE 0 END) AS yoone_12_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN total + total_tax ELSE 0 END) AS yoone_15_amount + FROM order_item + GROUP BY orderId + ), + monthly_totals AS ( + SELECT order_date, SUM(total) AS total_amount, + SUM(CASE WHEN siteId = 1 THEN total ELSE 0 END) AS togo_total_amount, + SUM(CASE WHEN siteId = 2 THEN total ELSE 0 END) AS can_total_amount, + COUNT(DISTINCT order_id) AS total_orders, + COUNT(DISTINCT CASE WHEN siteId = 1 THEN order_id END) AS togo_total_orders, + COUNT(DISTINCT CASE WHEN siteId = 2 THEN order_id END) AS can_total_orders, + SUM(CASE WHEN purchase_type = 'first_purchase' THEN total ELSE 0 END) AS first_purchase_total, + SUM(CASE WHEN purchase_type = 'repeat_purchase' THEN total ELSE 0 END) AS repeat_purchase_total, + SUM(CASE WHEN order_type = 'cpc' THEN total ELSE 0 END) AS cpc_total, + SUM(CASE WHEN order_type = 'non_cpc' THEN total ELSE 0 END) AS non_cpc_total, + SUM(CASE WHEN zyn_type = 'zyn' AND order_type = 'cpc' THEN total ELSE 0 END) AS zyn_total, + SUM(CASE WHEN zyn_type = 'zyn' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_zyn_total, + SUM(CASE WHEN yoone_type = 'yoone' AND order_type = 'cpc' THEN total ELSE 0 END) AS yoone_total, + SUM(CASE WHEN yoone_type = 'yoone' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_yoone_total, + SUM(CASE WHEN zex_type = 'zex' AND order_type = 'cpc' THEN total ELSE 0 END) AS zex_total, + SUM(CASE WHEN zex_type = 'zex' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_zex_total, + SUM(CASE WHEN source_type = 'typein' THEN total ELSE 0 END) AS direct_total, + SUM(CASE WHEN source_type = 'organic' THEN total ELSE 0 END) AS organic_total + FROM monthly_orders + GROUP BY order_date + ) + SELECT + mt.order_date, + COUNT(DISTINCT CASE WHEN mo.purchase_type = 'first_purchase' THEN mo.order_id END) AS first_purchase_orders, + COUNT(DISTINCT CASE WHEN mo.purchase_type = 'repeat_purchase' THEN mo.order_id END) AS repeat_purchase_orders, + COUNT(DISTINCT CASE WHEN mo.order_type = 'cpc' THEN mo.order_id END) AS cpc_orders, + COUNT(DISTINCT CASE WHEN mo.order_type = 'cpc' AND mo.siteId = 1 THEN mo.order_id END) AS togo_cpc_orders, + COUNT(DISTINCT CASE WHEN mo.order_type = 'cpc' AND mo.siteId = 2 THEN mo.order_id END) AS can_cpc_orders, + COUNT(DISTINCT CASE WHEN mo.order_type = 'non_cpc' THEN mo.order_id END) AS non_cpc_orders, + COUNT(DISTINCT CASE WHEN mo.order_type = 'non_cpc' AND mo.siteId = 1 THEN mo.order_id END) AS non_togo_cpc_orders, + COUNT(DISTINCT CASE WHEN mo.order_type = 'non_cpc' AND mo.siteId = 2 THEN mo.order_id END) AS non_can_cpc_orders, + COUNT(DISTINCT CASE WHEN mo.zyn_type = 'zyn' AND mo.order_type = 'cpc' THEN mo.order_id END) AS zyn_orders, + COUNT(DISTINCT CASE WHEN mo.zyn_type = 'zyn' AND mo.order_type = 'non_cpc' THEN mo.order_id END) AS non_zyn_orders, + COUNT(DISTINCT CASE WHEN mo.yoone_type = 'yoone' AND mo.order_type = 'cpc' THEN mo.order_id END) AS yoone_orders, + COUNT(DISTINCT CASE WHEN mo.yoone_type = 'yoone' AND mo.order_type = 'non_cpc' THEN mo.order_id END) AS non_yoone_orders, + COUNT(DISTINCT CASE WHEN mo.zex_type = 'zex' AND mo.order_type = 'cpc' THEN mo.order_id END) AS zex_orders, + COUNT(DISTINCT CASE WHEN mo.zex_type = 'zex' AND mo.order_type = 'non_cpc' THEN mo.order_id END) AS non_zex_orders, + COUNT(DISTINCT CASE WHEN mo.source_type = 'typein' THEN mo.order_id END) AS direct_orders, + COUNT(DISTINCT CASE WHEN mo.source_type = 'organic' THEN mo.order_id END) AS organic_orders, + mt.total_orders, + mt.togo_total_orders, + mt.can_total_orders, + mt.total_amount, + mt.togo_total_amount, + mt.can_total_amount, + mt.first_purchase_total, + mt.repeat_purchase_total, + mt.cpc_total, + mt.non_cpc_total, + mt.zyn_total, + mt.non_zyn_total, + mt.yoone_total, + mt.non_yoone_total, + mt.zex_total, + mt.non_zex_total, + mt.direct_total, + mt.organic_total, + COALESCE(SUM(os.zyn_quantity), 0) AS zyn_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.zyn_quantity ELSE 0 END) AS cpc_zyn_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.zyn_quantity ELSE 0 END) AS non_cpc_zyn_quantity, + COALESCE(SUM(os.yoone_quantity), 0) AS yoone_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_quantity ELSE 0 END) AS cpc_yoone_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_quantity ELSE 0 END) AS non_cpc_yoone_quantity, + COALESCE(SUM(os.yoone_G_quantity), 0) AS yoone_G_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_G_quantity ELSE 0 END) AS cpc_yoone_G_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_G_quantity ELSE 0 END) AS non_cpc_yoone_G_quantity, + COALESCE(SUM(os.yoone_S_quantity), 0) AS yoone_S_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_S_quantity ELSE 0 END) AS cpc_yoone_S_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_S_quantity ELSE 0 END) AS non_cpc_yoone_S_quantity, + COALESCE(SUM(os.yoone_3_quantity), 0) AS yoone_3_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_3_quantity ELSE 0 END) AS cpc_yoone_3_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_3_quantity ELSE 0 END) AS non_cpc_yoone_3_quantity, + COALESCE(SUM(os.yoone_6_quantity), 0) AS yoone_6_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_6_quantity ELSE 0 END) AS cpc_yoone_6_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_6_quantity ELSE 0 END) AS non_cpc_yoone_6_quantity, + COALESCE(SUM(os.yoone_9_quantity), 0) AS yoone_9_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_9_quantity ELSE 0 END) AS cpc_yoone_9_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_9_quantity ELSE 0 END) AS non_cpc_yoone_9_quantity, + COALESCE(SUM(os.yoone_12_quantity), 0) AS yoone_12_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_12_quantity ELSE 0 END) AS cpc_yoone_12_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_12_quantity ELSE 0 END) AS non_cpc_yoone_12_quantity, + COALESCE(SUM(os.yoone_15_quantity), 0) AS yoone_15_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_15_quantity ELSE 0 END) AS cpc_yoone_15_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_15_quantity ELSE 0 END) AS non_cpc_yoone_15_quantity, + COALESCE(SUM(os.zex_quantity), 0) AS zex_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.zex_quantity ELSE 0 END) AS cpc_zex_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.zex_quantity ELSE 0 END) AS non_cpc_zex_quantity, + COALESCE(SUM(oi.zyn_amount), 0) AS zyn_amount, + COALESCE(SUM(oi.yoone_amount), 0) AS yoone_amount, + COALESCE(SUM(oi.zex_amount), 0) AS zex_amount, + COALESCE(SUM(oi.yoone_G_amount), 0) AS yoone_G_amount, + COALESCE(SUM(oi.yoone_S_amount), 0) AS yoone_S_amount, + COALESCE(SUM(oi.yoone_3_amount), 0) AS yoone_3_amount, + COALESCE(SUM(oi.yoone_6_amount), 0) AS yoone_6_amount, + COALESCE(SUM(oi.yoone_9_amount), 0) AS yoone_9_amount, + COALESCE(SUM(oi.yoone_12_amount), 0) AS yoone_12_amount, + COALESCE(SUM(oi.yoone_15_amount), 0) AS yoone_15_amount, + ROUND(COALESCE(mt.total_amount / mt.total_orders, 0), 2) AS avg_total_amount, + ROUND(COALESCE(mt.togo_total_amount / mt.togo_total_orders, 0), 2) AS avg_togo_total_amount, + ROUND(COALESCE(mt.can_total_amount / mt.can_total_orders, 0), 2) AS avg_can_total_amount + FROM monthly_orders mo + LEFT JOIN monthly_totals mt ON mo.order_date = mt.order_date + LEFT JOIN order_sales_summary os ON mo.order_id = os.orderId + LEFT JOIN order_items_summary oi ON mo.order_id = oi.orderId + GROUP BY + mt.order_date, + mt.total_amount, + mt.togo_total_amount, + mt.can_total_amount, + mt.first_purchase_total, + mt.repeat_purchase_total, + mt.cpc_total, + mt.non_cpc_total, + mt.zyn_total, + mt.non_zyn_total, + mt.yoone_total, + mt.non_yoone_total, + mt.zex_total, + mt.non_zex_total, + mt.direct_total, + mt.organic_total, + mt.total_orders, + mt.togo_total_orders, + mt.can_total_orders + ORDER BY mt.order_date DESC; + `;} + return this.orderRepository.query(sql); } // async getOrderStatistics(params: OrderStatisticsParams) { @@ -933,7 +1322,8 @@ export class StatisticsService { user_first_order AS ( SELECT customer_email, - DATE_FORMAT(MIN(date_paid), '%Y-%m') AS first_order_month + DATE_FORMAT(MIN(date_paid), '%Y-%m') AS first_order_month, + SUM(total) AS first_order_total FROM \`order\` WHERE status IN ('processing', 'completed') GROUP BY customer_email @@ -946,7 +1336,7 @@ export class StatisticsService { WHERE status IN ('processing', 'completed') ), filtered_orders AS ( - SELECT o.customer_email, o.order_month, u.first_order_month, c.start_month + SELECT o.customer_email, o.order_month, u.first_order_month,u.first_order_total, c.start_month FROM order_months o JOIN user_first_order u ON o.customer_email = u.customer_email JOIN cutoff_months c ON 1=1 @@ -958,14 +1348,16 @@ export class StatisticsService { CASE WHEN first_order_month < start_month THEN CONCAT('>', start_month) ELSE first_order_month - END AS first_order_month_group + END AS first_order_month_group, + first_order_total FROM filtered_orders ), final_counts AS ( SELECT order_month, first_order_month_group, - COUNT(*) AS order_count + COUNT(*) AS order_count, + SUM(first_order_total) AS total FROM classified GROUP BY order_month, first_order_month_group ) @@ -985,7 +1377,8 @@ export class StatisticsService { SELECT customer_email, DATE_FORMAT(date_paid, '%Y-%m') AS order_month, - date_paid + date_paid, + total FROM \`order\` WHERE status IN ('processing', 'completed') ), @@ -998,7 +1391,8 @@ export class StatisticsService { monthly_users AS ( SELECT DISTINCT customer_email, - order_month + order_month, + total FROM filtered_users ), @@ -1016,6 +1410,7 @@ export class StatisticsService { SELECT m.customer_email, m.order_month, + m.total, CASE WHEN f.first_order_month = m.order_month THEN 'new' ELSE 'returning' @@ -1029,7 +1424,9 @@ export class StatisticsService { SELECT order_month, SUM(CASE WHEN customer_type = 'new' THEN 1 ELSE 0 END) AS new_user_count, - SUM(CASE WHEN customer_type = 'returning' THEN 1 ELSE 0 END) AS old_user_count + SUM(CASE WHEN customer_type = 'returning' THEN 1 ELSE 0 END) AS old_user_count, + SUM(CASE WHEN customer_type = 'new' THEN total ELSE 0 END) AS new_user_total, + SUM(CASE WHEN customer_type = 'returning' THEN total ELSE 0 END) AS old_user_total FROM labeled_users GROUP BY order_month ), @@ -1061,7 +1458,9 @@ export class StatisticsService { m.order_month, m.new_user_count, m.old_user_count, - COALESCE(i.inactive_user_count, 0) AS inactive_user_count + COALESCE(i.inactive_user_count, 0) AS inactive_user_count, + m.new_user_total, + m.old_user_total FROM monthly_new_old_counts m LEFT JOIN users_without_future_orders i ON m.order_month = i.order_month