apskel-pos-backend/internal/processor/analytics_processor.go

383 lines
12 KiB
Go
Raw Normal View History

2025-07-18 20:10:29 +07:00
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)
2025-08-14 00:45:14 +07:00
GetProductAnalyticsPerCategory(ctx context.Context, req *models.ProductAnalyticsPerCategoryRequest) (*models.ProductAnalyticsPerCategoryResponse, error)
2025-07-18 20:10:29 +07:00
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 {
2025-09-22 22:12:54 +07:00
req.Limit = 1000
2025-07-18 20:10:29 +07:00
}
// 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,
2025-10-07 00:02:15 +07:00
CategoryOrder: data.CategoryOrder,
2025-07-18 20:10:29 +07:00
QuantitySold: data.QuantitySold,
Revenue: data.Revenue,
AveragePrice: data.AveragePrice,
OrderCount: data.OrderCount,
})
}
return &models.ProductAnalyticsResponse{
OrganizationID: req.OrganizationID,
2025-08-14 00:45:14 +07:00
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,
2025-07-18 20:10:29 +07:00
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
}