347 lines
11 KiB
Go
347 lines
11 KiB
Go
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)
|
|
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 = 10
|
|
}
|
|
|
|
// 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,
|
|
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) 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
|
|
}
|