From b29677a1921e9c0e29e8f5b66ae16a4b3a9ebc82 Mon Sep 17 00:00:00 2001 From: Aditya Siregar Date: Mon, 22 Sep 2025 22:12:54 +0700 Subject: [PATCH] Fix Product Analysitcs --- internal/contract/analytics_contract.go | 2 +- internal/processor/analytics_processor.go | 2 +- internal/repository/analytics_repository.go | 50 +++++++++++++-------- internal/service/analytics_service.go | 4 +- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/internal/contract/analytics_contract.go b/internal/contract/analytics_contract.go index 817ed55..768b3ea 100644 --- a/internal/contract/analytics_contract.go +++ b/internal/contract/analytics_contract.go @@ -89,7 +89,7 @@ type ProductAnalyticsRequest struct { OutletID *uuid.UUID `form:"outlet_id,omitempty"` DateFrom string `form:"date_from" validate:"required"` DateTo string `form:"date_to" validate:"required"` - Limit int `form:"limit,default=10" validate:"min=1,max=100"` + Limit int `form:"limit,default=1000" validate:"min=1,max=1000"` } // ProductAnalyticsResponse represents the response for product analytics diff --git a/internal/processor/analytics_processor.go b/internal/processor/analytics_processor.go index bf121c0..7efa2c7 100644 --- a/internal/processor/analytics_processor.go +++ b/internal/processor/analytics_processor.go @@ -172,7 +172,7 @@ func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *m // Set default limit if req.Limit <= 0 { - req.Limit = 10 + req.Limit = 1000 } // Get analytics data from repository diff --git a/internal/repository/analytics_repository.go b/internal/repository/analytics_repository.go index b69b39e..e1980ae 100644 --- a/internal/repository/analytics_repository.go +++ b/internal/repository/analytics_repository.go @@ -45,8 +45,10 @@ func (r *AnalyticsRepositoryImpl) GetPaymentMethodAnalytics(ctx context.Context, Joins("JOIN payment_methods pm ON p.payment_method_id = pm.id"). Joins("JOIN orders o ON p.order_id = o.id"). Where("o.organization_id = ?", organizationID). + Where("o.is_void = ?", false). + Where("o.is_refund = ?", false). Where("p.status = ?", entities.PaymentTransactionStatusCompleted). - Where("p.created_at >= ? AND p.created_at <= ?", dateFrom, dateTo) + Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo) if outletID != nil { query = query.Where("o.outlet_id = ?", *outletID) @@ -81,7 +83,7 @@ func (r *AnalyticsRepositoryImpl) GetSalesAnalytics(ctx context.Context, organiz `+dateFormat+` as date, COALESCE(SUM(o.total_amount), 0) as sales, COUNT(o.id) as orders, - COALESCE(SUM(oi.quantity), 0) as items, + COALESCE(SUM(CASE WHEN oi.status != 'cancelled' AND oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as items, COALESCE(SUM(o.tax_amount), 0) as tax, COALESCE(SUM(o.discount_amount), 0) as discount, COALESCE(SUM(o.total_amount - o.tax_amount - o.discount_amount), 0) as net_sales @@ -89,6 +91,8 @@ func (r *AnalyticsRepositoryImpl) GetSalesAnalytics(ctx context.Context, organiz Joins("LEFT JOIN order_items oi ON o.id = oi.order_id"). Where("o.organization_id = ?", organizationID). Where("o.is_void = ?", false). + Where("o.is_refund = ?", false). + Where("o.payment_status = ?", entities.PaymentStatusCompleted). Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo) if outletID != nil { @@ -113,10 +117,11 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ p.name as product_name, c.id as category_id, c.name as category_name, - COALESCE(SUM(oi.quantity), 0) as quantity_sold, - COALESCE(SUM(oi.total_price), 0) as revenue, + COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as quantity_sold, + COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) as revenue, CASE - WHEN SUM(oi.quantity) > 0 THEN COALESCE(SUM(oi.total_price), 0) / SUM(oi.quantity) + WHEN SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END) > 0 + THEN COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) / SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END) ELSE 0 END as average_price, COUNT(DISTINCT oi.order_id) as order_count @@ -126,6 +131,9 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ Joins("JOIN orders o ON oi.order_id = o.id"). Where("o.organization_id = ?", organizationID). Where("o.is_void = ?", false). + Where("o.is_refund = ?", false). + Where("o.payment_status = ?", entities.PaymentStatusCompleted). + Where("oi.status != ?", entities.OrderItemStatusCancelled). Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo) if outletID != nil { @@ -134,7 +142,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ err := query. Group("p.id, p.name, c.id, c.name"). - Order("revenue DESC"). + Order("c.name ASC, revenue DESC"). Limit(limit). Scan(&results).Error @@ -149,8 +157,8 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerCategory(ctx context.Con Select(` c.id as category_id, c.name as category_name, - COALESCE(SUM(oi.total_price), 0) as total_revenue, - COALESCE(SUM(oi.quantity), 0) as total_quantity, + COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) as total_revenue, + COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as total_quantity, COUNT(DISTINCT p.id) as product_count, COUNT(DISTINCT oi.order_id) as order_count `). @@ -159,6 +167,9 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerCategory(ctx context.Con Joins("JOIN orders o ON oi.order_id = o.id"). Where("o.organization_id = ?", organizationID). Where("o.is_void = ?", false). + Where("o.is_refund = ?", false). + Where("o.payment_status = ?", entities.PaymentStatusCompleted). + Where("oi.status != ?", entities.OrderItemStatusCancelled). Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo) if outletID != nil { @@ -319,18 +330,18 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or p.name as product_name, c.id as category_id, c.name as category_name, - SUM(oi.quantity) as quantity_sold, - SUM(oi.total_price) as revenue, - SUM(oi.total_cost) as cost, - SUM(oi.total_price - oi.total_cost) as gross_profit, + SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END) as quantity_sold, + SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END) as revenue, + SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END) as cost, + SUM(CASE WHEN oi.is_fully_refunded = false THEN (oi.total_price - COALESCE(oi.refund_amount, 0)) - (oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0))) ELSE 0 END) as gross_profit, CASE - WHEN SUM(oi.total_price) > 0 - THEN (SUM(oi.total_price - oi.total_cost) / SUM(oi.total_price)) * 100 + WHEN SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END) > 0 + THEN (SUM(CASE WHEN oi.is_fully_refunded = false THEN (oi.total_price - COALESCE(oi.refund_amount, 0)) - (oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0))) ELSE 0 END) / SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END)) * 100 ELSE 0 END as gross_profit_margin, - AVG(oi.unit_price) as average_price, - AVG(oi.unit_cost) as average_cost, - AVG(oi.unit_price - oi.unit_cost) as profit_per_unit + AVG(CASE WHEN oi.is_fully_refunded = false THEN oi.unit_price ELSE NULL END) as average_price, + AVG(CASE WHEN oi.is_fully_refunded = false THEN oi.unit_cost ELSE NULL END) as average_cost, + AVG(CASE WHEN oi.is_fully_refunded = false THEN oi.unit_price - oi.unit_cost ELSE NULL END) as profit_per_unit `). Joins("JOIN orders o ON oi.order_id = o.id"). Joins("JOIN products p ON oi.product_id = p.id"). @@ -339,10 +350,11 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or Where("o.status = ?", entities.OrderStatusCompleted). Where("o.payment_status = ?", entities.PaymentStatusCompleted). Where("o.is_void = false AND o.is_refund = false"). + Where("oi.status != ?", entities.OrderItemStatusCancelled). Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo). Group("p.id, p.name, c.id, c.name"). - Order("gross_profit DESC"). - Limit(20) + Order("c.name ASC, gross_profit DESC"). + Limit(1000) if outletID != nil { productQuery = productQuery.Where("o.outlet_id = ?", *outletID) diff --git a/internal/service/analytics_service.go b/internal/service/analytics_service.go index 9be78cd..6f0d85a 100644 --- a/internal/service/analytics_service.go +++ b/internal/service/analytics_service.go @@ -185,8 +185,8 @@ func (s *AnalyticsServiceImpl) validateProductAnalyticsRequest(req *models.Produ return fmt.Errorf("date_from cannot be after date_to") } - if req.Limit < 1 || req.Limit > 100 { - return fmt.Errorf("limit must be between 1 and 100") + if req.Limit < 1 || req.Limit > 1000 { + return fmt.Errorf("limit must be between 1 and 1000") } return nil