package processor import ( "context" "fmt" "time" "apskel-pos-be/internal/models" "apskel-pos-be/internal/repository" ) type AnalyticsProcessor interface { GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error) GetSalesAnalytics(ctx context.Context, req *models.SalesAnalyticsRequest) (*models.SalesAnalyticsResponse, error) GetProductAnalytics(ctx context.Context, req *models.ProductAnalyticsRequest) (*models.ProductAnalyticsResponse, error) GetProductAnalyticsPerCategory(ctx context.Context, req *models.ProductAnalyticsPerCategoryRequest) (*models.ProductAnalyticsPerCategoryResponse, error) GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error) GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error) } type AnalyticsProcessorImpl struct { analyticsRepo repository.AnalyticsRepository } func NewAnalyticsProcessorImpl(analyticsRepo repository.AnalyticsRepository) *AnalyticsProcessorImpl { return &AnalyticsProcessorImpl{ analyticsRepo: analyticsRepo, } } func (p *AnalyticsProcessorImpl) GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error) { if req.DateFrom.After(req.DateTo) { return nil, fmt.Errorf("date_from cannot be after date_to") } analyticsData, err := p.analyticsRepo.GetPaymentMethodAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo) if err != nil { return nil, fmt.Errorf("failed to get payment method analytics: %w", err) } var totalAmount float64 var totalOrders int64 var totalPayments int64 for _, data := range analyticsData { totalAmount += data.TotalAmount totalOrders += data.OrderCount totalPayments += data.PaymentCount } var averageOrderValue float64 if totalOrders > 0 { averageOrderValue = totalAmount / float64(totalOrders) } // Calculate percentages var resultData []models.PaymentMethodAnalyticsData for _, data := range analyticsData { var percentage float64 if totalAmount > 0 { percentage = (data.TotalAmount / totalAmount) * 100 } resultData = append(resultData, models.PaymentMethodAnalyticsData{ PaymentMethodID: data.PaymentMethodID, PaymentMethodName: data.PaymentMethodName, PaymentMethodType: data.PaymentMethodType, TotalAmount: data.TotalAmount, OrderCount: data.OrderCount, PaymentCount: data.PaymentCount, Percentage: percentage, }) } summary := models.PaymentMethodSummary{ TotalAmount: totalAmount, TotalOrders: totalOrders, TotalPayments: totalPayments, AverageOrderValue: averageOrderValue, } return &models.PaymentMethodAnalyticsResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, DateFrom: req.DateFrom, DateTo: req.DateTo, GroupBy: req.GroupBy, Summary: summary, Data: resultData, }, nil } func (p *AnalyticsProcessorImpl) GetSalesAnalytics(ctx context.Context, req *models.SalesAnalyticsRequest) (*models.SalesAnalyticsResponse, error) { // Validate date range if req.DateFrom.After(req.DateTo) { return nil, fmt.Errorf("date_from cannot be after date_to") } // Validate groupBy if req.GroupBy == "" { req.GroupBy = "day" } // Get analytics data from repository analyticsData, err := p.analyticsRepo.GetSalesAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy) if err != nil { return nil, fmt.Errorf("failed to get sales analytics: %w", err) } // Calculate summary var totalSales float64 var totalOrders int64 var totalItems int64 var totalTax float64 var totalDiscount float64 var netSales float64 for _, data := range analyticsData { totalSales += data.Sales totalOrders += data.Orders totalItems += data.Items totalTax += data.Tax totalDiscount += data.Discount netSales += data.NetSales } var averageOrderValue float64 if totalOrders > 0 { averageOrderValue = totalSales / float64(totalOrders) } // Transform data var resultData []models.SalesAnalyticsData for _, data := range analyticsData { resultData = append(resultData, models.SalesAnalyticsData{ Date: data.Date, Sales: data.Sales, Orders: data.Orders, Items: data.Items, Tax: data.Tax, Discount: data.Discount, NetSales: data.NetSales, }) } summary := models.SalesSummary{ TotalSales: totalSales, TotalOrders: totalOrders, TotalItems: totalItems, AverageOrderValue: averageOrderValue, TotalTax: totalTax, TotalDiscount: totalDiscount, NetSales: netSales, } return &models.SalesAnalyticsResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, DateFrom: req.DateFrom, DateTo: req.DateTo, GroupBy: req.GroupBy, Summary: summary, Data: resultData, }, nil } func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *models.ProductAnalyticsRequest) (*models.ProductAnalyticsResponse, error) { // Validate date range if req.DateFrom.After(req.DateTo) { return nil, fmt.Errorf("date_from cannot be after date_to") } // Set default limit if req.Limit <= 0 { req.Limit = 1000 } // Get analytics data from repository analyticsData, err := p.analyticsRepo.GetProductAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.Limit) if err != nil { return nil, fmt.Errorf("failed to get product analytics: %w", err) } // Transform data var resultData []models.ProductAnalyticsData for _, data := range analyticsData { resultData = append(resultData, models.ProductAnalyticsData{ ProductID: data.ProductID, ProductName: data.ProductName, CategoryID: data.CategoryID, CategoryName: data.CategoryName, CategoryOrder: data.CategoryOrder, QuantitySold: data.QuantitySold, Revenue: data.Revenue, AveragePrice: data.AveragePrice, OrderCount: data.OrderCount, }) } return &models.ProductAnalyticsResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, DateFrom: req.DateFrom, DateTo: req.DateTo, Data: resultData, }, nil } func (p *AnalyticsProcessorImpl) GetProductAnalyticsPerCategory(ctx context.Context, req *models.ProductAnalyticsPerCategoryRequest) (*models.ProductAnalyticsPerCategoryResponse, error) { // Validate date range if req.DateFrom.After(req.DateTo) { return nil, fmt.Errorf("date_from cannot be after date_to") } // Get analytics data from repository analyticsData, err := p.analyticsRepo.GetProductAnalyticsPerCategory(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo) if err != nil { return nil, fmt.Errorf("failed to get product analytics per category: %w", err) } // Transform data var resultData []models.ProductAnalyticsPerCategoryData for _, data := range analyticsData { resultData = append(resultData, models.ProductAnalyticsPerCategoryData{ CategoryID: data.CategoryID, CategoryName: data.CategoryName, TotalRevenue: data.TotalRevenue, TotalQuantity: data.TotalQuantity, ProductCount: data.ProductCount, OrderCount: data.OrderCount, }) } return &models.ProductAnalyticsPerCategoryResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, DateFrom: req.DateFrom, DateTo: req.DateTo, Data: resultData, }, nil } func (p *AnalyticsProcessorImpl) GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error) { // Validate date range if req.DateFrom.After(req.DateTo) { return nil, fmt.Errorf("date_from cannot be after date_to") } // Get dashboard overview overview, err := p.analyticsRepo.GetDashboardOverview(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo) if err != nil { return nil, fmt.Errorf("failed to get dashboard overview: %w", err) } // Get top products (limit to 5 for dashboard) productReq := &models.ProductAnalyticsRequest{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, DateFrom: req.DateFrom, DateTo: req.DateTo, Limit: 5, } topProducts, err := p.GetProductAnalytics(ctx, productReq) if err != nil { return nil, fmt.Errorf("failed to get top products: %w", err) } // Get payment methods paymentReq := &models.PaymentMethodAnalyticsRequest{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, DateFrom: req.DateFrom, DateTo: req.DateTo, GroupBy: "day", } paymentMethods, err := p.GetPaymentMethodAnalytics(ctx, paymentReq) if err != nil { return nil, fmt.Errorf("failed to get payment methods: %w", err) } // Get recent sales (last 7 days) recentDateFrom := time.Now().AddDate(0, 0, -7) salesReq := &models.SalesAnalyticsRequest{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, DateFrom: recentDateFrom, DateTo: req.DateTo, GroupBy: "day", } recentSales, err := p.GetSalesAnalytics(ctx, salesReq) if err != nil { return nil, fmt.Errorf("failed to get recent sales: %w", err) } return &models.DashboardAnalyticsResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, DateFrom: req.DateFrom, DateTo: req.DateTo, Overview: models.DashboardOverview{ TotalSales: overview.TotalSales, TotalOrders: overview.TotalOrders, AverageOrderValue: overview.AverageOrderValue, TotalCustomers: overview.TotalCustomers, VoidedOrders: overview.VoidedOrders, RefundedOrders: overview.RefundedOrders, }, TopProducts: topProducts.Data, PaymentMethods: paymentMethods.Data, RecentSales: recentSales.Data, }, nil } func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error) { if req.DateFrom.After(req.DateTo) { return nil, fmt.Errorf("date_from cannot be after date_to") } // Get analytics data from repository result, err := p.analyticsRepo.GetProfitLossAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy) if err != nil { return nil, fmt.Errorf("failed to get profit/loss analytics: %w", err) } // Transform entities to models data := make([]models.ProfitLossData, len(result.Data)) for i, item := range result.Data { data[i] = models.ProfitLossData{ Date: item.Date, Revenue: item.Revenue, Cost: item.Cost, GrossProfit: item.GrossProfit, GrossProfitMargin: item.GrossProfitMargin, Tax: item.Tax, Discount: item.Discount, NetProfit: item.NetProfit, NetProfitMargin: item.NetProfitMargin, Orders: item.Orders, } } productData := make([]models.ProductProfitData, len(result.ProductData)) for i, item := range result.ProductData { productData[i] = models.ProductProfitData{ ProductID: item.ProductID, ProductName: item.ProductName, CategoryID: item.CategoryID, CategoryName: item.CategoryName, QuantitySold: item.QuantitySold, Revenue: item.Revenue, Cost: item.Cost, GrossProfit: item.GrossProfit, GrossProfitMargin: item.GrossProfitMargin, AveragePrice: item.AveragePrice, AverageCost: item.AverageCost, ProfitPerUnit: item.ProfitPerUnit, } } return &models.ProfitLossAnalyticsResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, DateFrom: req.DateFrom, DateTo: req.DateTo, GroupBy: req.GroupBy, Summary: models.ProfitLossSummary{ TotalRevenue: result.Summary.TotalRevenue, TotalCost: result.Summary.TotalCost, GrossProfit: result.Summary.GrossProfit, GrossProfitMargin: result.Summary.GrossProfitMargin, TotalTax: result.Summary.TotalTax, TotalDiscount: result.Summary.TotalDiscount, NetProfit: result.Summary.NetProfit, NetProfitMargin: result.Summary.NetProfitMargin, TotalOrders: result.Summary.TotalOrders, AverageProfit: result.Summary.AverageProfit, ProfitabilityRatio: result.Summary.ProfitabilityRatio, }, Data: data, ProductData: productData, }, nil }