From ba970229a9a7d30b5bd1bfd6bcbcf01dca907455 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 24 Apr 2026 17:56:38 +0700 Subject: [PATCH] Fix HPP --- internal/app/app.go | 8 - internal/contract/analytics_contract.go | 53 ++-- internal/contract/hpp_contract.go | 94 -------- internal/entities/analytics.go | 43 ++-- internal/handler/hpp_handler.go | 73 ------ internal/models/analytics.go | 53 ++-- internal/models/hpp.go | 94 -------- internal/processor/analytics_processor.go | 41 ++-- internal/processor/hpp_processor.go | 179 -------------- internal/repository/analytics_repository.go | 59 ++++- internal/repository/hpp_repository.go | 227 ------------------ internal/router/router.go | 70 +----- internal/service/hpp_service.go | 78 ------ internal/transformer/analytics_transformer.go | 63 +++-- internal/transformer/hpp_transformer.go | 136 ----------- 15 files changed, 206 insertions(+), 1065 deletions(-) delete mode 100644 internal/contract/hpp_contract.go delete mode 100644 internal/handler/hpp_handler.go delete mode 100644 internal/models/hpp.go delete mode 100644 internal/processor/hpp_processor.go delete mode 100644 internal/repository/hpp_repository.go delete mode 100644 internal/service/hpp_service.go delete mode 100644 internal/transformer/hpp_transformer.go diff --git a/internal/app/app.go b/internal/app/app.go index 75a4750..28e93e6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -104,7 +104,6 @@ func (a *App) Initialize(cfg *config.Config) error { validators.customerAuthValidator, services.customerPointsService, services.spinGameService, - services.hppService, middleware.customerAuthMiddleware, ) @@ -191,7 +190,6 @@ type repositories struct { customerAuthRepo repository.CustomerAuthRepository customerPointsRepo repository.CustomerPointsRepository otpRepo repository.OtpRepository - hppRepo *repository.HPPRepositoryImpl txManager *repository.TxManager } @@ -237,7 +235,6 @@ func (a *App) initRepositories() *repositories { customerAuthRepo: repository.NewCustomerAuthRepository(a.db), customerPointsRepo: repository.NewCustomerPointsRepository(a.db), otpRepo: repository.NewOtpRepository(a.db), - hppRepo: repository.NewHPPRepositoryImpl(a.db), txManager: repository.NewTxManager(a.db), } } @@ -279,7 +276,6 @@ type processors struct { customerAuthProcessor processor.CustomerAuthProcessor customerPointsProcessor *processor.CustomerPointsProcessor otpProcessor processor.OtpProcessor - hppProcessor *processor.HPPProcessorImpl fileClient processor.FileClient inventoryMovementService service.InventoryMovementService } @@ -327,7 +323,6 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor customerAuthProcessor: processor.NewCustomerAuthProcessor(repos.customerAuthRepo, otpProcessor, repos.otpRepo, cfg.GetCustomerJWTSecret(), cfg.GetCustomerJWTExpiresTTL()), customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo, repos.gameRepo), otpProcessor: otpProcessor, - hppProcessor: processor.NewHPPProcessorImpl(repos.hppRepo), fileClient: fileClient, inventoryMovementService: inventoryMovementService, } @@ -366,7 +361,6 @@ type services struct { customerAuthService service.CustomerAuthService customerPointsService service.CustomerPointsService spinGameService service.SpinGameService - hppService *service.HPPServiceImpl } func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services { @@ -402,7 +396,6 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con customerAuthService := service.NewCustomerAuthService(processors.customerAuthProcessor) customerPointsService := service.NewCustomerPointsService(processors.customerPointsProcessor) spinGameService := service.NewSpinGameService(processors.gamePlayProcessor, repos.txManager) - hppService := service.NewHPPServiceImpl(processors.hppProcessor) // Update order service with order ingredient transaction service orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager) @@ -440,7 +433,6 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con customerAuthService: customerAuthService, customerPointsService: customerPointsService, spinGameService: spinGameService, - hppService: hppService, } } diff --git a/internal/contract/analytics_contract.go b/internal/contract/analytics_contract.go index c97edd4..8e32a64 100644 --- a/internal/contract/analytics_contract.go +++ b/internal/contract/analytics_contract.go @@ -101,18 +101,23 @@ type ProductAnalyticsResponse struct { Data []ProductAnalyticsData `json:"data"` } -// ProductAnalyticsData represents individual product analytics data type ProductAnalyticsData struct { - ProductID uuid.UUID `json:"product_id"` - ProductName string `json:"product_name"` - ProductSku string `json:"product_sku"` - CategoryID uuid.UUID `json:"category_id"` - CategoryName string `json:"category_name"` - CategoryOrder int `json:"category_order"` - QuantitySold int64 `json:"quantity_sold"` - Revenue float64 `json:"revenue"` - AveragePrice float64 `json:"average_price"` - OrderCount int64 `json:"order_count"` + ProductID uuid.UUID `json:"product_id"` + ProductName string `json:"product_name"` + ProductSku string `json:"product_sku"` + CategoryID uuid.UUID `json:"category_id"` + CategoryName string `json:"category_name"` + CategoryOrder int `json:"category_order"` + QuantitySold int64 `json:"quantity_sold"` + Revenue float64 `json:"revenue"` + AveragePrice float64 `json:"average_price"` + OrderCount int64 `json:"order_count"` + StandardHppPerUnit float64 `json:"standard_hpp_per_unit"` + StandardHppTotal float64 `json:"standard_hpp_total"` + FifoHppPerUnit float64 `json:"fifo_hpp_per_unit"` + FifoHppTotal float64 `json:"fifo_hpp_total"` + MovingAverageHppPerUnit float64 `json:"moving_average_hpp_per_unit"` + MovingAverageHppTotal float64 `json:"moving_average_hpp_total"` } // ProductAnalyticsPerCategoryRequest represents the request for product analytics per category @@ -125,21 +130,23 @@ type ProductAnalyticsPerCategoryRequest struct { // ProductAnalyticsPerCategoryResponse represents the response for product analytics per category type ProductAnalyticsPerCategoryResponse struct { - OrganizationID uuid.UUID `json:"organization_id"` - OutletID *uuid.UUID `json:"outlet_id,omitempty"` - DateFrom time.Time `json:"date_from"` - DateTo time.Time `json:"date_to"` - Data []ProductAnalyticsPerCategoryData `json:"data"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` + DateFrom time.Time `json:"date_from"` + DateTo time.Time `json:"date_to"` + Data []ProductAnalyticsPerCategoryData `json:"data"` } -// ProductAnalyticsPerCategoryData represents individual category analytics data type ProductAnalyticsPerCategoryData struct { - CategoryID uuid.UUID `json:"category_id"` - CategoryName string `json:"category_name"` - TotalRevenue float64 `json:"total_revenue"` - TotalQuantity int64 `json:"total_quantity"` - ProductCount int64 `json:"product_count"` - OrderCount int64 `json:"order_count"` + CategoryID uuid.UUID `json:"category_id"` + CategoryName string `json:"category_name"` + TotalRevenue float64 `json:"total_revenue"` + TotalQuantity int64 `json:"total_quantity"` + ProductCount int64 `json:"product_count"` + OrderCount int64 `json:"order_count"` + TotalStandardHpp float64 `json:"total_standard_hpp"` + TotalFifoHpp float64 `json:"total_fifo_hpp"` + TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"` } // DashboardAnalyticsRequest represents the request for dashboard analytics diff --git a/internal/contract/hpp_contract.go b/internal/contract/hpp_contract.go deleted file mode 100644 index 76b1b62..0000000 --- a/internal/contract/hpp_contract.go +++ /dev/null @@ -1,94 +0,0 @@ -package contract - -import ( - "time" - - "github.com/google/uuid" -) - -type StandardHPPRequest struct { - OrganizationID uuid.UUID `form:"-"` - ProductID *uuid.UUID `form:"product_id,omitempty"` - CategoryID *uuid.UUID `form:"category_id,omitempty"` -} - -type StandardHPPResponse struct { - OrganizationID uuid.UUID `json:"organization_id"` - Summary HPPSummary `json:"summary"` - Products []StandardHPPProductData `json:"products"` - Ingredients []StandardHPPIngredientData `json:"ingredients,omitempty"` -} - -type StandardHPPProductData struct { - ProductID uuid.UUID `json:"product_id"` - ProductName string `json:"product_name"` - ProductSku string `json:"product_sku"` - CategoryID uuid.UUID `json:"category_id"` - CategoryName string `json:"category_name"` - SellingPrice float64 `json:"selling_price"` - ProductCost float64 `json:"product_cost"` - StandardCost float64 `json:"standard_cost"` - StandardHPPPercentage float64 `json:"standard_hpp_percentage"` - HasRecipe bool `json:"has_recipe"` -} - -type StandardHPPIngredientData struct { - ProductID uuid.UUID `json:"product_id"` - IngredientID uuid.UUID `json:"ingredient_id"` - IngredientName string `json:"ingredient_name"` - Quantity float64 `json:"quantity"` - UnitName string `json:"unit_name"` - CostPerUnit float64 `json:"cost_per_unit"` - WastePercentage float64 `json:"waste_percentage"` - TotalCost float64 `json:"total_cost"` -} - -type RealHPPRequest struct { - OrganizationID uuid.UUID `form:"-"` - OutletID *uuid.UUID `form:"outlet_id,omitempty"` - ProductID *uuid.UUID `form:"product_id,omitempty"` - CategoryID *uuid.UUID `form:"category_id,omitempty"` - DateFrom string `form:"date_from" validate:"required"` - DateTo string `form:"date_to" validate:"required"` - GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"` -} - -type RealHPPResponse struct { - OrganizationID uuid.UUID `json:"organization_id"` - OutletID *uuid.UUID `json:"outlet_id,omitempty"` - DateFrom time.Time `json:"date_from"` - DateTo time.Time `json:"date_to"` - GroupBy string `json:"group_by"` - Summary HPPSummary `json:"summary"` - Products []RealHPPProductData `json:"products"` - TimeSeries []RealHPPTimeSeriesData `json:"time_series,omitempty"` -} - -type RealHPPProductData struct { - ProductID uuid.UUID `json:"product_id"` - ProductName string `json:"product_name"` - ProductSku string `json:"product_sku"` - CategoryID uuid.UUID `json:"category_id"` - CategoryName string `json:"category_name"` - SellingPrice float64 `json:"selling_price"` - RealTotalCost float64 `json:"real_total_cost"` - RealTotalRevenue float64 `json:"real_total_revenue"` - RealHPPPercentage float64 `json:"real_hpp_percentage"` - TotalQuantitySold int64 `json:"total_quantity_sold"` - TotalOrders int64 `json:"total_orders"` -} - -type RealHPPTimeSeriesData struct { - Date time.Time `json:"date"` - RealTotalCost float64 `json:"real_total_cost"` - RealTotalRevenue float64 `json:"real_total_revenue"` - RealHPPPercentage float64 `json:"real_hpp_percentage"` - TotalOrders int64 `json:"total_orders"` -} - -type HPPSummary struct { - AverageStandardHPP float64 `json:"average_standard_hpp"` - AverageRealHPP float64 `json:"average_real_hpp"` - HPPVariance float64 `json:"hpp_variance"` - TotalProducts int64 `json:"total_products"` -} diff --git a/internal/entities/analytics.go b/internal/entities/analytics.go index 10ae208..5fae6fe 100644 --- a/internal/entities/analytics.go +++ b/internal/entities/analytics.go @@ -27,28 +27,35 @@ type SalesAnalytics struct { NetSales float64 `json:"net_sales"` } -// ProductAnalytics represents product analytics data type ProductAnalytics struct { - ProductID uuid.UUID `json:"product_id"` - ProductName string `json:"product_name"` - ProductSku string `json:"product_sku"` - CategoryID uuid.UUID `json:"category_id"` - CategoryName string `json:"category_name"` - CategoryOrder int `json:"category_order"` - QuantitySold int64 `json:"quantity_sold"` - Revenue float64 `json:"revenue"` - AveragePrice float64 `json:"average_price"` - OrderCount int64 `json:"order_count"` + ProductID uuid.UUID `json:"product_id"` + ProductName string `json:"product_name"` + ProductSku string `json:"product_sku"` + CategoryID uuid.UUID `json:"category_id"` + CategoryName string `json:"category_name"` + CategoryOrder int `json:"category_order"` + QuantitySold int64 `json:"quantity_sold"` + Revenue float64 `json:"revenue"` + AveragePrice float64 `json:"average_price"` + OrderCount int64 `json:"order_count"` + StandardHppPerUnit float64 `json:"standard_hpp_per_unit"` + StandardHppTotal float64 `json:"standard_hpp_total"` + FifoHppPerUnit float64 `json:"fifo_hpp_per_unit"` + FifoHppTotal float64 `json:"fifo_hpp_total"` + MovingAverageHppPerUnit float64 `json:"moving_average_hpp_per_unit"` + MovingAverageHppTotal float64 `json:"moving_average_hpp_total"` } -// ProductAnalyticsPerCategory represents product analytics data grouped by category type ProductAnalyticsPerCategory struct { - CategoryID uuid.UUID `json:"category_id"` - CategoryName string `json:"category_name"` - TotalRevenue float64 `json:"total_revenue"` - TotalQuantity int64 `json:"total_quantity"` - ProductCount int64 `json:"product_count"` - OrderCount int64 `json:"order_count"` + CategoryID uuid.UUID `json:"category_id"` + CategoryName string `json:"category_name"` + TotalRevenue float64 `json:"total_revenue"` + TotalQuantity int64 `json:"total_quantity"` + ProductCount int64 `json:"product_count"` + OrderCount int64 `json:"order_count"` + TotalStandardHpp float64 `json:"total_standard_hpp"` + TotalFifoHpp float64 `json:"total_fifo_hpp"` + TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"` } // DashboardOverview represents dashboard overview data diff --git a/internal/handler/hpp_handler.go b/internal/handler/hpp_handler.go deleted file mode 100644 index 93921a5..0000000 --- a/internal/handler/hpp_handler.go +++ /dev/null @@ -1,73 +0,0 @@ -package handler - -import ( - "apskel-pos-be/internal/appcontext" - "apskel-pos-be/internal/contract" - "apskel-pos-be/internal/service" - "apskel-pos-be/internal/transformer" - "apskel-pos-be/internal/util" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" -) - -type HPPHandler struct { - hppService service.HPPService - transformer transformer.Transformer -} - -func NewHPPHandler(hppService service.HPPService, t transformer.Transformer) *HPPHandler { - return &HPPHandler{ - hppService: hppService, - transformer: t, - } -} - -func (h *HPPHandler) GetStandardHPP(c *gin.Context) { - ctx := c.Request.Context() - contextInfo := appcontext.FromGinContext(ctx) - - var req contract.StandardHPPRequest - if err := c.ShouldBindQuery(&req); err != nil { - util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "HPPHandler::GetStandardHPP", err.Error())}), "HPPHandler::GetStandardHPP") - return - } - - req.OrganizationID = contextInfo.OrganizationID - modelReq := transformer.StandardHPPContractToModel(&req) - - response, err := h.hppService.GetStandardHPP(ctx, modelReq) - if err != nil { - util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "HPPHandler::GetStandardHPP", err.Error())}), "HPPHandler::GetStandardHPP") - return - } - - contractResp := transformer.StandardHPPModelToContract(response) - util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "HPPHandler::GetStandardHPP") -} - -func (h *HPPHandler) GetRealHPP(c *gin.Context) { - ctx := c.Request.Context() - contextInfo := appcontext.FromGinContext(ctx) - - var req contract.RealHPPRequest - if err := c.ShouldBindQuery(&req); err != nil { - util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "HPPHandler::GetRealHPP", err.Error())}), "HPPHandler::GetRealHPP") - return - } - - req.OrganizationID = contextInfo.OrganizationID - if contextInfo.OutletID != uuid.Nil { - req.OutletID = &contextInfo.OutletID - } - modelReq := transformer.RealHPPContractToModel(&req) - - response, err := h.hppService.GetRealHPP(ctx, modelReq) - if err != nil { - util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "HPPHandler::GetRealHPP", err.Error())}), "HPPHandler::GetRealHPP") - return - } - - contractResp := transformer.RealHPPModelToContract(response) - util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "HPPHandler::GetRealHPP") -} diff --git a/internal/models/analytics.go b/internal/models/analytics.go index 1e41e4e..59b8445 100644 --- a/internal/models/analytics.go +++ b/internal/models/analytics.go @@ -105,18 +105,23 @@ type ProductAnalyticsResponse struct { Data []ProductAnalyticsData `json:"data"` } -// ProductAnalyticsData represents individual product analytics data type ProductAnalyticsData struct { - ProductID uuid.UUID `json:"product_id"` - ProductName string `json:"product_name"` - ProductSku string `json:"product_sku"` - CategoryID uuid.UUID `json:"category_id"` - CategoryName string `json:"category_name"` - CategoryOrder int `json:"category_order"` - QuantitySold int64 `json:"quantity_sold"` - Revenue float64 `json:"revenue"` - AveragePrice float64 `json:"average_price"` - OrderCount int64 `json:"order_count"` + ProductID uuid.UUID `json:"product_id"` + ProductName string `json:"product_name"` + ProductSku string `json:"product_sku"` + CategoryID uuid.UUID `json:"category_id"` + CategoryName string `json:"category_name"` + CategoryOrder int `json:"category_order"` + QuantitySold int64 `json:"quantity_sold"` + Revenue float64 `json:"revenue"` + AveragePrice float64 `json:"average_price"` + OrderCount int64 `json:"order_count"` + StandardHppPerUnit float64 `json:"standard_hpp_per_unit"` + StandardHppTotal float64 `json:"standard_hpp_total"` + FifoHppPerUnit float64 `json:"fifo_hpp_per_unit"` + FifoHppTotal float64 `json:"fifo_hpp_total"` + MovingAverageHppPerUnit float64 `json:"moving_average_hpp_per_unit"` + MovingAverageHppTotal float64 `json:"moving_average_hpp_total"` } // ProductAnalyticsPerCategoryRequest represents the request for product analytics per category @@ -129,21 +134,23 @@ type ProductAnalyticsPerCategoryRequest struct { // ProductAnalyticsPerCategoryResponse represents the response for product analytics per category type ProductAnalyticsPerCategoryResponse struct { - OrganizationID uuid.UUID `json:"organization_id"` - OutletID *uuid.UUID `json:"outlet_id,omitempty"` - DateFrom time.Time `json:"date_from"` - DateTo time.Time `json:"date_to"` - Data []ProductAnalyticsPerCategoryData `json:"data"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` + DateFrom time.Time `json:"date_from"` + DateTo time.Time `json:"date_to"` + Data []ProductAnalyticsPerCategoryData `json:"data"` } -// ProductAnalyticsPerCategoryData represents individual category analytics data type ProductAnalyticsPerCategoryData struct { - CategoryID uuid.UUID `json:"category_id"` - CategoryName string `json:"category_name"` - TotalRevenue float64 `json:"total_revenue"` - TotalQuantity int64 `json:"total_quantity"` - ProductCount int64 `json:"product_count"` - OrderCount int64 `json:"order_count"` + CategoryID uuid.UUID `json:"category_id"` + CategoryName string `json:"category_name"` + TotalRevenue float64 `json:"total_revenue"` + TotalQuantity int64 `json:"total_quantity"` + ProductCount int64 `json:"product_count"` + OrderCount int64 `json:"order_count"` + TotalStandardHpp float64 `json:"total_standard_hpp"` + TotalFifoHpp float64 `json:"total_fifo_hpp"` + TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"` } // DashboardAnalyticsRequest represents the request for dashboard analytics diff --git a/internal/models/hpp.go b/internal/models/hpp.go deleted file mode 100644 index 2a97981..0000000 --- a/internal/models/hpp.go +++ /dev/null @@ -1,94 +0,0 @@ -package models - -import ( - "time" - - "github.com/google/uuid" -) - -type StandardHPPRequest struct { - OrganizationID uuid.UUID `validate:"required"` - ProductID *uuid.UUID `validate:"omitempty"` - CategoryID *uuid.UUID `validate:"omitempty"` -} - -type StandardHPPResponse struct { - OrganizationID uuid.UUID `json:"organization_id"` - Summary HPPSummary `json:"summary"` - Products []StandardHPPProduct `json:"products"` - Ingredients []StandardHPPIngredient `json:"ingredients,omitempty"` -} - -type StandardHPPProduct struct { - ProductID uuid.UUID `json:"product_id"` - ProductName string `json:"product_name"` - ProductSku string `json:"product_sku"` - CategoryID uuid.UUID `json:"category_id"` - CategoryName string `json:"category_name"` - SellingPrice float64 `json:"selling_price"` - ProductCost float64 `json:"product_cost"` - StandardCost float64 `json:"standard_cost"` - StandardHPPPercentage float64 `json:"standard_hpp_percentage"` - HasRecipe bool `json:"has_recipe"` -} - -type StandardHPPIngredient struct { - ProductID uuid.UUID `json:"product_id"` - IngredientID uuid.UUID `json:"ingredient_id"` - IngredientName string `json:"ingredient_name"` - Quantity float64 `json:"quantity"` - UnitName string `json:"unit_name"` - CostPerUnit float64 `json:"cost_per_unit"` - WastePercentage float64 `json:"waste_percentage"` - TotalCost float64 `json:"total_cost"` -} - -type RealHPPRequest struct { - OrganizationID uuid.UUID `validate:"required"` - OutletID *uuid.UUID `validate:"omitempty"` - ProductID *uuid.UUID `validate:"omitempty"` - CategoryID *uuid.UUID `validate:"omitempty"` - DateFrom time.Time `validate:"required"` - DateTo time.Time `validate:"required"` - GroupBy string `validate:"omitempty,oneof=day hour week month"` -} - -type RealHPPResponse struct { - OrganizationID uuid.UUID `json:"organization_id"` - OutletID *uuid.UUID `json:"outlet_id,omitempty"` - DateFrom time.Time `json:"date_from"` - DateTo time.Time `json:"date_to"` - GroupBy string `json:"group_by"` - Summary HPPSummary `json:"summary"` - Products []RealHPPProduct `json:"products"` - TimeSeries []RealHPPTimeSeries `json:"time_series,omitempty"` -} - -type RealHPPProduct struct { - ProductID uuid.UUID `json:"product_id"` - ProductName string `json:"product_name"` - ProductSku string `json:"product_sku"` - CategoryID uuid.UUID `json:"category_id"` - CategoryName string `json:"category_name"` - SellingPrice float64 `json:"selling_price"` - RealTotalCost float64 `json:"real_total_cost"` - RealTotalRevenue float64 `json:"real_total_revenue"` - RealHPPPercentage float64 `json:"real_hpp_percentage"` - TotalQuantitySold int64 `json:"total_quantity_sold"` - TotalOrders int64 `json:"total_orders"` -} - -type RealHPPTimeSeries struct { - Date time.Time `json:"date"` - RealTotalCost float64 `json:"real_total_cost"` - RealTotalRevenue float64 `json:"real_total_revenue"` - RealHPPPercentage float64 `json:"real_hpp_percentage"` - TotalOrders int64 `json:"total_orders"` -} - -type HPPSummary struct { - AverageStandardHPP float64 `json:"average_standard_hpp"` - AverageRealHPP float64 `json:"average_real_hpp"` - HPPVariance float64 `json:"hpp_variance"` - TotalProducts int64 `json:"total_products"` -} diff --git a/internal/processor/analytics_processor.go b/internal/processor/analytics_processor.go index d35ac8d..bef2ca7 100644 --- a/internal/processor/analytics_processor.go +++ b/internal/processor/analytics_processor.go @@ -185,16 +185,22 @@ func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *m var resultData []models.ProductAnalyticsData for _, data := range analyticsData { resultData = append(resultData, models.ProductAnalyticsData{ - ProductID: data.ProductID, - ProductName: data.ProductName, - ProductSku: data.ProductSku, - CategoryID: data.CategoryID, - CategoryName: data.CategoryName, - CategoryOrder: data.CategoryOrder, - QuantitySold: data.QuantitySold, - Revenue: data.Revenue, - AveragePrice: data.AveragePrice, - OrderCount: data.OrderCount, + ProductID: data.ProductID, + ProductName: data.ProductName, + ProductSku: data.ProductSku, + CategoryID: data.CategoryID, + CategoryName: data.CategoryName, + CategoryOrder: data.CategoryOrder, + QuantitySold: data.QuantitySold, + Revenue: data.Revenue, + AveragePrice: data.AveragePrice, + OrderCount: data.OrderCount, + StandardHppPerUnit: data.StandardHppPerUnit, + StandardHppTotal: data.StandardHppTotal, + FifoHppPerUnit: data.FifoHppPerUnit, + FifoHppTotal: data.FifoHppTotal, + MovingAverageHppPerUnit: data.MovingAverageHppPerUnit, + MovingAverageHppTotal: data.MovingAverageHppTotal, }) } @@ -223,12 +229,15 @@ func (p *AnalyticsProcessorImpl) GetProductAnalyticsPerCategory(ctx context.Cont 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, + CategoryID: data.CategoryID, + CategoryName: data.CategoryName, + TotalRevenue: data.TotalRevenue, + TotalQuantity: data.TotalQuantity, + ProductCount: data.ProductCount, + OrderCount: data.OrderCount, + TotalStandardHpp: data.TotalStandardHpp, + TotalFifoHpp: data.TotalFifoHpp, + TotalMovingAverageHpp: data.TotalMovingAverageHpp, }) } diff --git a/internal/processor/hpp_processor.go b/internal/processor/hpp_processor.go deleted file mode 100644 index 75fad27..0000000 --- a/internal/processor/hpp_processor.go +++ /dev/null @@ -1,179 +0,0 @@ -package processor - -import ( - "context" - "fmt" - - "apskel-pos-be/internal/entities" - "apskel-pos-be/internal/models" - "apskel-pos-be/internal/repository" - - "github.com/google/uuid" -) - -type HPPProcessor interface { - GetStandardHPP(ctx context.Context, req *models.StandardHPPRequest) (*models.StandardHPPResponse, error) - GetRealHPP(ctx context.Context, req *models.RealHPPRequest) (*models.RealHPPResponse, error) -} - -type HPPProcessorImpl struct { - hppRepo repository.HPPRepository -} - -func NewHPPProcessorImpl(hppRepo repository.HPPRepository) *HPPProcessorImpl { - return &HPPProcessorImpl{ - hppRepo: hppRepo, - } -} - -func (p *HPPProcessorImpl) GetStandardHPP(ctx context.Context, req *models.StandardHPPRequest) (*models.StandardHPPResponse, error) { - products, err := p.hppRepo.GetStandardHPP(ctx, req.OrganizationID, req.ProductID, req.CategoryID) - if err != nil { - return nil, fmt.Errorf("failed to get standard HPP: %w", err) - } - - productIDs := make([]uuid.UUID, 0) - for _, pp := range products { - if pp.HasRecipe { - productIDs = append(productIDs, pp.ProductID) - } - } - - ingredients, err := p.hppRepo.GetStandardHPPIngredients(ctx, req.OrganizationID, productIDs) - if err != nil { - return nil, fmt.Errorf("failed to get standard HPP ingredients: %w", err) - } - - productData := make([]models.StandardHPPProduct, 0, len(products)) - var totalHPP float64 - var hppCount int64 - - for _, pp := range products { - productData = append(productData, models.StandardHPPProduct{ - ProductID: pp.ProductID, - ProductName: pp.ProductName, - ProductSku: pp.ProductSku, - CategoryID: pp.CategoryID, - CategoryName: pp.CategoryName, - SellingPrice: pp.SellingPrice, - ProductCost: pp.ProductCost, - StandardCost: pp.StandardCost, - StandardHPPPercentage: pp.StandardHPPPercentage, - HasRecipe: pp.HasRecipe, - }) - if pp.HasRecipe { - totalHPP += pp.StandardHPPPercentage - hppCount++ - } - } - - ingredientData := make([]models.StandardHPPIngredient, 0, len(ingredients)) - for _, ing := range ingredients { - ingredientData = append(ingredientData, models.StandardHPPIngredient{ - ProductID: ing.ProductID, - IngredientID: ing.IngredientID, - IngredientName: ing.IngredientName, - Quantity: ing.Quantity, - UnitName: ing.UnitName, - CostPerUnit: ing.CostPerUnit, - WastePercentage: ing.WastePercentage, - TotalCost: ing.TotalCost, - }) - } - - var avgHPP float64 - if hppCount > 0 { - avgHPP = totalHPP / float64(hppCount) - } - - return &models.StandardHPPResponse{ - OrganizationID: req.OrganizationID, - Summary: models.HPPSummary{ - AverageStandardHPP: avgHPP, - AverageRealHPP: 0, - HPPVariance: 0, - TotalProducts: int64(len(products)), - }, - Products: productData, - Ingredients: ingredientData, - }, nil -} - -func (p *HPPProcessorImpl) GetRealHPP(ctx context.Context, req *models.RealHPPRequest) (*models.RealHPPResponse, error) { - if req.DateFrom.After(req.DateTo) { - return nil, fmt.Errorf("date_from cannot be after date_to") - } - - if req.GroupBy == "" { - req.GroupBy = "day" - } - - products, err := p.hppRepo.GetRealHPP(ctx, req.OrganizationID, req.OutletID, req.ProductID, req.CategoryID, req.DateFrom, req.DateTo) - if err != nil { - return nil, fmt.Errorf("failed to get real HPP: %w", err) - } - - var timeSeries []*entities.RealHPPTimeSeries - if req.GroupBy != "" { - timeSeries, err = p.hppRepo.GetRealHPPTimeSeries(ctx, req.OrganizationID, req.OutletID, req.ProductID, req.CategoryID, req.DateFrom, req.DateTo, req.GroupBy) - if err != nil { - return nil, fmt.Errorf("failed to get real HPP time series: %w", err) - } - } - - productData := make([]models.RealHPPProduct, 0, len(products)) - var totalHPP float64 - var totalRevenue float64 - var hppCount int64 - - for _, pp := range products { - productData = append(productData, models.RealHPPProduct{ - ProductID: pp.ProductID, - ProductName: pp.ProductName, - ProductSku: pp.ProductSku, - CategoryID: pp.CategoryID, - CategoryName: pp.CategoryName, - SellingPrice: pp.SellingPrice, - RealTotalCost: pp.RealTotalCost, - RealTotalRevenue: pp.RealTotalRevenue, - RealHPPPercentage: pp.RealHPPPercentage, - TotalQuantitySold: pp.TotalQuantitySold, - TotalOrders: pp.TotalOrders, - }) - if pp.RealTotalRevenue > 0 { - totalHPP += pp.RealHPPPercentage - totalRevenue += pp.RealTotalRevenue - hppCount++ - } - } - - var avgHPP float64 - if hppCount > 0 { - avgHPP = totalHPP / float64(hppCount) - } - - tsData := make([]models.RealHPPTimeSeries, 0, len(timeSeries)) - for _, ts := range timeSeries { - tsData = append(tsData, models.RealHPPTimeSeries{ - Date: ts.Date, - RealTotalCost: ts.RealTotalCost, - RealTotalRevenue: ts.RealTotalRevenue, - RealHPPPercentage: ts.RealHPPPercentage, - TotalOrders: ts.TotalOrders, - }) - } - - return &models.RealHPPResponse{ - OrganizationID: req.OrganizationID, - OutletID: req.OutletID, - DateFrom: req.DateFrom, - DateTo: req.DateTo, - GroupBy: req.GroupBy, - Summary: models.HPPSummary{ - AverageRealHPP: avgHPP, - TotalProducts: int64(len(products)), - }, - Products: productData, - TimeSeries: tsData, - }, nil -} diff --git a/internal/repository/analytics_repository.go b/internal/repository/analytics_repository.go index 1d06549..0ee622e 100644 --- a/internal/repository/analytics_repository.go +++ b/internal/repository/analytics_repository.go @@ -124,11 +124,45 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ WHEN SUM(oi.quantity) > 0 THEN COALESCE(SUM(oi.total_price), 0) / SUM(oi.quantity) ELSE 0 END as average_price, - COUNT(DISTINCT oi.order_id) as order_count + COUNT(DISTINCT oi.order_id) as order_count, + COALESCE(( + SELECT SUM(pr.quantity * (1 + COALESCE(pr.waste_percentage, 0)/100.0) * i.cost) + FROM product_recipes pr + JOIN ingredients i ON pr.ingredient_id = i.id + WHERE pr.product_id = p.id + ), p.cost, 0) as standard_hpp_per_unit, + COALESCE(( + SELECT SUM(pr.quantity * (1 + COALESCE(pr.waste_percentage, 0)/100.0) * i.cost) + FROM product_recipes pr + JOIN ingredients i ON pr.ingredient_id = i.id + WHERE pr.product_id = p.id + ), p.cost, 0) * COALESCE(SUM(oi.quantity), 0) as standard_hpp_total, + CASE + WHEN SUM(oi.quantity) > 0 THEN COALESCE(SUM(oi.total_cost), 0) / SUM(oi.quantity) + ELSE 0 + END as fifo_hpp_per_unit, + COALESCE(SUM(oi.total_cost), 0) as fifo_hpp_total, + COALESCE(mahpp.hpp_per_unit, p.cost, 0) as moving_average_hpp_per_unit, + COALESCE(mahpp.hpp_per_unit, p.cost, 0) * COALESCE(SUM(oi.quantity), 0) as moving_average_hpp_total `). Joins("JOIN products p ON oi.product_id = p.id"). Joins("JOIN categories c ON p.category_id = c.id"). Joins("JOIN orders o ON oi.order_id = o.id"). + Joins("LEFT JOIN (?) mahpp ON mahpp.product_id = p.id", + r.db.Table("product_recipes pr2"). + Select("pr2.product_id, SUM(pr2.quantity * (1 + COALESCE(pr2.waste_percentage, 0)/100.0) * COALESCE(ma.moving_avg_cost, ing.cost)) as hpp_per_unit"). + Joins("JOIN ingredients ing ON pr2.ingredient_id = ing.id"). + Joins("LEFT JOIN (?) ma ON ma.ingredient_id = pr2.ingredient_id", + r.db.Table("inventory_movements im"). + Select("im.item_id as ingredient_id, CASE WHEN SUM(im.quantity) > 0 THEN SUM(im.total_cost) / SUM(im.quantity) ELSE 0 END as moving_avg_cost"). + Where("im.movement_type = ?", "purchase"). + Where("im.item_type = ?", "INGREDIENT"). + Where("im.organization_id = ?", organizationID). + Where("im.created_at <= ?", dateTo). + Group("im.item_id"), + ). + Group("pr2.product_id"), + ). Where("o.organization_id = ?", organizationID). Where("o.is_void = ?", false). Where("o.is_refund = ?", false). @@ -141,7 +175,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ } err := query. - Group("p.id, p.name, c.id, c.name, c.order"). + Group("p.id, p.name, p.cost, c.id, c.name, c.order, mahpp.hpp_per_unit"). Order("revenue DESC"). Limit(limit). Scan(&results).Error @@ -160,11 +194,30 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerCategory(ctx context.Con 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 + COUNT(DISTINCT oi.order_id) as order_count, + COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(shpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_standard_hpp, + COALESCE(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), 0) as total_fifo_hpp, + COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(mahpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_moving_average_hpp `). Joins("JOIN products p ON oi.product_id = p.id"). Joins("JOIN categories c ON p.category_id = c.id"). Joins("JOIN orders o ON oi.order_id = o.id"). + Joins("LEFT JOIN (SELECT pr.product_id, SUM(pr.quantity * (1 + COALESCE(pr.waste_percentage, 0)/100.0) * i.cost) as hpp_per_unit FROM product_recipes pr JOIN ingredients i ON pr.ingredient_id = i.id GROUP BY pr.product_id) shpp ON shpp.product_id = p.id"). + Joins("LEFT JOIN (?) mahpp ON mahpp.product_id = p.id", + r.db.Table("product_recipes pr2"). + Select("pr2.product_id, SUM(pr2.quantity * (1 + COALESCE(pr2.waste_percentage, 0)/100.0) * COALESCE(ma.moving_avg_cost, ing.cost)) as hpp_per_unit"). + Joins("JOIN ingredients ing ON pr2.ingredient_id = ing.id"). + Joins("LEFT JOIN (?) ma ON ma.ingredient_id = pr2.ingredient_id", + r.db.Table("inventory_movements im"). + Select("im.item_id as ingredient_id, CASE WHEN SUM(im.quantity) > 0 THEN SUM(im.total_cost) / SUM(im.quantity) ELSE 0 END as moving_avg_cost"). + Where("im.movement_type = ?", "purchase"). + Where("im.item_type = ?", "INGREDIENT"). + Where("im.organization_id = ?", organizationID). + Where("im.created_at <= ?", dateTo). + Group("im.item_id"), + ). + Group("pr2.product_id"), + ). Where("o.organization_id = ?", organizationID). Where("o.is_void = ?", false). Where("o.is_refund = ?", false). diff --git a/internal/repository/hpp_repository.go b/internal/repository/hpp_repository.go deleted file mode 100644 index 9ed5b29..0000000 --- a/internal/repository/hpp_repository.go +++ /dev/null @@ -1,227 +0,0 @@ -package repository - -import ( - "context" - "time" - - "apskel-pos-be/internal/entities" - - "github.com/google/uuid" - "gorm.io/gorm" -) - -type HPPRepository interface { - GetStandardHPP(ctx context.Context, organizationID uuid.UUID, productID *uuid.UUID, categoryID *uuid.UUID) ([]*entities.StandardHPPProduct, error) - GetStandardHPPIngredients(ctx context.Context, organizationID uuid.UUID, productIDs []uuid.UUID) ([]*entities.StandardHPPIngredient, error) - GetRealHPP(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, productID *uuid.UUID, categoryID *uuid.UUID, dateFrom, dateTo time.Time) ([]*entities.RealHPPProduct, error) - GetRealHPPTimeSeries(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, productID *uuid.UUID, categoryID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) ([]*entities.RealHPPTimeSeries, error) -} - -type HPPRepositoryImpl struct { - db *gorm.DB -} - -func NewHPPRepositoryImpl(db *gorm.DB) *HPPRepositoryImpl { - return &HPPRepositoryImpl{db: db} -} - -func (r *HPPRepositoryImpl) GetStandardHPP(ctx context.Context, organizationID uuid.UUID, productID *uuid.UUID, categoryID *uuid.UUID) ([]*entities.StandardHPPProduct, error) { - var results []*entities.StandardHPPProduct - - query := r.db.WithContext(ctx). - Table("products p"). - Select(` - p.id as product_id, - p.name as product_name, - COALESCE(p.sku, '') as product_sku, - c.id as category_id, - c.name as category_name, - p.price as selling_price, - p.cost as product_cost, - COALESCE(SUM( - pr.quantity * i.cost * (1 + COALESCE(pr.waste_percentage, 0) / 100.0) - ), 0) as standard_cost, - CASE - WHEN p.price > 0 - THEN COALESCE(SUM(pr.quantity * i.cost * (1 + COALESCE(pr.waste_percentage, 0) / 100.0)), 0) / p.price * 100 - ELSE 0 - END as standard_hpp_percentage, - CASE WHEN COUNT(pr.id) > 0 THEN true ELSE false END as has_recipe - `). - Joins("LEFT JOIN product_recipes pr ON pr.product_id = p.id AND pr.organization_id = ?", organizationID). - Joins("LEFT JOIN ingredients i ON pr.ingredient_id = i.id"). - Joins("JOIN categories c ON p.category_id = c.id"). - Where("p.organization_id = ?", organizationID). - Where("p.is_active = ?", true) - - if productID != nil { - query = query.Where("p.id = ?", *productID) - } - if categoryID != nil { - query = query.Where("p.category_id = ?", *categoryID) - } - - err := query. - Group("p.id, p.name, p.sku, c.id, c.name, p.price, p.cost"). - Order("p.name ASC"). - Scan(&results).Error - - return results, err -} - -func (r *HPPRepositoryImpl) GetStandardHPPIngredients(ctx context.Context, organizationID uuid.UUID, productIDs []uuid.UUID) ([]*entities.StandardHPPIngredient, error) { - if len(productIDs) == 0 { - return []*entities.StandardHPPIngredient{}, nil - } - - var results []*entities.StandardHPPIngredient - - err := r.db.WithContext(ctx). - Table("product_recipes pr"). - Select(` - pr.product_id, - pr.ingredient_id, - i.name as ingredient_name, - pr.quantity, - COALESCE(u.name, '') as unit_name, - i.cost as cost_per_unit, - COALESCE(pr.waste_percentage, 0) as waste_percentage, - pr.quantity * i.cost * (1 + COALESCE(pr.waste_percentage, 0) / 100.0) as total_cost - `). - Joins("JOIN ingredients i ON pr.ingredient_id = i.id"). - Joins("LEFT JOIN units u ON i.unit_id = u.id"). - Where("pr.organization_id = ?", organizationID). - Where("pr.product_id IN ?", productIDs). - Order("pr.product_id, i.name"). - Scan(&results).Error - - return results, err -} - -func (r *HPPRepositoryImpl) GetRealHPP(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, productID *uuid.UUID, categoryID *uuid.UUID, dateFrom, dateTo time.Time) ([]*entities.RealHPPProduct, error) { - var results []*entities.RealHPPProduct - - query := r.db.WithContext(ctx). - Table("products p"). - Select(` - p.id as product_id, - p.name as product_name, - COALESCE(p.sku, '') as product_sku, - c.id as category_id, - c.name as category_name, - p.price as selling_price, - COALESCE(SUM( - CASE WHEN oi.is_fully_refunded = false - THEN oi.unit_cost * (oi.quantity - COALESCE(oi.refund_quantity, 0)) - ELSE 0 END - ), 0) as real_total_cost, - COALESCE(SUM( - CASE WHEN oi.is_fully_refunded = false - THEN oi.total_price - COALESCE(oi.refund_amount, 0) - ELSE 0 END - ), 0) as real_total_revenue, - CASE - WHEN COALESCE(SUM( - CASE WHEN oi.is_fully_refunded = false - THEN oi.total_price - COALESCE(oi.refund_amount, 0) - ELSE 0 END - ), 0) > 0 - THEN COALESCE(SUM( - CASE WHEN oi.is_fully_refunded = false - THEN oi.unit_cost * (oi.quantity - COALESCE(oi.refund_quantity, 0)) - ELSE 0 END - ), 0) / COALESCE(SUM( - CASE WHEN oi.is_fully_refunded = false - THEN oi.total_price - COALESCE(oi.refund_amount, 0) - ELSE 0 END - ), 0) * 100 - ELSE 0 - END as real_hpp_percentage, - COALESCE(SUM( - CASE WHEN oi.is_fully_refunded = false - THEN oi.quantity - COALESCE(oi.refund_quantity, 0) - ELSE 0 END - ), 0) as total_quantity_sold, - COALESCE(COUNT(DISTINCT oi.order_id), 0) as total_orders - `). - Joins("JOIN categories c ON p.category_id = c.id"). - Joins("LEFT JOIN order_items oi ON oi.product_id = p.id AND oi.status != 'cancelled'"). - Joins("LEFT JOIN orders o ON oi.order_id = o.id AND o.is_void = false AND o.is_refund = false AND o.status = 'completed' AND o.payment_status = 'completed' AND o.organization_id = ? AND o.created_at >= ? AND o.created_at <= ?", organizationID, dateFrom, dateTo). - Where("p.organization_id = ?", organizationID). - Where("p.is_active = ?", true) - - if outletID != nil { - query = query.Where("o.outlet_id = ? OR o.id IS NULL", *outletID) - } - if productID != nil { - query = query.Where("p.id = ?", *productID) - } - if categoryID != nil { - query = query.Where("p.category_id = ?", *categoryID) - } - - err := query. - Group("p.id, p.name, p.sku, c.id, c.name, p.price"). - Order("p.name ASC"). - Scan(&results).Error - - return results, err -} - -func (r *HPPRepositoryImpl) GetRealHPPTimeSeries(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, productID *uuid.UUID, categoryID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) ([]*entities.RealHPPTimeSeries, error) { - var results []*entities.RealHPPTimeSeries - - var timeFormat string - switch groupBy { - case "hour": - timeFormat = "DATE_TRUNC('hour', o.created_at)" - case "week": - timeFormat = "DATE_TRUNC('week', o.created_at)" - case "month": - timeFormat = "DATE_TRUNC('month', o.created_at)" - default: - timeFormat = "DATE_TRUNC('day', o.created_at)" - } - - query := r.db.WithContext(ctx). - Table("orders o"). - Select(` - `+timeFormat+` as date, - COALESCE(SUM( - oi.unit_cost * (oi.quantity - COALESCE(oi.refund_quantity, 0)) - ), 0) as real_total_cost, - COALESCE(SUM( - oi.total_price - COALESCE(oi.refund_amount, 0) - ), 0) as real_total_revenue, - CASE - WHEN COALESCE(SUM(oi.total_price - COALESCE(oi.refund_amount, 0)), 0) > 0 - THEN COALESCE(SUM(oi.unit_cost * (oi.quantity - COALESCE(oi.refund_quantity, 0))), 0) / COALESCE(SUM(oi.total_price - COALESCE(oi.refund_amount, 0)), 0) * 100 - ELSE 0 - END as real_hpp_percentage, - COUNT(DISTINCT o.id) as total_orders - `). - Joins("JOIN order_items oi ON oi.order_id = o.id AND oi.status != 'cancelled' AND oi.is_fully_refunded = false"). - Where("o.organization_id = ?", organizationID). - Where("o.is_void = false"). - Where("o.is_refund = false"). - Where("o.status = 'completed'"). - Where("o.payment_status = 'completed'"). - Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo) - - if outletID != nil { - query = query.Where("o.outlet_id = ?", *outletID) - } - if productID != nil { - query = query.Where("oi.product_id = ?", *productID) - } - if categoryID != nil { - query = query.Where("oi.product_id IN (SELECT id FROM products WHERE category_id = ?)", *categoryID) - } - - err := query. - Group(timeFormat). - Order(timeFormat). - Scan(&results).Error - - return results, err -} diff --git a/internal/router/router.go b/internal/router/router.go index cf29b2a..2fe6206 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -46,71 +46,11 @@ type Router struct { customerAuthHandler *handler.CustomerAuthHandler customerPointsHandler *handler.CustomerPointsHandler spinGameHandler *handler.SpinGameHandler - hppHandler *handler.HPPHandler authMiddleware *middleware.AuthMiddleware customerAuthMiddleware *middleware.CustomerAuthMiddleware } -func NewRouter(cfg *config.Config, - healthHandler *handler.HealthHandler, - authService service.AuthService, - authMiddleware *middleware.AuthMiddleware, - userService *service.UserServiceImpl, - userValidator *validator.UserValidatorImpl, - organizationService service.OrganizationService, - organizationValidator validator.OrganizationValidator, - outletService service.OutletService, - outletValidator validator.OutletValidator, - outletSettingService service.OutletSettingService, - categoryService service.CategoryService, - categoryValidator validator.CategoryValidator, - productService service.ProductService, - productValidator validator.ProductValidator, - productVariantService service.ProductVariantService, - productVariantValidator validator.ProductVariantValidator, - inventoryService service.InventoryService, - inventoryValidator validator.InventoryValidator, - orderService service.OrderService, - orderValidator validator.OrderValidator, - fileService service.FileService, - fileValidator validator.FileValidator, - customerService service.CustomerService, - customerValidator validator.CustomerValidator, - paymentMethodService service.PaymentMethodService, - paymentMethodValidator validator.PaymentMethodValidator, - analyticsService *service.AnalyticsServiceImpl, - reportService service.ReportService, - tableService *service.TableServiceImpl, - tableValidator *validator.TableValidator, - unitService handler.UnitService, - ingredientService handler.IngredientService, - productRecipeService service.ProductRecipeService, - vendorService service.VendorService, - vendorValidator validator.VendorValidator, - purchaseOrderService service.PurchaseOrderService, - purchaseOrderValidator validator.PurchaseOrderValidator, - unitConverterService service.IngredientUnitConverterService, - unitConverterValidator validator.IngredientUnitConverterValidator, - chartOfAccountTypeService service.ChartOfAccountTypeService, - chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, - chartOfAccountService service.ChartOfAccountService, - chartOfAccountValidator validator.ChartOfAccountValidator, - accountService service.AccountService, - accountValidator validator.AccountValidator, - orderIngredientTransactionService service.OrderIngredientTransactionService, - orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, - gamificationService service.GamificationService, - gamificationValidator validator.GamificationValidator, - rewardService service.RewardService, - rewardValidator validator.RewardValidator, - campaignService service.CampaignService, - campaignValidator validator.CampaignValidator, - customerAuthService service.CustomerAuthService, - customerAuthValidator validator.CustomerAuthValidator, - customerPointsService service.CustomerPointsService, - spinGameService service.SpinGameService, - hppService *service.HPPServiceImpl, - customerAuthMiddleware *middleware.CustomerAuthMiddleware) *Router { +func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware) *Router { return &Router{ config: cfg, @@ -146,7 +86,6 @@ func NewRouter(cfg *config.Config, customerAuthHandler: handler.NewCustomerAuthHandler(customerAuthService, customerAuthValidator), customerPointsHandler: handler.NewCustomerPointsHandler(customerPointsService), spinGameHandler: handler.NewSpinGameHandler(spinGameService), - hppHandler: handler.NewHPPHandler(hppService, transformer.NewTransformer()), authMiddleware: authMiddleware, customerAuthMiddleware: customerAuthMiddleware, productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator), @@ -363,13 +302,6 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { analytics.GET("/profit-loss", r.analyticsHandler.GetProfitLossAnalytics) } - hpp := protected.Group("/hpp") - hpp.Use(r.authMiddleware.RequireAdminOrManager()) - { - hpp.GET("/standard", r.hppHandler.GetStandardHPP) - hpp.GET("/real", r.hppHandler.GetRealHPP) - } - tables := protected.Group("/tables") tables.Use(r.authMiddleware.RequireAdminOrManager()) { diff --git a/internal/service/hpp_service.go b/internal/service/hpp_service.go deleted file mode 100644 index 2f80d02..0000000 --- a/internal/service/hpp_service.go +++ /dev/null @@ -1,78 +0,0 @@ -package service - -import ( - "context" - "fmt" - - "apskel-pos-be/internal/models" - "apskel-pos-be/internal/processor" - - "github.com/google/uuid" -) - -type HPPService interface { - GetStandardHPP(ctx context.Context, req *models.StandardHPPRequest) (*models.StandardHPPResponse, error) - GetRealHPP(ctx context.Context, req *models.RealHPPRequest) (*models.RealHPPResponse, error) -} - -type HPPServiceImpl struct { - hppProcessor processor.HPPProcessor -} - -func NewHPPServiceImpl(hppProcessor processor.HPPProcessor) *HPPServiceImpl { - return &HPPServiceImpl{ - hppProcessor: hppProcessor, - } -} - -func (s *HPPServiceImpl) GetStandardHPP(ctx context.Context, req *models.StandardHPPRequest) (*models.StandardHPPResponse, error) { - if err := s.validateStandardHPPRequest(req); err != nil { - return nil, fmt.Errorf("validation error: %w", err) - } - - response, err := s.hppProcessor.GetStandardHPP(ctx, req) - if err != nil { - return nil, fmt.Errorf("failed to get standard HPP: %w", err) - } - - return response, nil -} - -func (s *HPPServiceImpl) GetRealHPP(ctx context.Context, req *models.RealHPPRequest) (*models.RealHPPResponse, error) { - if err := s.validateRealHPPRequest(req); err != nil { - return nil, fmt.Errorf("validation error: %w", err) - } - - response, err := s.hppProcessor.GetRealHPP(ctx, req) - if err != nil { - return nil, fmt.Errorf("failed to get real HPP: %w", err) - } - - return response, nil -} - -func (s *HPPServiceImpl) validateStandardHPPRequest(req *models.StandardHPPRequest) error { - if req.OrganizationID == uuid.Nil { - return fmt.Errorf("organization_id is required") - } - return nil -} - -func (s *HPPServiceImpl) validateRealHPPRequest(req *models.RealHPPRequest) error { - if req.OrganizationID == uuid.Nil { - return fmt.Errorf("organization_id is required") - } - if req.DateFrom.IsZero() { - return fmt.Errorf("date_from is required") - } - if req.DateTo.IsZero() { - return fmt.Errorf("date_to is required") - } - if req.DateFrom.After(req.DateTo) { - return fmt.Errorf("date_from cannot be after date_to") - } - if req.GroupBy != "" && req.GroupBy != "hour" && req.GroupBy != "day" && req.GroupBy != "week" && req.GroupBy != "month" { - return fmt.Errorf("invalid group_by value, must be one of: hour, day, week, month") - } - return nil -} diff --git a/internal/transformer/analytics_transformer.go b/internal/transformer/analytics_transformer.go index 934da2a..e776d1b 100644 --- a/internal/transformer/analytics_transformer.go +++ b/internal/transformer/analytics_transformer.go @@ -155,16 +155,22 @@ func ProductAnalyticsModelToContract(resp *models.ProductAnalyticsResponse) *con var data []contract.ProductAnalyticsData for _, item := range resp.Data { data = append(data, contract.ProductAnalyticsData{ - ProductID: item.ProductID, - ProductName: item.ProductName, - ProductSku: item.ProductSku, - CategoryID: item.CategoryID, - CategoryName: item.CategoryName, - CategoryOrder: item.CategoryOrder, - QuantitySold: item.QuantitySold, - Revenue: item.Revenue, - AveragePrice: item.AveragePrice, - OrderCount: item.OrderCount, + ProductID: item.ProductID, + ProductName: item.ProductName, + ProductSku: item.ProductSku, + CategoryID: item.CategoryID, + CategoryName: item.CategoryName, + CategoryOrder: item.CategoryOrder, + QuantitySold: item.QuantitySold, + Revenue: item.Revenue, + AveragePrice: item.AveragePrice, + OrderCount: item.OrderCount, + StandardHppPerUnit: item.StandardHppPerUnit, + StandardHppTotal: item.StandardHppTotal, + FifoHppPerUnit: item.FifoHppPerUnit, + FifoHppTotal: item.FifoHppTotal, + MovingAverageHppPerUnit: item.MovingAverageHppPerUnit, + MovingAverageHppTotal: item.MovingAverageHppTotal, }) } @@ -208,12 +214,15 @@ func ProductAnalyticsPerCategoryModelToContract(resp *models.ProductAnalyticsPer var data []contract.ProductAnalyticsPerCategoryData for _, item := range resp.Data { data = append(data, contract.ProductAnalyticsPerCategoryData{ - CategoryID: item.CategoryID, - CategoryName: item.CategoryName, - TotalRevenue: item.TotalRevenue, - TotalQuantity: item.TotalQuantity, - ProductCount: item.ProductCount, - OrderCount: item.OrderCount, + CategoryID: item.CategoryID, + CategoryName: item.CategoryName, + TotalRevenue: item.TotalRevenue, + TotalQuantity: item.TotalQuantity, + ProductCount: item.ProductCount, + OrderCount: item.OrderCount, + TotalStandardHpp: item.TotalStandardHpp, + TotalFifoHpp: item.TotalFifoHpp, + TotalMovingAverageHpp: item.TotalMovingAverageHpp, }) } @@ -257,14 +266,20 @@ func DashboardAnalyticsModelToContract(resp *models.DashboardAnalyticsResponse) var topProducts []contract.ProductAnalyticsData for _, item := range resp.TopProducts { topProducts = append(topProducts, contract.ProductAnalyticsData{ - ProductID: item.ProductID, - ProductName: item.ProductName, - CategoryID: item.CategoryID, - CategoryName: item.CategoryName, - QuantitySold: item.QuantitySold, - Revenue: item.Revenue, - AveragePrice: item.AveragePrice, - OrderCount: item.OrderCount, + ProductID: item.ProductID, + ProductName: item.ProductName, + CategoryID: item.CategoryID, + CategoryName: item.CategoryName, + QuantitySold: item.QuantitySold, + Revenue: item.Revenue, + AveragePrice: item.AveragePrice, + OrderCount: item.OrderCount, + StandardHppPerUnit: item.StandardHppPerUnit, + StandardHppTotal: item.StandardHppTotal, + FifoHppPerUnit: item.FifoHppPerUnit, + FifoHppTotal: item.FifoHppTotal, + MovingAverageHppPerUnit: item.MovingAverageHppPerUnit, + MovingAverageHppTotal: item.MovingAverageHppTotal, }) } diff --git a/internal/transformer/hpp_transformer.go b/internal/transformer/hpp_transformer.go deleted file mode 100644 index 1f1c67c..0000000 --- a/internal/transformer/hpp_transformer.go +++ /dev/null @@ -1,136 +0,0 @@ -package transformer - -import ( - "apskel-pos-be/internal/contract" - "apskel-pos-be/internal/models" - "apskel-pos-be/internal/util" - "time" -) - -func StandardHPPContractToModel(req *contract.StandardHPPRequest) *models.StandardHPPRequest { - return &models.StandardHPPRequest{ - OrganizationID: req.OrganizationID, - ProductID: req.ProductID, - CategoryID: req.CategoryID, - } -} - -func StandardHPPModelToContract(resp *models.StandardHPPResponse) *contract.StandardHPPResponse { - if resp == nil { - return nil - } - - products := make([]contract.StandardHPPProductData, len(resp.Products)) - for i, p := range resp.Products { - products[i] = contract.StandardHPPProductData{ - ProductID: p.ProductID, - ProductName: p.ProductName, - ProductSku: p.ProductSku, - CategoryID: p.CategoryID, - CategoryName: p.CategoryName, - SellingPrice: p.SellingPrice, - ProductCost: p.ProductCost, - StandardCost: p.StandardCost, - StandardHPPPercentage: p.StandardHPPPercentage, - HasRecipe: p.HasRecipe, - } - } - - ingredients := make([]contract.StandardHPPIngredientData, len(resp.Ingredients)) - for i, ing := range resp.Ingredients { - ingredients[i] = contract.StandardHPPIngredientData{ - ProductID: ing.ProductID, - IngredientID: ing.IngredientID, - IngredientName: ing.IngredientName, - Quantity: ing.Quantity, - UnitName: ing.UnitName, - CostPerUnit: ing.CostPerUnit, - WastePercentage: ing.WastePercentage, - TotalCost: ing.TotalCost, - } - } - - return &contract.StandardHPPResponse{ - OrganizationID: resp.OrganizationID, - Summary: hppSummaryModelToContract(resp.Summary), - Products: products, - Ingredients: ingredients, - } -} - -func RealHPPContractToModel(req *contract.RealHPPRequest) *models.RealHPPRequest { - var dateFrom, dateTo time.Time - - if fromTime, toTime, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo); err == nil { - if fromTime != nil { - dateFrom = *fromTime - } - if toTime != nil { - dateTo = *toTime - } - } - - return &models.RealHPPRequest{ - OrganizationID: req.OrganizationID, - OutletID: req.OutletID, - ProductID: req.ProductID, - CategoryID: req.CategoryID, - DateFrom: dateFrom, - DateTo: dateTo, - GroupBy: req.GroupBy, - } -} - -func RealHPPModelToContract(resp *models.RealHPPResponse) *contract.RealHPPResponse { - if resp == nil { - return nil - } - - products := make([]contract.RealHPPProductData, len(resp.Products)) - for i, p := range resp.Products { - products[i] = contract.RealHPPProductData{ - ProductID: p.ProductID, - ProductName: p.ProductName, - ProductSku: p.ProductSku, - CategoryID: p.CategoryID, - CategoryName: p.CategoryName, - SellingPrice: p.SellingPrice, - RealTotalCost: p.RealTotalCost, - RealTotalRevenue: p.RealTotalRevenue, - RealHPPPercentage: p.RealHPPPercentage, - TotalQuantitySold: p.TotalQuantitySold, - TotalOrders: p.TotalOrders, - } - } - - timeSeries := make([]contract.RealHPPTimeSeriesData, len(resp.TimeSeries)) - for i, ts := range resp.TimeSeries { - timeSeries[i] = contract.RealHPPTimeSeriesData{ - Date: ts.Date, - RealTotalCost: ts.RealTotalCost, - RealTotalRevenue: ts.RealTotalRevenue, - RealHPPPercentage: ts.RealHPPPercentage, - TotalOrders: ts.TotalOrders, - } - } - - return &contract.RealHPPResponse{ - OrganizationID: resp.OrganizationID, - OutletID: resp.OutletID, - DateFrom: resp.DateFrom, - DateTo: resp.DateTo, - GroupBy: resp.GroupBy, - Summary: hppSummaryModelToContract(resp.Summary), - Products: products, - TimeSeries: timeSeries, - } -} - -func hppSummaryModelToContract(s models.HPPSummary) contract.HPPSummary { - return contract.HPPSummary{ - AverageStandardHPP: s.AverageStandardHPP, - AverageRealHPP: s.AverageRealHPP, - HPPVariance: s.HPPVariance, - TotalProducts: s.TotalProducts, - } -}