hpp #1

Merged
aefril merged 5 commits from hpp into main 2026-04-26 16:08:02 +00:00
15 changed files with 206 additions and 1065 deletions
Showing only changes of commit ba970229a9 - Show all commits

View File

@ -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,
}
}

View File

@ -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

View File

@ -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"`
}

View File

@ -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

View File

@ -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")
}

View File

@ -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

View File

@ -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"`
}

View File

@ -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,
})
}

View File

@ -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
}

View File

@ -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).

View File

@ -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
}

View File

@ -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())
{

View File

@ -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
}

View File

@ -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,
})
}

View File

@ -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,
}
}