From 4f6208e4791bbe006e7fa52c012c6e960f740d88 Mon Sep 17 00:00:00 2001 From: Aditya Siregar Date: Sat, 13 Sep 2025 02:17:51 +0700 Subject: [PATCH] fix --- internal/app/app.go | 11 +- internal/contract/product_recipe_contract.go | 76 ++++++++++-- internal/contract/purchase_order_contract.go | 8 +- internal/entities/product.go | 2 +- internal/entities/product_recipe.go | 19 +-- internal/handler/product_recipe_handler.go | 31 +++-- internal/models/product_recipe.go | 56 ++++----- .../order_ingredient_transaction_processor.go | 34 +++--- .../processor/product_recipe_processor.go | 109 ++++++++++-------- internal/processor/repository_interfaces.go | 12 +- internal/service/order_service.go | 48 ++++---- internal/service/product_recipe_service.go | 92 +++++++++++---- internal/service/purchase_order_service.go | 12 +- .../transformer/purchase_order_transformer.go | 49 ++++++-- .../validator/purchase_order_validator.go | 39 +++++-- ...ste_percentage_to_product_recipes.down.sql | 2 + ...waste_percentage_to_product_recipes.up.sql | 5 + 17 files changed, 398 insertions(+), 207 deletions(-) create mode 100644 migrations/000047_add_waste_percentage_to_product_recipes.down.sql create mode 100644 migrations/000047_add_waste_percentage_to_product_recipes.up.sql diff --git a/internal/app/app.go b/internal/app/app.go index 451fa5f..6b224ab 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -167,7 +167,6 @@ type repositories struct { chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl accountRepo *repository.AccountRepositoryImpl orderIngredientTransactionRepo *repository.OrderIngredientTransactionRepositoryImpl - productIngredientRepo *repository.ProductIngredientRepository txManager *repository.TxManager } @@ -201,10 +200,6 @@ func (a *App) initRepositories() *repositories { chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db), accountRepo: repository.NewAccountRepositoryImpl(a.db), orderIngredientTransactionRepo: repository.NewOrderIngredientTransactionRepositoryImpl(a.db).(*repository.OrderIngredientTransactionRepositoryImpl), - productIngredientRepo: func() *repository.ProductIngredientRepository { - db, _ := a.db.DB() - return repository.NewProductIngredientRepository(db) - }(), txManager: repository.NewTxManager(a.db), } } @@ -266,7 +261,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo), chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo), accountProcessor: processor.NewAccountProcessorImpl(repos.accountRepo, repos.chartOfAccountRepo), - orderIngredientTransactionProcessor: processor.NewOrderIngredientTransactionProcessorImpl(repos.orderIngredientTransactionRepo, repos.productIngredientRepo, repos.ingredientRepo, repos.unitRepo).(*processor.OrderIngredientTransactionProcessorImpl), + orderIngredientTransactionProcessor: processor.NewOrderIngredientTransactionProcessorImpl(repos.orderIngredientTransactionRepo, repos.productRecipeRepo, repos.ingredientRepo, repos.unitRepo).(*processor.OrderIngredientTransactionProcessorImpl), fileClient: fileClient, inventoryMovementService: inventoryMovementService, } @@ -312,7 +307,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con productService := service.NewProductService(processors.productProcessor) productVariantService := service.NewProductVariantService(processors.productVariantProcessor) inventoryService := service.NewInventoryService(processors.inventoryProcessor) - orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, nil, processors.orderIngredientTransactionProcessor, *repos.productIngredientRepo, repos.txManager) // Will be updated after orderIngredientTransactionService is created + orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, nil, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager) // Will be updated after orderIngredientTransactionService is created paymentMethodService := service.NewPaymentMethodService(processors.paymentMethodProcessor) fileService := service.NewFileServiceImpl(processors.fileProcessor) var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor) @@ -331,7 +326,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con orderIngredientTransactionService := service.NewOrderIngredientTransactionService(processors.orderIngredientTransactionProcessor, repos.txManager) // Update order service with order ingredient transaction service - orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productIngredientRepo, repos.txManager) + orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager) return &services{ userService: service.NewUserService(processors.userProcessor), diff --git a/internal/contract/product_recipe_contract.go b/internal/contract/product_recipe_contract.go index 92529cc..9df640c 100644 --- a/internal/contract/product_recipe_contract.go +++ b/internal/contract/product_recipe_contract.go @@ -1,18 +1,74 @@ package contract import ( - "apskel-pos-be/internal/models" + "time" "github.com/google/uuid" ) -type ProductRecipeContract interface { - Create(request *models.CreateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) - GetByID(id uuid.UUID, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) - GetByProductID(productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) - GetByProductAndVariantID(productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) - GetByIngredientID(ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) - Update(id uuid.UUID, request *models.UpdateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) - Delete(id uuid.UUID, organizationID uuid.UUID) error - BulkCreate(recipes []models.CreateProductRecipeRequest, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) +// Request structures +type CreateProductRecipeRequest struct { + OutletID *uuid.UUID `json:"outlet_id"` + ProductID uuid.UUID `json:"product_id" validate:"required"` + VariantID *uuid.UUID `json:"variant_id"` + IngredientID uuid.UUID `json:"ingredient_id" validate:"required"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` + WastePercentage float64 `json:"waste_percentage" validate:"min=0,max=100"` +} + +type UpdateProductRecipeRequest struct { + OutletID *uuid.UUID `json:"outlet_id"` + VariantID *uuid.UUID `json:"variant_id"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` + WastePercentage float64 `json:"waste_percentage" validate:"min=0,max=100"` +} + +type GetProductRecipeByProductIDRequest struct { + ProductID uuid.UUID `json:"-"` + VariantID *uuid.UUID `json:"-"` +} + +type BulkCreateProductRecipeRequest struct { + Recipes []CreateProductRecipeRequest `json:"recipes" validate:"required,min=1"` +} + +// Response structures +type ProductRecipeResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` + ProductID uuid.UUID `json:"product_id"` + VariantID *uuid.UUID `json:"variant_id"` + IngredientID uuid.UUID `json:"ingredient_id"` + Quantity float64 `json:"quantity"` + WastePercentage float64 `json:"waste_percentage"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Product *ProductResponse `json:"product,omitempty"` + ProductVariant *ProductVariantResponse `json:"product_variant,omitempty"` + Ingredient *ProductRecipeIngredientResponse `json:"ingredient,omitempty"` +} + +type ProductRecipeIngredientResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` + Name string `json:"name"` + UnitID uuid.UUID `json:"unit_id"` + Cost float64 `json:"cost"` + Stock float64 `json:"stock"` + IsSemiFinished bool `json:"is_semi_finished"` + IsActive bool `json:"is_active"` + Metadata map[string]interface{} `json:"metadata"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Unit *ProductRecipeUnitResponse `json:"unit,omitempty"` +} + +type ProductRecipeUnitResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } \ No newline at end of file diff --git a/internal/contract/purchase_order_contract.go b/internal/contract/purchase_order_contract.go index 4f22d22..4e57cb4 100644 --- a/internal/contract/purchase_order_contract.go +++ b/internal/contract/purchase_order_contract.go @@ -9,8 +9,8 @@ import ( type CreatePurchaseOrderRequest struct { VendorID uuid.UUID `json:"vendor_id" validate:"required"` PONumber string `json:"po_number" validate:"required,min=1,max=50"` - TransactionDate time.Time `json:"transaction_date" validate:"required"` - DueDate time.Time `json:"due_date" validate:"required"` + TransactionDate string `json:"transaction_date" validate:"required"` // Format: YYYY-MM-DD + DueDate string `json:"due_date" validate:"required"` // Format: YYYY-MM-DD Reference *string `json:"reference,omitempty" validate:"omitempty,max=100"` Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved received cancelled"` Message *string `json:"message,omitempty" validate:"omitempty"` @@ -29,8 +29,8 @@ type CreatePurchaseOrderItemRequest struct { type UpdatePurchaseOrderRequest struct { VendorID *uuid.UUID `json:"vendor_id,omitempty" validate:"omitempty"` PONumber *string `json:"po_number,omitempty" validate:"omitempty,min=1,max=50"` - TransactionDate *time.Time `json:"transaction_date,omitempty" validate:"omitempty"` - DueDate *time.Time `json:"due_date,omitempty" validate:"omitempty"` + TransactionDate *string `json:"transaction_date,omitempty" validate:"omitempty"` // Format: YYYY-MM-DD + DueDate *string `json:"due_date,omitempty" validate:"omitempty"` // Format: YYYY-MM-DD Reference *string `json:"reference,omitempty" validate:"omitempty,max=100"` Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved received cancelled"` Message *string `json:"message,omitempty" validate:"omitempty"` diff --git a/internal/entities/product.go b/internal/entities/product.go index 9e52af2..b76d5f2 100644 --- a/internal/entities/product.go +++ b/internal/entities/product.go @@ -30,7 +30,7 @@ type Product struct { Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"` Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"` ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"` - ProductIngredients []ProductIngredient `gorm:"foreignKey:ProductID" json:"product_ingredients,omitempty"` + ProductRecipes []ProductRecipe `gorm:"foreignKey:ProductID" json:"product_recipes,omitempty"` Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"` OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"` } diff --git a/internal/entities/product_recipe.go b/internal/entities/product_recipe.go index 47916c2..d178f2b 100644 --- a/internal/entities/product_recipe.go +++ b/internal/entities/product_recipe.go @@ -8,15 +8,16 @@ import ( ) type ProductRecipe struct { - ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` - OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"` - OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"` - ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id"` - VariantID *uuid.UUID `gorm:"type:uuid;index" json:"variant_id"` - IngredientID uuid.UUID `gorm:"type:uuid;not null;index" json:"ingredient_id"` - Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity"` - CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"` + OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"` + ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id"` + VariantID *uuid.UUID `gorm:"type:uuid;index" json:"variant_id"` + IngredientID uuid.UUID `gorm:"type:uuid;not null;index" json:"ingredient_id"` + Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity"` + WastePercentage float64 `gorm:"type:decimal(5,2);default:0" json:"waste_percentage"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` // Relations Product *Product `gorm:"foreignKey:ProductID" json:"product,omitempty"` diff --git a/internal/handler/product_recipe_handler.go b/internal/handler/product_recipe_handler.go index ad13a1c..15066f1 100644 --- a/internal/handler/product_recipe_handler.go +++ b/internal/handler/product_recipe_handler.go @@ -5,7 +5,6 @@ import ( "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" "apskel-pos-be/internal/logger" - "apskel-pos-be/internal/models" "apskel-pos-be/internal/service" "apskel-pos-be/internal/util" "net/http" @@ -28,7 +27,7 @@ func (h *ProductRecipeHandler) Create(c *gin.Context) { ctx := c.Request.Context() contextInfo := appcontext.FromGinContext(ctx) - var request models.CreateProductRecipeRequest + var request contract.CreateProductRecipeRequest if err := c.ShouldBindJSON(&request); err != nil { logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::Create -> request binding failed") validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) @@ -75,6 +74,7 @@ func (h *ProductRecipeHandler) GetByProductID(c *gin.Context) { ctx := c.Request.Context() contextInfo := appcontext.FromGinContext(ctx) + // Parse product ID from URL parameter productIDStr := c.Param("product_id") productID, err := uuid.Parse(productIDStr) if err != nil { @@ -84,10 +84,9 @@ func (h *ProductRecipeHandler) GetByProductID(c *gin.Context) { return } - // Check if variant_id is provided - variantIDStr := c.Query("variant_id") + // Parse optional variant ID from query parameter var variantID *uuid.UUID - if variantIDStr != "" { + if variantIDStr := c.Query("variant_id"); variantIDStr != "" { parsed, err := uuid.Parse(variantIDStr) if err != nil { logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::GetByProductID -> Invalid variant ID") @@ -98,15 +97,14 @@ func (h *ProductRecipeHandler) GetByProductID(c *gin.Context) { variantID = &parsed } - var recipes []*models.ProductRecipeResponse - if variantIDStr != "" { - // Get by product and variant ID - recipes, err = h.productRecipeService.GetByProductAndVariantID(ctx, productID, variantID, contextInfo.OrganizationID) - } else { - // Get by product ID only - recipes, err = h.productRecipeService.GetByProductID(ctx, productID, contextInfo.OrganizationID) + // Create request object + request := &contract.GetProductRecipeByProductIDRequest{ + ProductID: productID, + VariantID: variantID, } + // Call service + recipes, err := h.productRecipeService.GetByProductID(ctx, request, contextInfo.OrganizationID) if err != nil { logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::GetByProductID -> Failed to get product recipes") validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()) @@ -154,7 +152,7 @@ func (h *ProductRecipeHandler) Update(c *gin.Context) { return } - var request models.UpdateProductRecipeRequest + var request contract.UpdateProductRecipeRequest if err := c.ShouldBindJSON(&request); err != nil { logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::Update -> request binding failed") validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) @@ -204,10 +202,7 @@ func (h *ProductRecipeHandler) BulkCreate(c *gin.Context) { ctx := c.Request.Context() contextInfo := appcontext.FromGinContext(ctx) - var request struct { - Recipes []models.CreateProductRecipeRequest `json:"recipes" validate:"required,min=1"` - } - + var request contract.BulkCreateProductRecipeRequest if err := c.ShouldBindJSON(&request); err != nil { logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::BulkCreate -> request binding failed") validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) @@ -215,7 +210,7 @@ func (h *ProductRecipeHandler) BulkCreate(c *gin.Context) { return } - recipes, err := h.productRecipeService.BulkCreate(ctx, contextInfo.OrganizationID, request.Recipes) + recipes, err := h.productRecipeService.BulkCreate(ctx, contextInfo.OrganizationID, &request) if err != nil { logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::BulkCreate -> Failed to bulk create product recipes") validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()) diff --git a/internal/models/product_recipe.go b/internal/models/product_recipe.go index 15e96c3..5df109c 100644 --- a/internal/models/product_recipe.go +++ b/internal/models/product_recipe.go @@ -7,15 +7,16 @@ import ( ) type ProductRecipe struct { - ID uuid.UUID `json:"id"` - OrganizationID uuid.UUID `json:"organization_id"` - OutletID *uuid.UUID `json:"outlet_id"` - ProductID uuid.UUID `json:"product_id"` - VariantID *uuid.UUID `json:"variant_id"` - IngredientID uuid.UUID `json:"ingredient_id"` - Quantity float64 `json:"quantity"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` + ProductID uuid.UUID `json:"product_id"` + VariantID *uuid.UUID `json:"variant_id"` + IngredientID uuid.UUID `json:"ingredient_id"` + Quantity float64 `json:"quantity"` + WastePercentage float64 `json:"waste_percentage"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // Relations Product *Product `json:"product,omitempty"` @@ -24,29 +25,32 @@ type ProductRecipe struct { } type CreateProductRecipeRequest struct { - OutletID *uuid.UUID `json:"outlet_id"` - ProductID uuid.UUID `json:"product_id" validate:"required"` - VariantID *uuid.UUID `json:"variant_id"` - IngredientID uuid.UUID `json:"ingredient_id" validate:"required"` - Quantity float64 `json:"quantity" validate:"required,gt=0"` + OutletID *uuid.UUID `json:"outlet_id"` + ProductID uuid.UUID `json:"product_id" validate:"required"` + VariantID *uuid.UUID `json:"variant_id"` + IngredientID uuid.UUID `json:"ingredient_id" validate:"required"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` + WastePercentage float64 `json:"waste_percentage" validate:"min=0,max=100"` } type UpdateProductRecipeRequest struct { - OutletID *uuid.UUID `json:"outlet_id"` - VariantID *uuid.UUID `json:"variant_id"` - Quantity float64 `json:"quantity" validate:"required,gt=0"` + OutletID *uuid.UUID `json:"outlet_id"` + VariantID *uuid.UUID `json:"variant_id"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` + WastePercentage float64 `json:"waste_percentage" validate:"min=0,max=100"` } type ProductRecipeResponse struct { - ID uuid.UUID `json:"id"` - OrganizationID uuid.UUID `json:"organization_id"` - OutletID *uuid.UUID `json:"outlet_id"` - ProductID uuid.UUID `json:"product_id"` - VariantID *uuid.UUID `json:"variant_id"` - IngredientID uuid.UUID `json:"ingredient_id"` - Quantity float64 `json:"quantity"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` + ProductID uuid.UUID `json:"product_id"` + VariantID *uuid.UUID `json:"variant_id"` + IngredientID uuid.UUID `json:"ingredient_id"` + Quantity float64 `json:"quantity"` + WastePercentage float64 `json:"waste_percentage"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // Relations Product *Product `json:"product,omitempty"` diff --git a/internal/processor/order_ingredient_transaction_processor.go b/internal/processor/order_ingredient_transaction_processor.go index bcef77d..ba84237 100644 --- a/internal/processor/order_ingredient_transaction_processor.go +++ b/internal/processor/order_ingredient_transaction_processor.go @@ -28,20 +28,20 @@ type OrderIngredientTransactionProcessor interface { type OrderIngredientTransactionProcessorImpl struct { orderIngredientTransactionRepo OrderIngredientTransactionRepository - productIngredientRepo ProductIngredientRepository + productRecipeRepo ProductRecipeRepository ingredientRepo IngredientRepository unitRepo UnitRepository } func NewOrderIngredientTransactionProcessorImpl( orderIngredientTransactionRepo OrderIngredientTransactionRepository, - productIngredientRepo ProductIngredientRepository, + productRecipeRepo ProductRecipeRepository, ingredientRepo IngredientRepository, unitRepo UnitRepository, ) OrderIngredientTransactionProcessor { return &OrderIngredientTransactionProcessorImpl{ orderIngredientTransactionRepo: orderIngredientTransactionRepo, - productIngredientRepo: productIngredientRepo, + productRecipeRepo: productRecipeRepo, ingredientRepo: ingredientRepo, unitRepo: unitRepo, } @@ -334,36 +334,36 @@ func (p *OrderIngredientTransactionProcessorImpl) BulkCreateOrderIngredientTrans } func (p *OrderIngredientTransactionProcessorImpl) CalculateWasteQuantities(ctx context.Context, productID uuid.UUID, quantity float64, organizationID uuid.UUID) ([]*models.CreateOrderIngredientTransactionRequest, error) { - // Get product ingredients - productIngredients, err := p.productIngredientRepo.GetByProductID(ctx, productID, organizationID) + // Get product recipes + productRecipes, err := p.productRecipeRepo.GetByProductID(ctx, productID, organizationID) if err != nil { - return nil, fmt.Errorf("failed to get product ingredients: %w", err) + return nil, fmt.Errorf("failed to get product recipes: %w", err) } - if len(productIngredients) == 0 { + if len(productRecipes) == 0 { return []*models.CreateOrderIngredientTransactionRequest{}, nil } // Get ingredient details for unit information ingredientMap := make(map[uuid.UUID]*entities.Ingredient) - for _, pi := range productIngredients { - ingredient, err := p.ingredientRepo.GetByID(ctx, pi.IngredientID, organizationID) + for _, pr := range productRecipes { + ingredient, err := p.ingredientRepo.GetByID(ctx, pr.IngredientID, organizationID) if err != nil { - return nil, fmt.Errorf("failed to get ingredient %s: %w", pi.IngredientID, err) + return nil, fmt.Errorf("failed to get ingredient %s: %w", pr.IngredientID, err) } - ingredientMap[pi.IngredientID] = ingredient + ingredientMap[pr.IngredientID] = ingredient } // Calculate quantities for each ingredient - transactions := make([]*models.CreateOrderIngredientTransactionRequest, 0, len(productIngredients)) - for _, pi := range productIngredients { - ingredient := ingredientMap[pi.IngredientID] + transactions := make([]*models.CreateOrderIngredientTransactionRequest, 0, len(productRecipes)) + for _, pr := range productRecipes { + ingredient := ingredientMap[pr.IngredientID] // Calculate net quantity (actual quantity needed for the product) - netQty := pi.Quantity * quantity + netQty := pr.Quantity * quantity // Calculate gross quantity (including waste) - wasteMultiplier := 1 + (pi.WastePercentage / 100) + wasteMultiplier := 1 + (pr.WastePercentage / 100) grossQty := netQty * wasteMultiplier // Calculate waste quantity @@ -379,7 +379,7 @@ func (p *OrderIngredientTransactionProcessorImpl) CalculateWasteQuantities(ctx c } transaction := &models.CreateOrderIngredientTransactionRequest{ - IngredientID: pi.IngredientID, + IngredientID: pr.IngredientID, GrossQty: util.RoundToDecimalPlaces(grossQty, 3), NetQty: util.RoundToDecimalPlaces(netQty, 3), WasteQty: util.RoundToDecimalPlaces(wasteQty, 3), diff --git a/internal/processor/product_recipe_processor.go b/internal/processor/product_recipe_processor.go index 7fc39d8..831257e 100644 --- a/internal/processor/product_recipe_processor.go +++ b/internal/processor/product_recipe_processor.go @@ -1,27 +1,26 @@ package processor import ( - "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" "context" "fmt" "time" "apskel-pos-be/internal/entities" - "apskel-pos-be/internal/models" "apskel-pos-be/internal/repository" "github.com/google/uuid" ) type ProductRecipeProcessor interface { - Create(ctx context.Context, req *models.CreateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) - GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) - GetByProductID(ctx context.Context, productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) - GetByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) - GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) - Update(ctx context.Context, id uuid.UUID, req *models.UpdateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) + Create(ctx context.Context, req *contract.CreateProductRecipeRequest, organizationID uuid.UUID) (*contract.ProductRecipeResponse, error) + GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*contract.ProductRecipeResponse, error) + GetByProductID(ctx context.Context, productID uuid.UUID, organizationID uuid.UUID) ([]*contract.ProductRecipeResponse, error) + GetByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*contract.ProductRecipeResponse, error) + GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*contract.ProductRecipeResponse, error) + Update(ctx context.Context, id uuid.UUID, req *contract.UpdateProductRecipeRequest, organizationID uuid.UUID) (*contract.ProductRecipeResponse, error) Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error - BulkCreate(ctx context.Context, recipes []models.CreateProductRecipeRequest, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) + BulkCreate(ctx context.Context, recipes []contract.CreateProductRecipeRequest, organizationID uuid.UUID) ([]*contract.ProductRecipeResponse, error) } type ProductRecipeProcessorImpl struct { @@ -38,7 +37,7 @@ func NewProductRecipeProcessor(productRecipeRepo *repository.ProductRecipeReposi } } -func (p *ProductRecipeProcessorImpl) Create(ctx context.Context, req *models.CreateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) { +func (p *ProductRecipeProcessorImpl) Create(ctx context.Context, req *contract.CreateProductRecipeRequest, organizationID uuid.UUID) (*contract.ProductRecipeResponse, error) { _, err := p.productRepo.GetByID(ctx, req.ProductID) if err != nil { return nil, fmt.Errorf("invalid product: %w", err) @@ -50,15 +49,16 @@ func (p *ProductRecipeProcessorImpl) Create(ctx context.Context, req *models.Cre } entity := &entities.ProductRecipe{ - ID: uuid.New(), - OrganizationID: organizationID, - OutletID: req.OutletID, - ProductID: req.ProductID, - VariantID: req.VariantID, - IngredientID: req.IngredientID, - Quantity: req.Quantity, - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), + ID: uuid.New(), + OrganizationID: organizationID, + OutletID: req.OutletID, + ProductID: req.ProductID, + VariantID: req.VariantID, + IngredientID: req.IngredientID, + Quantity: req.Quantity, + WastePercentage: req.WastePercentage, + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), } if err := p.productRecipeRepo.Create(ctx, entity); err != nil { @@ -73,7 +73,7 @@ func (p *ProductRecipeProcessorImpl) Create(ctx context.Context, req *models.Cre return p.entityToResponse(createdEntity), nil } -func (p *ProductRecipeProcessorImpl) GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) { +func (p *ProductRecipeProcessorImpl) GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*contract.ProductRecipeResponse, error) { entity, err := p.productRecipeRepo.GetByID(ctx, id, organizationID) if err != nil { return nil, fmt.Errorf("failed to get product recipe: %w", err) @@ -82,13 +82,13 @@ func (p *ProductRecipeProcessorImpl) GetByID(ctx context.Context, id uuid.UUID, return p.entityToResponse(entity), nil } -func (p *ProductRecipeProcessorImpl) GetByProductID(ctx context.Context, productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) { +func (p *ProductRecipeProcessorImpl) GetByProductID(ctx context.Context, productID uuid.UUID, organizationID uuid.UUID) ([]*contract.ProductRecipeResponse, error) { entities, err := p.productRecipeRepo.GetByProductID(ctx, productID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get product recipes by product ID: %w", err) } - responses := make([]*models.ProductRecipeResponse, len(entities)) + responses := make([]*contract.ProductRecipeResponse, len(entities)) for i, entity := range entities { responses[i] = p.entityToResponse(entity) } @@ -96,13 +96,13 @@ func (p *ProductRecipeProcessorImpl) GetByProductID(ctx context.Context, product return responses, nil } -func (p *ProductRecipeProcessorImpl) GetByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) { +func (p *ProductRecipeProcessorImpl) GetByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*contract.ProductRecipeResponse, error) { entities, err := p.productRecipeRepo.GetByProductAndVariantID(ctx, productID, variantID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get product recipes by product and variant ID: %w", err) } - responses := make([]*models.ProductRecipeResponse, len(entities)) + responses := make([]*contract.ProductRecipeResponse, len(entities)) for i, entity := range entities { responses[i] = p.entityToResponse(entity) } @@ -110,13 +110,13 @@ func (p *ProductRecipeProcessorImpl) GetByProductAndVariantID(ctx context.Contex return responses, nil } -func (p *ProductRecipeProcessorImpl) GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) { +func (p *ProductRecipeProcessorImpl) GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*contract.ProductRecipeResponse, error) { entities, err := p.productRecipeRepo.GetByIngredientID(ctx, ingredientID, organizationID) if err != nil { return nil, fmt.Errorf("failed to get product recipes by ingredient ID: %w", err) } - responses := make([]*models.ProductRecipeResponse, len(entities)) + responses := make([]*contract.ProductRecipeResponse, len(entities)) for i, entity := range entities { responses[i] = p.entityToResponse(entity) } @@ -124,7 +124,7 @@ func (p *ProductRecipeProcessorImpl) GetByIngredientID(ctx context.Context, ingr return responses, nil } -func (p *ProductRecipeProcessorImpl) Update(ctx context.Context, id uuid.UUID, req *models.UpdateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) { +func (p *ProductRecipeProcessorImpl) Update(ctx context.Context, id uuid.UUID, req *contract.UpdateProductRecipeRequest, organizationID uuid.UUID) (*contract.ProductRecipeResponse, error) { // Get existing entity existingEntity, err := p.productRecipeRepo.GetByID(ctx, id, organizationID) if err != nil { @@ -135,6 +135,7 @@ func (p *ProductRecipeProcessorImpl) Update(ctx context.Context, id uuid.UUID, r existingEntity.OutletID = req.OutletID existingEntity.VariantID = req.VariantID existingEntity.Quantity = req.Quantity + existingEntity.WastePercentage = req.WastePercentage existingEntity.UpdatedAt = time.Now().UTC() if err := p.productRecipeRepo.Update(ctx, existingEntity); err != nil { @@ -158,8 +159,8 @@ func (p *ProductRecipeProcessorImpl) Delete(ctx context.Context, id uuid.UUID, o return nil } -func (p *ProductRecipeProcessorImpl) BulkCreate(ctx context.Context, recipes []models.CreateProductRecipeRequest, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) { - responses := make([]*models.ProductRecipeResponse, 0, len(recipes)) +func (p *ProductRecipeProcessorImpl) BulkCreate(ctx context.Context, recipes []contract.CreateProductRecipeRequest, organizationID uuid.UUID) ([]*contract.ProductRecipeResponse, error) { + responses := make([]*contract.ProductRecipeResponse, 0, len(recipes)) for _, recipe := range recipes { response, err := p.Create(ctx, &recipe, organizationID) @@ -172,21 +173,22 @@ func (p *ProductRecipeProcessorImpl) BulkCreate(ctx context.Context, recipes []m return responses, nil } -func (p *ProductRecipeProcessorImpl) entityToResponse(entity *entities.ProductRecipe) *models.ProductRecipeResponse { - response := &models.ProductRecipeResponse{ - ID: entity.ID, - OrganizationID: entity.OrganizationID, - OutletID: entity.OutletID, - ProductID: entity.ProductID, - VariantID: entity.VariantID, - IngredientID: entity.IngredientID, - Quantity: entity.Quantity, - CreatedAt: entity.CreatedAt, - UpdatedAt: entity.UpdatedAt, +func (p *ProductRecipeProcessorImpl) entityToResponse(entity *entities.ProductRecipe) *contract.ProductRecipeResponse { + response := &contract.ProductRecipeResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, + ProductID: entity.ProductID, + VariantID: entity.VariantID, + IngredientID: entity.IngredientID, + Quantity: entity.Quantity, + WastePercentage: entity.WastePercentage, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, } if entity.Product != nil { - response.Product = &models.Product{ + response.Product = &contract.ProductResponse{ ID: entity.Product.ID, OrganizationID: entity.Product.OrganizationID, CategoryID: entity.Product.CategoryID, @@ -195,11 +197,9 @@ func (p *ProductRecipeProcessorImpl) entityToResponse(entity *entities.ProductRe Description: entity.Product.Description, Price: entity.Product.Price, Cost: entity.Product.Cost, - BusinessType: constants.BusinessType(entity.Product.BusinessType), + BusinessType: string(entity.Product.BusinessType), ImageURL: entity.Product.ImageURL, PrinterType: entity.Product.PrinterType, - UnitID: entity.Product.UnitID, - HasIngredients: entity.Product.HasIngredients, Metadata: entity.Product.Metadata, IsActive: entity.Product.IsActive, CreatedAt: entity.Product.CreatedAt, @@ -208,7 +208,7 @@ func (p *ProductRecipeProcessorImpl) entityToResponse(entity *entities.ProductRe } if entity.ProductVariant != nil { - response.ProductVariant = &models.ProductVariant{ + response.ProductVariant = &contract.ProductVariantResponse{ ID: entity.ProductVariant.ID, ProductID: entity.ProductVariant.ProductID, Name: entity.ProductVariant.Name, @@ -221,7 +221,7 @@ func (p *ProductRecipeProcessorImpl) entityToResponse(entity *entities.ProductRe } if entity.Ingredient != nil { - response.Ingredient = &models.Ingredient{ + response.Ingredient = &contract.ProductRecipeIngredientResponse{ ID: entity.Ingredient.ID, OrganizationID: entity.Ingredient.OrganizationID, OutletID: entity.Ingredient.OutletID, @@ -235,7 +235,22 @@ func (p *ProductRecipeProcessorImpl) entityToResponse(entity *entities.ProductRe CreatedAt: entity.Ingredient.CreatedAt, UpdatedAt: entity.Ingredient.UpdatedAt, } + + // Add unit if available + if entity.Ingredient.Unit != nil { + symbol := "" + if entity.Ingredient.Unit.Abbreviation != nil { + symbol = *entity.Ingredient.Unit.Abbreviation + } + response.Ingredient.Unit = &contract.ProductRecipeUnitResponse{ + ID: entity.Ingredient.Unit.ID, + Name: entity.Ingredient.Unit.Name, + Symbol: symbol, + CreatedAt: entity.Ingredient.Unit.CreatedAt, + UpdatedAt: entity.Ingredient.Unit.UpdatedAt, + } + } } return response -} +} \ No newline at end of file diff --git a/internal/processor/repository_interfaces.go b/internal/processor/repository_interfaces.go index 573b282..3a9acc4 100644 --- a/internal/processor/repository_interfaces.go +++ b/internal/processor/repository_interfaces.go @@ -56,12 +56,12 @@ type OrderIngredientTransactionRepository interface { BulkCreate(ctx context.Context, transactions []*entities.OrderIngredientTransaction) error } -type ProductIngredientRepository interface { - Create(ctx context.Context, productIngredient *entities.ProductIngredient) error - GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.ProductIngredient, error) - GetByProductID(ctx context.Context, productID, organizationID uuid.UUID) ([]*entities.ProductIngredient, error) - GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.ProductIngredient, error) - Update(ctx context.Context, productIngredient *entities.ProductIngredient) error +type ProductRecipeRepository interface { + Create(ctx context.Context, productRecipe *entities.ProductRecipe) error + GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.ProductRecipe, error) + GetByProductID(ctx context.Context, productID, organizationID uuid.UUID) ([]*entities.ProductRecipe, error) + GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.ProductRecipe, error) + Update(ctx context.Context, productRecipe *entities.ProductRecipe) error Delete(ctx context.Context, id, organizationID uuid.UUID) error DeleteByProductID(ctx context.Context, productID, organizationID uuid.UUID) error } diff --git a/internal/service/order_service.go b/internal/service/order_service.go index b206931..e077742 100644 --- a/internal/service/order_service.go +++ b/internal/service/order_service.go @@ -35,17 +35,17 @@ type OrderServiceImpl struct { tableRepo repository.TableRepositoryInterface orderIngredientTransactionService *OrderIngredientTransactionService orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor - productIngredientRepo repository.ProductIngredientRepository + productRecipeRepo repository.ProductRecipeRepository txManager *repository.TxManager } -func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repository.TableRepositoryInterface, orderIngredientTransactionService *OrderIngredientTransactionService, orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor, productIngredientRepo repository.ProductIngredientRepository, txManager *repository.TxManager) *OrderServiceImpl { +func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repository.TableRepositoryInterface, orderIngredientTransactionService *OrderIngredientTransactionService, orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor, productRecipeRepo repository.ProductRecipeRepository, txManager *repository.TxManager) *OrderServiceImpl { return &OrderServiceImpl{ orderProcessor: orderProcessor, tableRepo: tableRepo, orderIngredientTransactionService: orderIngredientTransactionService, orderIngredientTransactionProcessor: orderIngredientTransactionProcessor, - productIngredientRepo: productIngredientRepo, + productRecipeRepo: productRecipeRepo, txManager: txManager, } } @@ -112,18 +112,18 @@ func (s *OrderServiceImpl) createIngredientTransactions(ctx context.Context, ord var allTransactions []*contract.CreateOrderIngredientTransactionRequest for _, orderItem := range orderItems { - // Get product ingredients for this product - productIngredients, err := s.productIngredientRepo.GetByProductID(ctx, orderItem.ProductID, organizationID) + // Get product recipes for this product + productRecipes, err := s.productRecipeRepo.GetByProductID(ctx, orderItem.ProductID, organizationID) if err != nil { - return nil, fmt.Errorf("failed to get product ingredients for product %s: %w", orderItem.ProductID, err) + return nil, fmt.Errorf("failed to get product recipes for product %s: %w", orderItem.ProductID, err) } - if len(productIngredients) == 0 { - continue // Skip if no ingredients + if len(productRecipes) == 0 { + continue // Skip if no recipes } // Calculate waste quantities - transactions, err := s.calculateWasteQuantities(productIngredients, float64(orderItem.Quantity)) + transactions, err := s.calculateWasteQuantities(productRecipes, float64(orderItem.Quantity)) if err != nil { return nil, fmt.Errorf("failed to calculate waste quantities for product %s: %w", err) } @@ -646,17 +646,17 @@ func (s *OrderServiceImpl) handleTableReleaseOnVoid(ctx context.Context, orderID func (s *OrderServiceImpl) createOrderIngredientTransactions(ctx context.Context, order *models.Order, orderItems []*models.OrderItem) error { for _, orderItem := range orderItems { - productIngredients, err := s.productIngredientRepo.GetByProductID(ctx, orderItem.ProductID, order.OrganizationID) + productRecipes, err := s.productRecipeRepo.GetByProductID(ctx, orderItem.ProductID, order.OrganizationID) if err != nil { - return fmt.Errorf("failed to get product ingredients for product %s: %w", orderItem.ProductID, err) + return fmt.Errorf("failed to get product recipes for product %s: %w", orderItem.ProductID, err) } - if len(productIngredients) == 0 { - continue // Skip if no ingredients + if len(productRecipes) == 0 { + continue // Skip if no recipes } // Calculate waste quantities using the utility function - transactions, err := s.calculateWasteQuantities(productIngredients, float64(orderItem.Quantity)) + transactions, err := s.calculateWasteQuantities(productRecipes, float64(orderItem.Quantity)) if err != nil { return fmt.Errorf("failed to calculate waste quantities for product %s: %w", orderItem.ProductID, err) } @@ -681,20 +681,20 @@ func (s *OrderServiceImpl) createOrderIngredientTransactions(ctx context.Context return nil } -// calculateWasteQuantities calculates gross, net, and waste quantities for product ingredients -func (s *OrderServiceImpl) calculateWasteQuantities(productIngredients []*entities.ProductIngredient, quantity float64) ([]*contract.CreateOrderIngredientTransactionRequest, error) { - if len(productIngredients) == 0 { +// calculateWasteQuantities calculates gross, net, and waste quantities for product recipes +func (s *OrderServiceImpl) calculateWasteQuantities(productRecipes []*entities.ProductRecipe, quantity float64) ([]*contract.CreateOrderIngredientTransactionRequest, error) { + if len(productRecipes) == 0 { return []*contract.CreateOrderIngredientTransactionRequest{}, nil } - transactions := make([]*contract.CreateOrderIngredientTransactionRequest, 0, len(productIngredients)) + transactions := make([]*contract.CreateOrderIngredientTransactionRequest, 0, len(productRecipes)) - for _, pi := range productIngredients { + for _, pr := range productRecipes { // Calculate net quantity (actual quantity needed for the product) - netQty := pi.Quantity * quantity + netQty := pr.Quantity * quantity // Calculate gross quantity (including waste) - wasteMultiplier := 1 + (pi.WastePercentage / 100) + wasteMultiplier := 1 + (pr.WastePercentage / 100) grossQty := netQty * wasteMultiplier // Calculate waste quantity @@ -702,12 +702,12 @@ func (s *OrderServiceImpl) calculateWasteQuantities(productIngredients []*entiti // Get unit name from ingredient unitName := "unit" // default - if pi.Ingredient != nil && pi.Ingredient.Unit != nil { - unitName = pi.Ingredient.Unit.Name + if pr.Ingredient != nil && pr.Ingredient.Unit != nil { + unitName = pr.Ingredient.Unit.Name } transaction := &contract.CreateOrderIngredientTransactionRequest{ - IngredientID: pi.IngredientID, + IngredientID: pr.IngredientID, GrossQty: util.RoundToDecimalPlaces(grossQty, 3), NetQty: util.RoundToDecimalPlaces(netQty, 3), WasteQty: util.RoundToDecimalPlaces(wasteQty, 3), diff --git a/internal/service/product_recipe_service.go b/internal/service/product_recipe_service.go index 63ef4f8..bfaf76d 100644 --- a/internal/service/product_recipe_service.go +++ b/internal/service/product_recipe_service.go @@ -1,22 +1,22 @@ package service import ( - "apskel-pos-be/internal/models" + "apskel-pos-be/internal/contract" "apskel-pos-be/internal/processor" "context" + "fmt" "github.com/google/uuid" ) type ProductRecipeService interface { - Create(ctx context.Context, organizationID uuid.UUID, req *models.CreateProductRecipeRequest) (*models.ProductRecipeResponse, error) - GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) - GetByProductID(ctx context.Context, productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) - GetByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) - GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) - Update(ctx context.Context, id uuid.UUID, organizationID uuid.UUID, req *models.UpdateProductRecipeRequest) (*models.ProductRecipeResponse, error) + Create(ctx context.Context, organizationID uuid.UUID, req *contract.CreateProductRecipeRequest) (*contract.ProductRecipeResponse, error) + GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*contract.ProductRecipeResponse, error) + GetByProductID(ctx context.Context, req *contract.GetProductRecipeByProductIDRequest, organizationID uuid.UUID) ([]*contract.ProductRecipeResponse, error) + GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*contract.ProductRecipeResponse, error) + Update(ctx context.Context, id uuid.UUID, organizationID uuid.UUID, req *contract.UpdateProductRecipeRequest) (*contract.ProductRecipeResponse, error) Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error - BulkCreate(ctx context.Context, organizationID uuid.UUID, recipes []models.CreateProductRecipeRequest) ([]*models.ProductRecipeResponse, error) + BulkCreate(ctx context.Context, organizationID uuid.UUID, req *contract.BulkCreateProductRecipeRequest) ([]*contract.ProductRecipeResponse, error) } type ProductRecipeServiceImpl struct { @@ -29,34 +29,86 @@ func NewProductRecipeService(processor processor.ProductRecipeProcessor) *Produc } } -func (s *ProductRecipeServiceImpl) Create(ctx context.Context, organizationID uuid.UUID, req *models.CreateProductRecipeRequest) (*models.ProductRecipeResponse, error) { +func (s *ProductRecipeServiceImpl) Create(ctx context.Context, organizationID uuid.UUID, req *contract.CreateProductRecipeRequest) (*contract.ProductRecipeResponse, error) { + // Validate request + if req == nil { + return nil, fmt.Errorf("request cannot be nil") + } + + // Call processor to handle business logic return s.processor.Create(ctx, req, organizationID) } -func (s *ProductRecipeServiceImpl) GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) { +func (s *ProductRecipeServiceImpl) GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*contract.ProductRecipeResponse, error) { + // Validate ID + if id == uuid.Nil { + return nil, fmt.Errorf("invalid recipe ID") + } + return s.processor.GetByID(ctx, id, organizationID) } -func (s *ProductRecipeServiceImpl) GetByProductID(ctx context.Context, productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) { - return s.processor.GetByProductID(ctx, productID, organizationID) +func (s *ProductRecipeServiceImpl) GetByProductID(ctx context.Context, req *contract.GetProductRecipeByProductIDRequest, organizationID uuid.UUID) ([]*contract.ProductRecipeResponse, error) { + // Validate request + if req == nil { + return nil, fmt.Errorf("request cannot be nil") + } + + // Validate product ID + if req.ProductID == uuid.Nil { + return nil, fmt.Errorf("invalid product ID") + } + + // If variant ID is provided, get by product and variant + if req.VariantID != nil { + return s.processor.GetByProductAndVariantID(ctx, req.ProductID, req.VariantID, organizationID) + } + + // Otherwise get by product ID only + return s.processor.GetByProductID(ctx, req.ProductID, organizationID) } -func (s *ProductRecipeServiceImpl) GetByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) { - return s.processor.GetByProductAndVariantID(ctx, productID, variantID, organizationID) -} +func (s *ProductRecipeServiceImpl) GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*contract.ProductRecipeResponse, error) { + // Validate ingredient ID + if ingredientID == uuid.Nil { + return nil, fmt.Errorf("invalid ingredient ID") + } -func (s *ProductRecipeServiceImpl) GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) { return s.processor.GetByIngredientID(ctx, ingredientID, organizationID) } -func (s *ProductRecipeServiceImpl) Update(ctx context.Context, id uuid.UUID, organizationID uuid.UUID, req *models.UpdateProductRecipeRequest) (*models.ProductRecipeResponse, error) { +func (s *ProductRecipeServiceImpl) Update(ctx context.Context, id uuid.UUID, organizationID uuid.UUID, req *contract.UpdateProductRecipeRequest) (*contract.ProductRecipeResponse, error) { + // Validate ID + if id == uuid.Nil { + return nil, fmt.Errorf("invalid recipe ID") + } + + // Validate request + if req == nil { + return nil, fmt.Errorf("request cannot be nil") + } + return s.processor.Update(ctx, id, req, organizationID) } func (s *ProductRecipeServiceImpl) Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error { + // Validate ID + if id == uuid.Nil { + return fmt.Errorf("invalid recipe ID") + } + return s.processor.Delete(ctx, id, organizationID) } -func (s *ProductRecipeServiceImpl) BulkCreate(ctx context.Context, organizationID uuid.UUID, recipes []models.CreateProductRecipeRequest) ([]*models.ProductRecipeResponse, error) { - return s.processor.BulkCreate(ctx, recipes, organizationID) -} +func (s *ProductRecipeServiceImpl) BulkCreate(ctx context.Context, organizationID uuid.UUID, req *contract.BulkCreateProductRecipeRequest) ([]*contract.ProductRecipeResponse, error) { + // Validate request + if req == nil { + return nil, fmt.Errorf("request cannot be nil") + } + + if len(req.Recipes) == 0 { + return nil, fmt.Errorf("at least one recipe is required") + } + + return s.processor.BulkCreate(ctx, req.Recipes, organizationID) +} \ No newline at end of file diff --git a/internal/service/purchase_order_service.go b/internal/service/purchase_order_service.go index db84a76..4c9df39 100644 --- a/internal/service/purchase_order_service.go +++ b/internal/service/purchase_order_service.go @@ -34,7 +34,11 @@ func NewPurchaseOrderService(purchaseOrderProcessor processor.PurchaseOrderProce } func (s *PurchaseOrderServiceImpl) CreatePurchaseOrder(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreatePurchaseOrderRequest) *contract.Response { - modelReq := transformer.CreatePurchaseOrderRequestToModel(req) + modelReq, err := transformer.CreatePurchaseOrderRequestToModel(req) + if err != nil { + errorResp := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.PurchaseOrderServiceEntity, "Invalid date format. Use YYYY-MM-DD format") + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } poResponse, err := s.purchaseOrderProcessor.CreatePurchaseOrder(ctx, apctx.OrganizationID, modelReq) if err != nil { @@ -47,7 +51,11 @@ func (s *PurchaseOrderServiceImpl) CreatePurchaseOrder(ctx context.Context, apct } func (s *PurchaseOrderServiceImpl) UpdatePurchaseOrder(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdatePurchaseOrderRequest) *contract.Response { - modelReq := transformer.UpdatePurchaseOrderRequestToModel(req) + modelReq, err := transformer.UpdatePurchaseOrderRequestToModel(req) + if err != nil { + errorResp := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.PurchaseOrderServiceEntity, "Invalid date format. Use YYYY-MM-DD format") + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } poResponse, err := s.purchaseOrderProcessor.UpdatePurchaseOrder(ctx, id, apctx.OrganizationID, modelReq) if err != nil { diff --git a/internal/transformer/purchase_order_transformer.go b/internal/transformer/purchase_order_transformer.go index 8164176..a867df0 100644 --- a/internal/transformer/purchase_order_transformer.go +++ b/internal/transformer/purchase_order_transformer.go @@ -3,10 +3,11 @@ package transformer import ( "apskel-pos-be/internal/contract" "apskel-pos-be/internal/models" + "time" ) // Contract to Model conversions -func CreatePurchaseOrderRequestToModel(req *contract.CreatePurchaseOrderRequest) *models.CreatePurchaseOrderRequest { +func CreatePurchaseOrderRequestToModel(req *contract.CreatePurchaseOrderRequest) (*models.CreatePurchaseOrderRequest, error) { items := make([]models.CreatePurchaseOrderItemRequest, len(req.Items)) for i, item := range req.Items { items[i] = models.CreatePurchaseOrderItemRequest{ @@ -18,20 +19,32 @@ func CreatePurchaseOrderRequestToModel(req *contract.CreatePurchaseOrderRequest) } } + // Parse transaction date + transactionDate, err := time.Parse("2006-01-02", req.TransactionDate) + if err != nil { + return nil, err + } + + // Parse due date + dueDate, err := time.Parse("2006-01-02", req.DueDate) + if err != nil { + return nil, err + } + return &models.CreatePurchaseOrderRequest{ VendorID: req.VendorID, PONumber: req.PONumber, - TransactionDate: req.TransactionDate, - DueDate: req.DueDate, + TransactionDate: transactionDate, + DueDate: dueDate, Reference: req.Reference, Status: req.Status, Message: req.Message, Items: items, AttachmentFileIDs: req.AttachmentFileIDs, - } + }, nil } -func UpdatePurchaseOrderRequestToModel(req *contract.UpdatePurchaseOrderRequest) *models.UpdatePurchaseOrderRequest { +func UpdatePurchaseOrderRequestToModel(req *contract.UpdatePurchaseOrderRequest) (*models.UpdatePurchaseOrderRequest, error) { var items []models.UpdatePurchaseOrderItemRequest if req.Items != nil { items = make([]models.UpdatePurchaseOrderItemRequest, len(req.Items)) @@ -47,17 +60,37 @@ func UpdatePurchaseOrderRequestToModel(req *contract.UpdatePurchaseOrderRequest) } } + // Parse transaction date if provided + var transactionDate *time.Time + if req.TransactionDate != nil && *req.TransactionDate != "" { + parsedDate, err := time.Parse("2006-01-02", *req.TransactionDate) + if err != nil { + return nil, err + } + transactionDate = &parsedDate + } + + // Parse due date if provided + var dueDate *time.Time + if req.DueDate != nil && *req.DueDate != "" { + parsedDate, err := time.Parse("2006-01-02", *req.DueDate) + if err != nil { + return nil, err + } + dueDate = &parsedDate + } + return &models.UpdatePurchaseOrderRequest{ VendorID: req.VendorID, PONumber: req.PONumber, - TransactionDate: req.TransactionDate, - DueDate: req.DueDate, + TransactionDate: transactionDate, + DueDate: dueDate, Reference: req.Reference, Status: req.Status, Message: req.Message, Items: items, AttachmentFileIDs: req.AttachmentFileIDs, - } + }, nil } func ListPurchaseOrdersRequestToModel(req *contract.ListPurchaseOrdersRequest) *models.ListPurchaseOrdersRequest { diff --git a/internal/validator/purchase_order_validator.go b/internal/validator/purchase_order_validator.go index eedd287..f6706fa 100644 --- a/internal/validator/purchase_order_validator.go +++ b/internal/validator/purchase_order_validator.go @@ -3,6 +3,7 @@ package validator import ( "errors" "strings" + "time" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" @@ -37,15 +38,26 @@ func (v *PurchaseOrderValidatorImpl) ValidateCreatePurchaseOrderRequest(req *con return errors.New("po_number must be between 1 and 50 characters"), constants.MalformedFieldErrorCode } - if req.TransactionDate.IsZero() { + // Validate transaction date + if strings.TrimSpace(req.TransactionDate) == "" { return errors.New("transaction_date is required"), constants.MissingFieldErrorCode } - - if req.DueDate.IsZero() { - return errors.New("due_date is required"), constants.MissingFieldErrorCode + transactionDate, err := time.Parse("2006-01-02", req.TransactionDate) + if err != nil { + return errors.New("transaction_date must be in YYYY-MM-DD format"), constants.MalformedFieldErrorCode } - if req.DueDate.Before(req.TransactionDate) { + // Validate due date + if strings.TrimSpace(req.DueDate) == "" { + return errors.New("due_date is required"), constants.MissingFieldErrorCode + } + dueDate, err := time.Parse("2006-01-02", req.DueDate) + if err != nil { + return errors.New("due_date must be in YYYY-MM-DD format"), constants.MalformedFieldErrorCode + } + + // Check if due date is after transaction date + if dueDate.Before(transactionDate) { return errors.New("due_date must be after transaction_date"), constants.MalformedFieldErrorCode } @@ -88,9 +100,22 @@ func (v *PurchaseOrderValidatorImpl) ValidateUpdatePurchaseOrderRequest(req *con } } + // Validate dates if both are provided if req.TransactionDate != nil && req.DueDate != nil { - if req.DueDate.Before(*req.TransactionDate) { - return errors.New("due_date must be after transaction_date"), constants.MalformedFieldErrorCode + if *req.TransactionDate != "" && *req.DueDate != "" { + transactionDate, err := time.Parse("2006-01-02", *req.TransactionDate) + if err != nil { + return errors.New("transaction_date must be in YYYY-MM-DD format"), constants.MalformedFieldErrorCode + } + + dueDate, err := time.Parse("2006-01-02", *req.DueDate) + if err != nil { + return errors.New("due_date must be in YYYY-MM-DD format"), constants.MalformedFieldErrorCode + } + + if dueDate.Before(transactionDate) { + return errors.New("due_date must be after transaction_date"), constants.MalformedFieldErrorCode + } } } diff --git a/migrations/000047_add_waste_percentage_to_product_recipes.down.sql b/migrations/000047_add_waste_percentage_to_product_recipes.down.sql new file mode 100644 index 0000000..26e768a --- /dev/null +++ b/migrations/000047_add_waste_percentage_to_product_recipes.down.sql @@ -0,0 +1,2 @@ +-- Remove waste_percentage column from product_recipes table +ALTER TABLE product_recipes DROP COLUMN waste_percentage; \ No newline at end of file diff --git a/migrations/000047_add_waste_percentage_to_product_recipes.up.sql b/migrations/000047_add_waste_percentage_to_product_recipes.up.sql new file mode 100644 index 0000000..d983d91 --- /dev/null +++ b/migrations/000047_add_waste_percentage_to_product_recipes.up.sql @@ -0,0 +1,5 @@ +-- Add waste_percentage column to product_recipes table +ALTER TABLE product_recipes +ADD COLUMN waste_percentage DECIMAL(5,2) DEFAULT 0.00 CHECK (waste_percentage >= 0 AND waste_percentage <= 100); + +COMMENT ON COLUMN product_recipes.waste_percentage IS 'Waste percentage for this ingredient (0-100). Used to calculate gross quantity needed including waste.'; \ No newline at end of file