From 72f67cb519ce9649d0e224ce9c0e18f6d70eeebc Mon Sep 17 00:00:00 2001 From: Efril Date: Thu, 21 May 2026 21:20:54 +0700 Subject: [PATCH] create or update product assign to product outlet --- internal/contract/product_contract.go | 11 +++++----- internal/handler/product_handler.go | 3 ++- internal/models/product.go | 2 ++ internal/processor/product_processor.go | 24 +++++++++++++++++++++ internal/service/product_service.go | 6 +++--- internal/transformer/product_transformer.go | 18 +++++++++++++++- 6 files changed, 54 insertions(+), 10 deletions(-) diff --git a/internal/contract/product_contract.go b/internal/contract/product_contract.go index afc2e7b..1fec481 100644 --- a/internal/contract/product_contract.go +++ b/internal/contract/product_contract.go @@ -8,6 +8,7 @@ import ( type CreateProductRequest struct { CategoryID uuid.UUID `json:"category_id" validate:"required"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` SKU *string `json:"sku,omitempty"` Name string `json:"name" validate:"required,min=1,max=255"` Description *string `json:"description,omitempty"` @@ -19,12 +20,13 @@ type CreateProductRequest struct { Metadata map[string]interface{} `json:"metadata,omitempty"` IsActive *bool `json:"is_active,omitempty"` Variants []CreateProductVariantRequest `json:"variants,omitempty"` - InitialStock *int `json:"initial_stock,omitempty" validate:"omitempty,min=0"` // Initial stock quantity for all outlets - ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` // Reorder level for all outlets - CreateInventory bool `json:"create_inventory,omitempty"` // Whether to create inventory records for all outlets + InitialStock *int `json:"initial_stock,omitempty" validate:"omitempty,min=0"` + ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` + CreateInventory bool `json:"create_inventory,omitempty"` } type UpdateProductRequest struct { + OutletID *uuid.UUID `json:"outlet_id,omitempty"` CategoryID *uuid.UUID `json:"category_id,omitempty"` SKU *string `json:"sku,omitempty"` Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` @@ -36,8 +38,7 @@ type UpdateProductRequest struct { PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"` Metadata map[string]interface{} `json:"metadata,omitempty"` IsActive *bool `json:"is_active,omitempty"` - // Stock management fields - ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` // Update reorder level for all existing inventory records + ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` } type CreateProductVariantRequest struct { diff --git a/internal/handler/product_handler.go b/internal/handler/product_handler.go index ad389d9..d514858 100644 --- a/internal/handler/product_handler.go +++ b/internal/handler/product_handler.go @@ -60,6 +60,7 @@ func (h *ProductHandler) CreateProduct(c *gin.Context) { func (h *ProductHandler) UpdateProduct(c *gin.Context) { ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) productIDStr := c.Param("id") productID, err := uuid.Parse(productIDStr) @@ -85,7 +86,7 @@ func (h *ProductHandler) UpdateProduct(c *gin.Context) { return } - productResponse := h.productService.UpdateProduct(ctx, productID, &req) + productResponse := h.productService.UpdateProduct(ctx, contextInfo, productID, &req) if productResponse.HasErrors() { errorResp := productResponse.GetErrors()[0] logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::UpdateProduct -> Failed to update product from service") diff --git a/internal/models/product.go b/internal/models/product.go index beda4c3..77e6b6e 100644 --- a/internal/models/product.go +++ b/internal/models/product.go @@ -40,6 +40,7 @@ type ProductVariant struct { type CreateProductRequest struct { OrganizationID uuid.UUID `validate:"required"` + OutletID uuid.UUID `validate:"omitempty"` // If set, upsert product_outlet_prices on create CategoryID uuid.UUID `validate:"required"` SKU *string `validate:"omitempty,max=100"` Name string `validate:"required,min=1,max=255"` @@ -60,6 +61,7 @@ type CreateProductRequest struct { } type UpdateProductRequest struct { + OutletID uuid.UUID `validate:"omitempty"` // If set, upsert product_outlet_prices on update CategoryID *uuid.UUID `validate:"omitempty"` SKU *string `validate:"omitempty,max=100"` Name *string `validate:"omitempty,min=1,max=255"` diff --git a/internal/processor/product_processor.go b/internal/processor/product_processor.go index 3a9c9ea..db8d560 100644 --- a/internal/processor/product_processor.go +++ b/internal/processor/product_processor.go @@ -122,6 +122,18 @@ func (p *ProductProcessorImpl) CreateProduct(ctx context.Context, req *models.Cr } } + // Upsert outlet-specific price if outlet context is present + if req.OutletID != uuid.Nil { + outletPriceEntity := &entities.ProductOutletPrice{ + ProductID: productEntity.ID, + OutletID: req.OutletID, + Price: req.Price, + } + if err := p.outletPriceRepo.Upsert(ctx, outletPriceEntity); err != nil { + return nil, fmt.Errorf("failed to assign outlet price: %w", err) + } + } + productWithCategory, err := p.productRepo.GetWithCategory(ctx, productEntity.ID) if err != nil { return nil, fmt.Errorf("failed to retrieve created product: %w", err) @@ -183,6 +195,18 @@ func (p *ProductProcessorImpl) UpdateProduct(ctx context.Context, id uuid.UUID, } } + // Upsert outlet-specific price if outlet context is present + if req.OutletID != uuid.Nil && req.Price != nil { + outletPriceEntity := &entities.ProductOutletPrice{ + ProductID: id, + OutletID: req.OutletID, + Price: *req.Price, + } + if err := p.outletPriceRepo.Upsert(ctx, outletPriceEntity); err != nil { + return nil, fmt.Errorf("failed to assign outlet price: %w", err) + } + } + productWithCategory, err := p.productRepo.GetWithCategory(ctx, id) if err != nil { return nil, fmt.Errorf("failed to retrieve updated product: %w", err) diff --git a/internal/service/product_service.go b/internal/service/product_service.go index fce1eb6..86a91ba 100644 --- a/internal/service/product_service.go +++ b/internal/service/product_service.go @@ -14,7 +14,7 @@ import ( type ProductService interface { CreateProduct(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateProductRequest) *contract.Response - UpdateProduct(ctx context.Context, id uuid.UUID, req *contract.UpdateProductRequest) *contract.Response + UpdateProduct(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdateProductRequest) *contract.Response DeleteProduct(ctx context.Context, id uuid.UUID) *contract.Response GetProductByID(ctx context.Context, id uuid.UUID, outletID uuid.UUID) *contract.Response ListProducts(ctx context.Context, req *contract.ListProductsRequest) *contract.Response @@ -44,8 +44,8 @@ func (s *ProductServiceImpl) CreateProduct(ctx context.Context, apctx *appcontex return contract.BuildSuccessResponse(contractResponse) } -func (s *ProductServiceImpl) UpdateProduct(ctx context.Context, id uuid.UUID, req *contract.UpdateProductRequest) *contract.Response { - modelReq := transformer.UpdateProductRequestToModel(req) +func (s *ProductServiceImpl) UpdateProduct(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdateProductRequest) *contract.Response { + modelReq := transformer.UpdateProductRequestToModel(apctx, req) productResponse, err := s.productProcessor.UpdateProduct(ctx, id, modelReq) if err != nil { diff --git a/internal/transformer/product_transformer.go b/internal/transformer/product_transformer.go index be2b9fd..47c309f 100644 --- a/internal/transformer/product_transformer.go +++ b/internal/transformer/product_transformer.go @@ -5,6 +5,8 @@ import ( "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" "apskel-pos-be/internal/models" + + "github.com/google/uuid" ) func CreateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.CreateProductRequest) *models.CreateProductRequest { @@ -37,8 +39,15 @@ func CreateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.Cr metadata = make(map[string]interface{}) } + // Prioritize outlet_id from context, fallback to request body + outletID := apctx.OutletID + if outletID == uuid.Nil && req.OutletID != nil { + outletID = *req.OutletID + } + return &models.CreateProductRequest{ OrganizationID: apctx.OrganizationID, + OutletID: outletID, CategoryID: req.CategoryID, SKU: req.SKU, Name: req.Name, @@ -53,13 +62,20 @@ func CreateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.Cr } } -func UpdateProductRequestToModel(req *contract.UpdateProductRequest) *models.UpdateProductRequest { +func UpdateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.UpdateProductRequest) *models.UpdateProductRequest { metadata := req.Metadata if metadata == nil { metadata = make(map[string]interface{}) } + // Prioritize outlet_id from context, fallback to request body + outletID := apctx.OutletID + if outletID == uuid.Nil && req.OutletID != nil { + outletID = *req.OutletID + } + return &models.UpdateProductRequest{ + OutletID: outletID, CategoryID: req.CategoryID, SKU: req.SKU, Name: req.Name,