From 91960f0e57818becaa55910beb674bd1fa511af6 Mon Sep 17 00:00:00 2001 From: Efril Date: Thu, 21 May 2026 21:27:57 +0700 Subject: [PATCH] categories add outlet id --- internal/contract/category_contract.go | 10 +++++--- internal/entities/category.go | 19 ++++++++------- internal/handler/category_handler.go | 18 +++++++++++++- internal/mappers/category_mapper.go | 24 ++++++++++++------- internal/models/category.go | 18 ++++++++------ internal/service/category_service.go | 3 +++ internal/transformer/category_transformer.go | 13 +++++++--- ...00069_add_outlet_id_to_categories.down.sql | 5 ++++ .../000069_add_outlet_id_to_categories.up.sql | 6 +++++ 9 files changed, 84 insertions(+), 32 deletions(-) create mode 100644 migrations/000069_add_outlet_id_to_categories.down.sql create mode 100644 migrations/000069_add_outlet_id_to_categories.up.sql diff --git a/internal/contract/category_contract.go b/internal/contract/category_contract.go index 5cc9c71..7c74e82 100644 --- a/internal/contract/category_contract.go +++ b/internal/contract/category_contract.go @@ -10,7 +10,8 @@ type CreateCategoryRequest struct { Name string `json:"name" validate:"required,min=1,max=255"` Description *string `json:"description,omitempty"` BusinessType *string `json:"business_type,omitempty"` - Order *int `json:"order,omitempty"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` + Order *int `json:"order,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` } @@ -18,12 +19,14 @@ type UpdateCategoryRequest struct { Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` Description *string `json:"description,omitempty"` BusinessType *string `json:"business_type,omitempty"` - Order *int `json:"order,omitempty"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` + Order *int `json:"order,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` } type ListCategoriesRequest struct { OrganizationID *uuid.UUID `json:"organization_id,omitempty"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` BusinessType string `json:"business_type,omitempty"` Search string `json:"search,omitempty"` Page int `json:"page" validate:"required,min=1"` @@ -34,10 +37,11 @@ type ListCategoriesRequest struct { type CategoryResponse struct { ID uuid.UUID `json:"id"` OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` Name string `json:"name"` Description *string `json:"description"` BusinessType string `json:"business_type"` - Order int `json:"order"` + Order int `json:"order"` Metadata map[string]interface{} `json:"metadata"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` diff --git a/internal/entities/category.go b/internal/entities/category.go index c5accc6..8cb00e1 100644 --- a/internal/entities/category.go +++ b/internal/entities/category.go @@ -31,15 +31,16 @@ func (m *Metadata) Scan(value interface{}) error { } type Category 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" validate:"required"` - Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"` - Description *string `gorm:"type:text" json:"description"` - Order int `gorm:"default:0" json:"order"` - BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"` - Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` - 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" validate:"required"` + OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"` + Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"` + Description *string `gorm:"type:text" json:"description"` + Order int `gorm:"default:0" json:"order"` + BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"` + Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` Products []Product `gorm:"foreignKey:CategoryID" json:"products,omitempty"` diff --git a/internal/handler/category_handler.go b/internal/handler/category_handler.go index ddfa8a3..36ea3c4 100644 --- a/internal/handler/category_handler.go +++ b/internal/handler/category_handler.go @@ -36,7 +36,7 @@ func (h *CategoryHandler) CreateCategory(c *gin.Context) { contextInfo := appcontext.FromGinContext(ctx) var req contract.CreateCategoryRequest - fmt.Printf("CategoryHandler::CreateCategory -> Request: %+v\n", req) + fmt.Printf("CategoryHandler::CreateCategory -> Request: %+v\n", req) if err := c.ShouldBindJSON(&req); err != nil { logger.FromContext(c.Request.Context()).WithError(err).Error("CategoryHandler::CreateCategory -> request binding failed") validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) @@ -44,6 +44,11 @@ func (h *CategoryHandler) CreateCategory(c *gin.Context) { return } + // Inject outlet_id from context if user has one and request doesn't provide it + if req.OutletID == nil && contextInfo.OutletID != uuid.Nil { + req.OutletID = &contextInfo.OutletID + } + validationError, validationErrorCode := h.categoryValidator.ValidateCreateCategoryRequest(&req) if validationError != nil { validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) @@ -149,6 +154,11 @@ func (h *CategoryHandler) ListCategories(c *gin.Context) { OrganizationID: &contextInfo.OrganizationID, } + // Inject outlet_id from context if user has one + if contextInfo.OutletID != uuid.Nil { + req.OutletID = &contextInfo.OutletID + } + // Parse query parameters if pageStr := c.Query("page"); pageStr != "" { if page, err := strconv.Atoi(pageStr); err == nil { @@ -176,6 +186,12 @@ func (h *CategoryHandler) ListCategories(c *gin.Context) { } } + if outletIDStr := c.Query("outlet_id"); outletIDStr != "" { + if outletID, err := uuid.Parse(outletIDStr); err == nil { + req.OutletID = &outletID + } + } + validationError, validationErrorCode := h.categoryValidator.ValidateListCategoriesRequest(req) if validationError != nil { logger.FromContext(ctx).WithError(validationError).Error("CategoryHandler::ListCategories -> request validation failed") diff --git a/internal/mappers/category_mapper.go b/internal/mappers/category_mapper.go index 40e8187..71fa0aa 100644 --- a/internal/mappers/category_mapper.go +++ b/internal/mappers/category_mapper.go @@ -13,11 +13,12 @@ func CategoryEntityToModel(entity *entities.Category) *models.Category { return &models.Category{ ID: entity.ID, OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, Name: entity.Name, Description: entity.Description, - ImageURL: nil, // Entity doesn't have ImageURL, model does - Order: entity.Order, // Entity doesn't have SortOrder, model does - IsActive: true, // Entity doesn't have IsActive, default to true + ImageURL: nil, + Order: entity.Order, + IsActive: true, CreatedAt: entity.CreatedAt, UpdatedAt: entity.UpdatedAt, } @@ -32,14 +33,14 @@ func CategoryModelToEntity(model *models.Category) *entities.Category { if model.ImageURL != nil { metadata["image_url"] = *model.ImageURL } - // metadata["sort_order"] = model.SortOrder return &entities.Category{ ID: model.ID, OrganizationID: model.OrganizationID, + OutletID: model.OutletID, Name: model.Name, Description: model.Description, - BusinessType: "restaurant", // Default business type + BusinessType: "restaurant", Order: model.Order, Metadata: metadata, CreatedAt: model.CreatedAt, @@ -56,14 +57,14 @@ func CreateCategoryRequestToEntity(req *models.CreateCategoryRequest) *entities. if req.ImageURL != nil { metadata["image_url"] = *req.ImageURL } - // metadata["sort_order"] = req.SortOrder return &entities.Category{ OrganizationID: req.OrganizationID, + OutletID: req.OutletID, Name: req.Name, Description: req.Description, Order: req.Order, - BusinessType: "restaurant", // Default business type + BusinessType: "restaurant", Metadata: metadata, } } @@ -87,11 +88,12 @@ func CategoryEntityToResponse(entity *entities.Category) *models.CategoryRespons return &models.CategoryResponse{ ID: entity.ID, OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, Name: entity.Name, Description: entity.Description, ImageURL: imageURL, - Order: entity.Order, - IsActive: true, // Default to true since entity doesn't have this field + Order: entity.Order, + IsActive: true, CreatedAt: entity.CreatedAt, UpdatedAt: entity.UpdatedAt, } @@ -121,6 +123,10 @@ func UpdateCategoryEntityFromRequest(entity *entities.Category, req *models.Upda if req.Order != nil { entity.Order = *req.Order } + + if req.OutletID != nil { + entity.OutletID = req.OutletID + } } func CategoryEntitiesToModels(entities []*entities.Category) []*models.Category { diff --git a/internal/models/category.go b/internal/models/category.go index eff397a..443083b 100644 --- a/internal/models/category.go +++ b/internal/models/category.go @@ -9,10 +9,11 @@ import ( type Category struct { ID uuid.UUID OrganizationID uuid.UUID + OutletID *uuid.UUID Name string Description *string ImageURL *string - Order int + Order int IsActive bool CreatedAt time.Time UpdatedAt time.Time @@ -20,27 +21,30 @@ type Category struct { type CreateCategoryRequest struct { OrganizationID uuid.UUID `validate:"required"` - Name string `validate:"required,min=1,max=255"` - Description *string `validate:"omitempty,max=1000"` - ImageURL *string `validate:"omitempty,url"` - Order int `validate:"min=0"` + OutletID *uuid.UUID + Name string `validate:"required,min=1,max=255"` + Description *string `validate:"omitempty,max=1000"` + ImageURL *string `validate:"omitempty,url"` + Order int `validate:"min=0"` } type UpdateCategoryRequest struct { Name *string `validate:"omitempty,min=1,max=255"` Description *string `validate:"omitempty,max=1000"` ImageURL *string `validate:"omitempty,url"` - Order *int `validate:"omitempty,min=0"` + OutletID *uuid.UUID + Order *int `validate:"omitempty,min=0"` IsActive *bool } type CategoryResponse struct { ID uuid.UUID OrganizationID uuid.UUID + OutletID *uuid.UUID Name string Description *string ImageURL *string - Order int + Order int IsActive bool CreatedAt time.Time UpdatedAt time.Time diff --git a/internal/service/category_service.go b/internal/service/category_service.go index cfec8aa..5ee4c58 100644 --- a/internal/service/category_service.go +++ b/internal/service/category_service.go @@ -85,6 +85,9 @@ func (s *CategoryServiceImpl) ListCategories(ctx context.Context, req *contract. if req.OrganizationID != nil { filters["organization_id"] = *req.OrganizationID } + if req.OutletID != nil { + filters["outlet_id"] = *req.OutletID + } if req.BusinessType != "" { filters["business_type"] = req.BusinessType } diff --git a/internal/transformer/category_transformer.go b/internal/transformer/category_transformer.go index 962eee6..b2dbc1e 100644 --- a/internal/transformer/category_transformer.go +++ b/internal/transformer/category_transformer.go @@ -7,12 +7,17 @@ import ( ) func CreateCategoryRequestToModel(apctx *appcontext.ContextInfo, req *contract.CreateCategoryRequest) *models.CreateCategoryRequest { + order := 0 + if req.Order != nil { + order = *req.Order + } return &models.CreateCategoryRequest{ OrganizationID: apctx.OrganizationID, + OutletID: req.OutletID, Name: req.Name, Description: req.Description, ImageURL: nil, - Order: *req.Order, + Order: order, } } @@ -21,7 +26,8 @@ func UpdateCategoryRequestToModel(req *contract.UpdateCategoryRequest) *models.U Name: req.Name, Description: req.Description, ImageURL: nil, - Order: req.Order, + OutletID: req.OutletID, + Order: req.Order, IsActive: nil, } } @@ -34,9 +40,10 @@ func CategoryModelResponseToResponse(cat *models.CategoryResponse) *contract.Cat return &contract.CategoryResponse{ ID: cat.ID, OrganizationID: cat.OrganizationID, + OutletID: cat.OutletID, Name: cat.Name, Description: cat.Description, - BusinessType: "restaurant", // Default business type + BusinessType: "restaurant", Order: cat.Order, Metadata: map[string]interface{}{}, CreatedAt: cat.CreatedAt, diff --git a/migrations/000069_add_outlet_id_to_categories.down.sql b/migrations/000069_add_outlet_id_to_categories.down.sql new file mode 100644 index 0000000..98daf75 --- /dev/null +++ b/migrations/000069_add_outlet_id_to_categories.down.sql @@ -0,0 +1,5 @@ +-- Remove outlet_id column from categories table +DROP INDEX IF EXISTS idx_categories_outlet_id; + +ALTER TABLE categories + DROP COLUMN IF EXISTS outlet_id; diff --git a/migrations/000069_add_outlet_id_to_categories.up.sql b/migrations/000069_add_outlet_id_to_categories.up.sql new file mode 100644 index 0000000..47457c8 --- /dev/null +++ b/migrations/000069_add_outlet_id_to_categories.up.sql @@ -0,0 +1,6 @@ +-- Add outlet_id column to categories table (nullable) +ALTER TABLE categories + ADD COLUMN outlet_id UUID REFERENCES outlets(id) ON DELETE SET NULL; + +-- Index for outlet_id filter +CREATE INDEX idx_categories_outlet_id ON categories(outlet_id);