hpp #1
@ -104,6 +104,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
validators.customerAuthValidator,
|
validators.customerAuthValidator,
|
||||||
services.customerPointsService,
|
services.customerPointsService,
|
||||||
services.spinGameService,
|
services.spinGameService,
|
||||||
|
services.hppService,
|
||||||
middleware.customerAuthMiddleware,
|
middleware.customerAuthMiddleware,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -190,6 +191,7 @@ type repositories struct {
|
|||||||
customerAuthRepo repository.CustomerAuthRepository
|
customerAuthRepo repository.CustomerAuthRepository
|
||||||
customerPointsRepo repository.CustomerPointsRepository
|
customerPointsRepo repository.CustomerPointsRepository
|
||||||
otpRepo repository.OtpRepository
|
otpRepo repository.OtpRepository
|
||||||
|
hppRepo *repository.HPPRepositoryImpl
|
||||||
txManager *repository.TxManager
|
txManager *repository.TxManager
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,6 +237,7 @@ func (a *App) initRepositories() *repositories {
|
|||||||
customerAuthRepo: repository.NewCustomerAuthRepository(a.db),
|
customerAuthRepo: repository.NewCustomerAuthRepository(a.db),
|
||||||
customerPointsRepo: repository.NewCustomerPointsRepository(a.db),
|
customerPointsRepo: repository.NewCustomerPointsRepository(a.db),
|
||||||
otpRepo: repository.NewOtpRepository(a.db),
|
otpRepo: repository.NewOtpRepository(a.db),
|
||||||
|
hppRepo: repository.NewHPPRepositoryImpl(a.db),
|
||||||
txManager: repository.NewTxManager(a.db),
|
txManager: repository.NewTxManager(a.db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -276,6 +279,7 @@ type processors struct {
|
|||||||
customerAuthProcessor processor.CustomerAuthProcessor
|
customerAuthProcessor processor.CustomerAuthProcessor
|
||||||
customerPointsProcessor *processor.CustomerPointsProcessor
|
customerPointsProcessor *processor.CustomerPointsProcessor
|
||||||
otpProcessor processor.OtpProcessor
|
otpProcessor processor.OtpProcessor
|
||||||
|
hppProcessor *processor.HPPProcessorImpl
|
||||||
fileClient processor.FileClient
|
fileClient processor.FileClient
|
||||||
inventoryMovementService service.InventoryMovementService
|
inventoryMovementService service.InventoryMovementService
|
||||||
}
|
}
|
||||||
@ -323,6 +327,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
customerAuthProcessor: processor.NewCustomerAuthProcessor(repos.customerAuthRepo, otpProcessor, repos.otpRepo, cfg.GetCustomerJWTSecret(), cfg.GetCustomerJWTExpiresTTL()),
|
customerAuthProcessor: processor.NewCustomerAuthProcessor(repos.customerAuthRepo, otpProcessor, repos.otpRepo, cfg.GetCustomerJWTSecret(), cfg.GetCustomerJWTExpiresTTL()),
|
||||||
customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo, repos.gameRepo),
|
customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo, repos.gameRepo),
|
||||||
otpProcessor: otpProcessor,
|
otpProcessor: otpProcessor,
|
||||||
|
hppProcessor: processor.NewHPPProcessorImpl(repos.hppRepo),
|
||||||
fileClient: fileClient,
|
fileClient: fileClient,
|
||||||
inventoryMovementService: inventoryMovementService,
|
inventoryMovementService: inventoryMovementService,
|
||||||
}
|
}
|
||||||
@ -361,6 +366,7 @@ type services struct {
|
|||||||
customerAuthService service.CustomerAuthService
|
customerAuthService service.CustomerAuthService
|
||||||
customerPointsService service.CustomerPointsService
|
customerPointsService service.CustomerPointsService
|
||||||
spinGameService service.SpinGameService
|
spinGameService service.SpinGameService
|
||||||
|
hppService *service.HPPServiceImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
||||||
@ -396,6 +402,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
customerAuthService := service.NewCustomerAuthService(processors.customerAuthProcessor)
|
customerAuthService := service.NewCustomerAuthService(processors.customerAuthProcessor)
|
||||||
customerPointsService := service.NewCustomerPointsService(processors.customerPointsProcessor)
|
customerPointsService := service.NewCustomerPointsService(processors.customerPointsProcessor)
|
||||||
spinGameService := service.NewSpinGameService(processors.gamePlayProcessor, repos.txManager)
|
spinGameService := service.NewSpinGameService(processors.gamePlayProcessor, repos.txManager)
|
||||||
|
hppService := service.NewHPPServiceImpl(processors.hppProcessor)
|
||||||
|
|
||||||
// Update order service with order ingredient transaction service
|
// Update order service with order ingredient transaction service
|
||||||
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager)
|
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager)
|
||||||
@ -433,6 +440,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
customerAuthService: customerAuthService,
|
customerAuthService: customerAuthService,
|
||||||
customerPointsService: customerPointsService,
|
customerPointsService: customerPointsService,
|
||||||
spinGameService: spinGameService,
|
spinGameService: spinGameService,
|
||||||
|
hppService: hppService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
94
internal/contract/hpp_contract.go
Normal file
94
internal/contract/hpp_contract.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
60
internal/entities/hpp.go
Normal file
60
internal/entities/hpp.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 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"`
|
||||||
|
}
|
||||||
73
internal/handler/hpp_handler.go
Normal file
73
internal/handler/hpp_handler.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
94
internal/models/hpp.go
Normal file
94
internal/models/hpp.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
179
internal/processor/hpp_processor.go
Normal file
179
internal/processor/hpp_processor.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
227
internal/repository/hpp_repository.go
Normal file
227
internal/repository/hpp_repository.go
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@ -46,6 +46,7 @@ type Router struct {
|
|||||||
customerAuthHandler *handler.CustomerAuthHandler
|
customerAuthHandler *handler.CustomerAuthHandler
|
||||||
customerPointsHandler *handler.CustomerPointsHandler
|
customerPointsHandler *handler.CustomerPointsHandler
|
||||||
spinGameHandler *handler.SpinGameHandler
|
spinGameHandler *handler.SpinGameHandler
|
||||||
|
hppHandler *handler.HPPHandler
|
||||||
authMiddleware *middleware.AuthMiddleware
|
authMiddleware *middleware.AuthMiddleware
|
||||||
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
||||||
}
|
}
|
||||||
@ -108,6 +109,7 @@ func NewRouter(cfg *config.Config,
|
|||||||
customerAuthValidator validator.CustomerAuthValidator,
|
customerAuthValidator validator.CustomerAuthValidator,
|
||||||
customerPointsService service.CustomerPointsService,
|
customerPointsService service.CustomerPointsService,
|
||||||
spinGameService service.SpinGameService,
|
spinGameService service.SpinGameService,
|
||||||
|
hppService *service.HPPServiceImpl,
|
||||||
customerAuthMiddleware *middleware.CustomerAuthMiddleware) *Router {
|
customerAuthMiddleware *middleware.CustomerAuthMiddleware) *Router {
|
||||||
|
|
||||||
return &Router{
|
return &Router{
|
||||||
@ -144,6 +146,7 @@ func NewRouter(cfg *config.Config,
|
|||||||
customerAuthHandler: handler.NewCustomerAuthHandler(customerAuthService, customerAuthValidator),
|
customerAuthHandler: handler.NewCustomerAuthHandler(customerAuthService, customerAuthValidator),
|
||||||
customerPointsHandler: handler.NewCustomerPointsHandler(customerPointsService),
|
customerPointsHandler: handler.NewCustomerPointsHandler(customerPointsService),
|
||||||
spinGameHandler: handler.NewSpinGameHandler(spinGameService),
|
spinGameHandler: handler.NewSpinGameHandler(spinGameService),
|
||||||
|
hppHandler: handler.NewHPPHandler(hppService, transformer.NewTransformer()),
|
||||||
authMiddleware: authMiddleware,
|
authMiddleware: authMiddleware,
|
||||||
customerAuthMiddleware: customerAuthMiddleware,
|
customerAuthMiddleware: customerAuthMiddleware,
|
||||||
productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator),
|
productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator),
|
||||||
@ -360,6 +363,13 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
analytics.GET("/profit-loss", r.analyticsHandler.GetProfitLossAnalytics)
|
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 := protected.Group("/tables")
|
||||||
tables.Use(r.authMiddleware.RequireAdminOrManager())
|
tables.Use(r.authMiddleware.RequireAdminOrManager())
|
||||||
{
|
{
|
||||||
|
|||||||
78
internal/service/hpp_service.go
Normal file
78
internal/service/hpp_service.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
136
internal/transformer/hpp_transformer.go
Normal file
136
internal/transformer/hpp_transformer.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user