Compare commits

...
12 Commits
Author SHA1 Message Date
Efril 75fdb8e847 Merge branch 'hpp' of https://gits.altru.id/apksel-dev/apskel-pos-backend into hpp 2026-04-26 23:04:47 +07:00
Efril 8efa644680 payment amount fix 0 2026-04-26 23:04:31 +07:00
ryan ce99aef289 delete leftover 2026-04-24 18:48:40 +07:00
ryan ba970229a9 Fix HPP 2026-04-24 17:56:38 +07:00
ryan 9b606b4c8b Implementasi HPP std dan real 2026-04-23 11:28:51 +07:00
Efril d3dddea1c7 add category name at product response 2026-04-20 14:21:27 +07:00
Efril 80a78137a0 fix create outlet 2026-04-20 12:31:33 +07:00
efrilm 3826a6b7a9 create outlet route 2026-04-16 19:23:36 +07:00
efrilm d695bedc97 update product response 2026-04-16 17:14:18 +07:00
efrilm 3db4afbce6 fix update ingredients 2026-04-16 15:44:13 +07:00
efrilm 535e4c84f6 product variant route 2026-04-16 14:30:48 +07:00
Efril f25ec1c06f create outlet 2026-04-14 19:09:09 +07:00
22 changed files with 431 additions and 182 deletions
+15 -1
View File
@@ -2,6 +2,7 @@ package appcontext
import (
"context"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
@@ -77,5 +78,18 @@ func FromContext(ctx context.Context) *ContextInfo {
if info, ok := ctx.Value(ctxKey).(*ContextInfo); ok {
return info
}
return nil
// Fallback: construct ContextInfo from individual context values
return &ContextInfo{
CorrelationID: value(ctx, CorrelationIDKey),
UserID: uuidValue(ctx, UserIDKey),
OutletID: uuidValue(ctx, OutletIDKey),
OrganizationID: uuidValue(ctx, OrganizationIDKey),
AppVersion: value(ctx, AppVersionKey),
AppID: value(ctx, AppIDKey),
AppType: value(ctx, AppTypeKey),
Platform: value(ctx, PlatformKey),
DeviceOS: value(ctx, DeviceOSKey),
UserLocale: value(ctx, UserLocaleKey),
UserRole: value(ctx, UserRoleKey),
}
}
+9 -2
View File
@@ -101,7 +101,6 @@ 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"`
@@ -113,6 +112,12 @@ type ProductAnalyticsData struct {
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
@@ -132,7 +137,6 @@ type ProductAnalyticsPerCategoryResponse struct {
Data []ProductAnalyticsPerCategoryData `json:"data"`
}
// ProductAnalyticsPerCategoryData represents individual category analytics data
type ProductAnalyticsPerCategoryData struct {
CategoryID uuid.UUID `json:"category_id"`
CategoryName string `json:"category_name"`
@@ -140,6 +144,9 @@ type ProductAnalyticsPerCategoryData struct {
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
+1 -1
View File
@@ -185,7 +185,7 @@ type CreatePaymentRequest struct {
type CreatePaymentOrderItemRequest struct {
OrderItemID uuid.UUID `json:"order_item_id" validate:"required"`
Amount float64 `json:"amount" validate:"required,min=0"`
Amount float64 `json:"amount" validate:"min=0"`
}
type PaymentResponse struct {
+1
View File
@@ -59,6 +59,7 @@ type ProductResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
CategoryID uuid.UUID `json:"category_id"`
CategoryName string `json:"category_name"`
SKU *string `json:"sku"`
Name string `json:"name"`
Description *string `json:"description"`
+9 -2
View File
@@ -27,7 +27,6 @@ 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"`
@@ -39,9 +38,14 @@ type ProductAnalytics struct {
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"`
@@ -49,6 +53,9 @@ type ProductAnalyticsPerCategory struct {
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
+1 -1
View File
@@ -19,5 +19,5 @@ type Ingredient struct {
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
Unit *Unit `gorm:"foreignKey:UnitID;references:ID" json:"unit,omitempty"`
}
+31
View File
@@ -102,6 +102,37 @@ func (h *OutletHandler) GetOutlet(c *gin.Context) {
util.HandleResponse(c.Writer, c.Request, outletResponse, "OutletHandler::GetOutlet")
}
func (h *OutletHandler) CreateOutlet(c *gin.Context) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
var req contract.CreateOutletRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.FromContext(ctx).WithError(err).Error("OutletHandler::CreateOutlet -> Failed to bind JSON")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid request body")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OutletHandler::CreateOutlet")
return
}
req.OrganizationID = contextInfo.OrganizationID
validationError, validationErrorCode := h.outletValidator.ValidateCreateOutletRequest(&req)
if validationError != nil {
logger.FromContext(ctx).WithError(validationError).Error("OutletHandler::CreateOutlet -> request validation failed")
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OutletHandler::CreateOutlet")
return
}
outletResponse := h.outletService.CreateOutlet(ctx, &req)
if outletResponse.HasErrors() {
errorResp := outletResponse.GetErrors()[0]
logger.FromContext(ctx).WithError(errorResp).Error("OutletHandler::CreateOutlet -> Failed to create outlet from service")
}
util.HandleResponse(c.Writer, c.Request, outletResponse, "OutletHandler::CreateOutlet")
}
func (h *OutletHandler) UpdateOutlet(c *gin.Context) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
+9
View File
@@ -4,6 +4,8 @@ import (
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models"
"github.com/google/uuid"
)
func ProductEntityToModel(entity *entities.Product) *models.Product {
@@ -118,10 +120,17 @@ func ProductEntityToResponse(entity *entities.Product) *models.ProductResponse {
}
}
// Get category name from the Category relation
categoryName := ""
if entity.Category.ID != uuid.Nil {
categoryName = entity.Category.Name
}
return &models.ProductResponse{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
CategoryID: entity.CategoryID,
CategoryName: categoryName,
SKU: entity.SKU,
Name: entity.Name,
Description: entity.Description,
+9 -2
View File
@@ -105,7 +105,6 @@ 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"`
@@ -117,6 +116,12 @@ type ProductAnalyticsData struct {
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
@@ -136,7 +141,6 @@ type ProductAnalyticsPerCategoryResponse struct {
Data []ProductAnalyticsPerCategoryData `json:"data"`
}
// ProductAnalyticsPerCategoryData represents individual category analytics data
type ProductAnalyticsPerCategoryData struct {
CategoryID uuid.UUID `json:"category_id"`
CategoryName string `json:"category_name"`
@@ -144,6 +148,9 @@ type ProductAnalyticsPerCategoryData struct {
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
+1
View File
@@ -95,6 +95,7 @@ type ProductResponse struct {
ID uuid.UUID
OrganizationID uuid.UUID
CategoryID uuid.UUID
CategoryName string
SKU *string
Name string
Description *string
@@ -195,6 +195,12 @@ func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *m
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,
})
}
@@ -229,6 +235,9 @@ func (p *AnalyticsProcessorImpl) GetProductAnalyticsPerCategory(ctx context.Cont
TotalQuantity: data.TotalQuantity,
ProductCount: data.ProductCount,
OrderCount: data.OrderCount,
TotalStandardHpp: data.TotalStandardHpp,
TotalFifoHpp: data.TotalFifoHpp,
TotalMovingAverageHpp: data.TotalMovingAverageHpp,
})
}
@@ -180,7 +180,10 @@ func (p *IngredientProcessorImpl) UpdateIngredient(ctx context.Context, id uuid.
}
// Update fields
if req.OutletID != nil {
existingIngredient.OutletID = req.OutletID
}
existingIngredient.Name = req.Name
existingIngredient.UnitID = req.UnitID
existingIngredient.Cost = req.Cost
+2 -2
View File
@@ -64,7 +64,7 @@ func (p *OutletProcessorImpl) GetOutletByID(ctx context.Context, organizationID,
func (p *OutletProcessorImpl) CreateOutlet(ctx context.Context, req *models.CreateOutletRequest) (*models.OutletResponse, error) {
// Get organization ID from context
contextInfo := appcontext.FromContext(ctx)
if contextInfo.OrganizationID == uuid.Nil {
if contextInfo == nil || contextInfo.OrganizationID == uuid.Nil {
return nil, fmt.Errorf("organization ID not found in context")
}
@@ -124,7 +124,7 @@ func (p *OutletProcessorImpl) UpdateOutlet(ctx context.Context, outletID uuid.UU
func (p *OutletProcessorImpl) DeleteOutlet(ctx context.Context, outletID uuid.UUID) error {
contextInfo := appcontext.FromContext(ctx)
if contextInfo.OrganizationID == uuid.Nil {
if contextInfo == nil || contextInfo.OrganizationID == uuid.Nil {
return fmt.Errorf("organization ID not found in context")
}
@@ -1,12 +1,11 @@
package processor
import (
"context"
"fmt"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"context"
"fmt"
"github.com/google/uuid"
)
@@ -368,6 +367,8 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
return nil, fmt.Errorf("purchase order not found: %w", err)
}
fmt.Println("status:", po.Status)
// Check if status is changing to "received" and current status is not "received"
if status == "received" && po.Status != "received" {
// Get purchase order with items for inventory update
+56 -3
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).
+5 -1
View File
@@ -66,7 +66,11 @@ func (r *IngredientRepository) GetAll(ctx context.Context, organizationID uuid.U
}
func (r *IngredientRepository) Update(ctx context.Context, ingredient *entities.Ingredient) error {
result := r.db.WithContext(ctx).Where("id = ? AND organization_id = ?", ingredient.ID, ingredient.OrganizationID).Save(ingredient)
result := r.db.WithContext(ctx).
Model(&entities.Ingredient{}).
Where("id = ? AND organization_id = ?", ingredient.ID, ingredient.OrganizationID).
Select("outlet_id", "name", "unit_id", "cost", "stock", "is_semi_finished", "is_active", "metadata", "updated_at").
Updates(ingredient)
if result.Error != nil {
return result.Error
}
@@ -27,6 +27,7 @@ func (r *ProductRecipeRepository) GetByID(ctx context.Context, id, organizationI
Preload("Product").
Preload("ProductVariant").
Preload("Ingredient").
Preload("Ingredient.Unit").
Where("id = ? AND organization_id = ?", id, organizationID).
First(&productRecipe).Error
if err != nil {
@@ -41,6 +42,7 @@ func (r *ProductRecipeRepository) GetByProductID(ctx context.Context, productID,
Preload("Product").
Preload("ProductVariant").
Preload("Ingredient").
Preload("Ingredient.Unit").
Where("product_id = ? AND organization_id = ?", productID, organizationID).
Order("created_at DESC").
Find(&productRecipes).Error
@@ -56,6 +58,7 @@ func (r *ProductRecipeRepository) GetByProductAndVariantID(ctx context.Context,
Preload("Product").
Preload("ProductVariant").
Preload("Ingredient").
Preload("Ingredient.Unit").
Where("product_id = ? AND organization_id = ?", productID, organizationID)
if variantID != nil {
@@ -77,6 +80,7 @@ func (r *ProductRecipeRepository) GetByIngredientID(ctx context.Context, ingredi
Preload("Product").
Preload("ProductVariant").
Preload("Ingredient").
Preload("Ingredient.Unit").
Where("ingredient_id = ? AND organization_id = ?", ingredientID, organizationID).
Order("created_at DESC").
Find(&productRecipes).Error
+11 -59
View File
@@ -50,65 +50,7 @@ type Router struct {
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,
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,6 +88,7 @@ func NewRouter(cfg *config.Config,
spinGameHandler: handler.NewSpinGameHandler(spinGameService),
authMiddleware: authMiddleware,
customerAuthMiddleware: customerAuthMiddleware,
productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator),
}
}
@@ -270,6 +213,14 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
products.DELETE("/:id", r.productHandler.DeleteProduct)
}
productVariants := protected.Group("/product-variants")
{
productVariants.POST("", r.productVariantHandler.CreateProductVariant)
productVariants.PUT("/:id", r.productVariantHandler.UpdateProductVariant)
productVariants.DELETE("/:id", r.productVariantHandler.DeleteProductVariant)
productVariants.GET("/:id", r.productVariantHandler.GetProductVariant)
}
inventory := protected.Group("/inventory")
inventory.Use(r.authMiddleware.RequireAdminOrManager())
{
@@ -594,6 +545,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
outlets := protected.Group("/outlets")
outlets.Use(r.authMiddleware.RequireAdminOrManager())
{
outlets.POST("", r.outletHandler.CreateOutlet)
outlets.GET("/list", r.outletHandler.ListOutlets)
outlets.GET("/detail/:id", r.outletHandler.GetOutlet)
outlets.PUT("/detail/:id", r.outletHandler.UpdateOutlet)
@@ -165,6 +165,12 @@ func ProductAnalyticsModelToContract(resp *models.ProductAnalyticsResponse) *con
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,
})
}
@@ -214,6 +220,9 @@ func ProductAnalyticsPerCategoryModelToContract(resp *models.ProductAnalyticsPer
TotalQuantity: item.TotalQuantity,
ProductCount: item.ProductCount,
OrderCount: item.OrderCount,
TotalStandardHpp: item.TotalStandardHpp,
TotalFifoHpp: item.TotalFifoHpp,
TotalMovingAverageHpp: item.TotalMovingAverageHpp,
})
}
@@ -265,6 +274,12 @@ func DashboardAnalyticsModelToContract(resp *models.DashboardAnalyticsResponse)
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,
})
}
@@ -101,6 +101,7 @@ func ProductModelResponseToResponse(prod *models.ProductResponse) *contract.Prod
ID: prod.ID,
OrganizationID: prod.OrganizationID,
CategoryID: prod.CategoryID,
CategoryName: prod.CategoryName,
SKU: prod.SKU,
Name: prod.Name,
Description: prod.Description,
@@ -0,0 +1,39 @@
-- =========================
-- DROP ORDER DISCOUNTS
-- =========================
DROP INDEX IF EXISTS idx_order_discounts_discount;
DROP INDEX IF EXISTS idx_order_discounts_outlet;
DROP INDEX IF EXISTS idx_order_discounts_order;
DROP TABLE IF EXISTS order_discounts;
-- =========================
-- DROP DISCOUNT CATEGORIES
-- =========================
DROP INDEX IF EXISTS idx_discount_categories_category;
DROP INDEX IF EXISTS idx_discount_categories_discount;
DROP TABLE IF EXISTS discount_categories;
-- =========================
-- DROP DISCOUNT PRODUCTS
-- =========================
DROP INDEX IF EXISTS idx_discount_products_product;
DROP INDEX IF EXISTS idx_discount_products_discount;
DROP TABLE IF EXISTS discount_products;
-- =========================
-- DROP DISCOUNT OUTLETS
-- =========================
DROP INDEX IF EXISTS idx_discount_outlets_outlet;
DROP INDEX IF EXISTS idx_discount_outlets_discount;
DROP TABLE IF EXISTS discount_outlets;
-- =========================
-- DROP DISCOUNTS
-- =========================
DROP INDEX IF EXISTS idx_discounts_customer_type;
DROP INDEX IF EXISTS idx_discounts_dates;
DROP INDEX IF EXISTS idx_discounts_active;
DROP INDEX IF EXISTS idx_discounts_code;
DROP INDEX IF EXISTS idx_discounts_organization;
DROP INDEX IF EXISTS idx_discounts_campaign;
DROP TABLE IF EXISTS discounts;
@@ -0,0 +1,91 @@
CREATE TABLE discounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
campaign_id UUID NULL,
organization_id UUID NOT NULL,
code VARCHAR(50) NOT NULL,
name VARCHAR(255) NOT NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('percentage', 'fixed_amount', 'free_product')),
value DECIMAL(15,2) NOT NULL,
min_purchase_qty INT DEFAULT 0,
min_purchase_amount DECIMAL(15,2) DEFAULT 0,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
usage_limit_per_customer INT DEFAULT NULL,
usage_limit_total INT DEFAULT NULL,
usage_count INT DEFAULT 0,
customer_type VARCHAR(20) DEFAULT 'all' CHECK (customer_type IN ('all', 'member', 'vip')),
is_stackable BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
priority INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (campaign_id) REFERENCES campaigns(id) ON DELETE SET NULL,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
CONSTRAINT chk_discount_ownership CHECK (campaign_id IS NOT NULL OR organization_id IS NOT NULL),
CONSTRAINT unique_code_per_org UNIQUE (organization_id, code)
);
CREATE INDEX idx_discounts_campaign ON discounts(campaign_id);
CREATE INDEX idx_discounts_organization ON discounts(organization_id);
CREATE INDEX idx_discounts_code ON discounts(code);
CREATE INDEX idx_discounts_active ON discounts(is_active);
CREATE INDEX idx_discounts_dates ON discounts(start_date, end_date);
CREATE INDEX idx_discounts_customer_type ON discounts(customer_type);
CREATE TABLE discount_outlets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
discount_id UUID NOT NULL,
outlet_id UUID NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (discount_id) REFERENCES discounts(id) ON DELETE CASCADE,
FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE CASCADE,
CONSTRAINT unique_discount_outlet UNIQUE (discount_id, outlet_id)
);
CREATE INDEX idx_discount_outlets_discount ON discount_outlets(discount_id);
CREATE INDEX idx_discount_outlets_outlet ON discount_outlets(outlet_id);
CREATE TABLE discount_products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
discount_id UUID NOT NULL,
product_id UUID NOT NULL,
rule_type VARCHAR(20) NOT NULL CHECK (rule_type IN ('required', 'free', 'excluded')),
quantity INT DEFAULT 1,
free_quantity INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (discount_id) REFERENCES discounts(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);
CREATE INDEX idx_discount_products_discount ON discount_products(discount_id);
CREATE INDEX idx_discount_products_product ON discount_products(product_id);
CREATE TABLE discount_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
discount_id UUID NOT NULL,
category_id UUID NOT NULL,
rule_type VARCHAR(20) NOT NULL CHECK (rule_type IN ('included', 'excluded')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (discount_id) REFERENCES discounts(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);
CREATE INDEX idx_discount_categories_discount ON discount_categories(discount_id);
CREATE INDEX idx_discount_categories_category ON discount_categories(category_id);
CREATE TABLE order_discounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL,
outlet_id UUID NOT NULL,
discount_id UUID NOT NULL,
discount_amount DECIMAL(15,2) NOT NULL,
applied_rules JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE RESTRICT,
FOREIGN KEY (discount_id) REFERENCES discounts(id) ON DELETE RESTRICT
);
CREATE INDEX idx_order_discounts_order ON order_discounts(order_id);
CREATE INDEX idx_order_discounts_outlet ON order_discounts(outlet_id);
CREATE INDEX idx_order_discounts_discount ON order_discounts(discount_id);