categories add outlet id

This commit is contained in:
Efril 2026-05-21 21:27:57 +07:00
parent 72f67cb519
commit 91960f0e57
9 changed files with 84 additions and 32 deletions

View File

@ -10,7 +10,8 @@ type CreateCategoryRequest struct {
Name string `json:"name" validate:"required,min=1,max=255"` Name string `json:"name" validate:"required,min=1,max=255"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
BusinessType *string `json:"business_type,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"` 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"` Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
BusinessType *string `json:"business_type,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"` Metadata map[string]interface{} `json:"metadata,omitempty"`
} }
type ListCategoriesRequest struct { type ListCategoriesRequest struct {
OrganizationID *uuid.UUID `json:"organization_id,omitempty"` OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
BusinessType string `json:"business_type,omitempty"` BusinessType string `json:"business_type,omitempty"`
Search string `json:"search,omitempty"` Search string `json:"search,omitempty"`
Page int `json:"page" validate:"required,min=1"` Page int `json:"page" validate:"required,min=1"`
@ -34,10 +37,11 @@ type ListCategoriesRequest struct {
type CategoryResponse struct { type CategoryResponse struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
BusinessType string `json:"business_type"` BusinessType string `json:"business_type"`
Order int `json:"order"` Order int `json:"order"`
Metadata map[string]interface{} `json:"metadata"` Metadata map[string]interface{} `json:"metadata"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`

View File

@ -31,15 +31,16 @@ func (m *Metadata) Scan(value interface{}) error {
} }
type Category struct { type Category struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` 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"` 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"` OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"`
Description *string `gorm:"type:text" json:"description"` Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
Order int `gorm:"default:0" json:"order"` Description *string `gorm:"type:text" json:"description"`
BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"` Order int `gorm:"default:0" json:"order"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Products []Product `gorm:"foreignKey:CategoryID" json:"products,omitempty"` Products []Product `gorm:"foreignKey:CategoryID" json:"products,omitempty"`

View File

@ -36,7 +36,7 @@ func (h *CategoryHandler) CreateCategory(c *gin.Context) {
contextInfo := appcontext.FromGinContext(ctx) contextInfo := appcontext.FromGinContext(ctx)
var req contract.CreateCategoryRequest 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 { if err := c.ShouldBindJSON(&req); err != nil {
logger.FromContext(c.Request.Context()).WithError(err).Error("CategoryHandler::CreateCategory -> request binding failed") logger.FromContext(c.Request.Context()).WithError(err).Error("CategoryHandler::CreateCategory -> request binding failed")
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
@ -44,6 +44,11 @@ func (h *CategoryHandler) CreateCategory(c *gin.Context) {
return 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) validationError, validationErrorCode := h.categoryValidator.ValidateCreateCategoryRequest(&req)
if validationError != nil { if validationError != nil {
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
@ -149,6 +154,11 @@ func (h *CategoryHandler) ListCategories(c *gin.Context) {
OrganizationID: &contextInfo.OrganizationID, OrganizationID: &contextInfo.OrganizationID,
} }
// Inject outlet_id from context if user has one
if contextInfo.OutletID != uuid.Nil {
req.OutletID = &contextInfo.OutletID
}
// Parse query parameters // Parse query parameters
if pageStr := c.Query("page"); pageStr != "" { if pageStr := c.Query("page"); pageStr != "" {
if page, err := strconv.Atoi(pageStr); err == nil { 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) validationError, validationErrorCode := h.categoryValidator.ValidateListCategoriesRequest(req)
if validationError != nil { if validationError != nil {
logger.FromContext(ctx).WithError(validationError).Error("CategoryHandler::ListCategories -> request validation failed") logger.FromContext(ctx).WithError(validationError).Error("CategoryHandler::ListCategories -> request validation failed")

View File

@ -13,11 +13,12 @@ func CategoryEntityToModel(entity *entities.Category) *models.Category {
return &models.Category{ return &models.Category{
ID: entity.ID, ID: entity.ID,
OrganizationID: entity.OrganizationID, OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
Name: entity.Name, Name: entity.Name,
Description: entity.Description, Description: entity.Description,
ImageURL: nil, // Entity doesn't have ImageURL, model does ImageURL: nil,
Order: entity.Order, // Entity doesn't have SortOrder, model does Order: entity.Order,
IsActive: true, // Entity doesn't have IsActive, default to true IsActive: true,
CreatedAt: entity.CreatedAt, CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt, UpdatedAt: entity.UpdatedAt,
} }
@ -32,14 +33,14 @@ func CategoryModelToEntity(model *models.Category) *entities.Category {
if model.ImageURL != nil { if model.ImageURL != nil {
metadata["image_url"] = *model.ImageURL metadata["image_url"] = *model.ImageURL
} }
// metadata["sort_order"] = model.SortOrder
return &entities.Category{ return &entities.Category{
ID: model.ID, ID: model.ID,
OrganizationID: model.OrganizationID, OrganizationID: model.OrganizationID,
OutletID: model.OutletID,
Name: model.Name, Name: model.Name,
Description: model.Description, Description: model.Description,
BusinessType: "restaurant", // Default business type BusinessType: "restaurant",
Order: model.Order, Order: model.Order,
Metadata: metadata, Metadata: metadata,
CreatedAt: model.CreatedAt, CreatedAt: model.CreatedAt,
@ -56,14 +57,14 @@ func CreateCategoryRequestToEntity(req *models.CreateCategoryRequest) *entities.
if req.ImageURL != nil { if req.ImageURL != nil {
metadata["image_url"] = *req.ImageURL metadata["image_url"] = *req.ImageURL
} }
// metadata["sort_order"] = req.SortOrder
return &entities.Category{ return &entities.Category{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID,
Name: req.Name, Name: req.Name,
Description: req.Description, Description: req.Description,
Order: req.Order, Order: req.Order,
BusinessType: "restaurant", // Default business type BusinessType: "restaurant",
Metadata: metadata, Metadata: metadata,
} }
} }
@ -87,11 +88,12 @@ func CategoryEntityToResponse(entity *entities.Category) *models.CategoryRespons
return &models.CategoryResponse{ return &models.CategoryResponse{
ID: entity.ID, ID: entity.ID,
OrganizationID: entity.OrganizationID, OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
Name: entity.Name, Name: entity.Name,
Description: entity.Description, Description: entity.Description,
ImageURL: imageURL, ImageURL: imageURL,
Order: entity.Order, Order: entity.Order,
IsActive: true, // Default to true since entity doesn't have this field IsActive: true,
CreatedAt: entity.CreatedAt, CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt, UpdatedAt: entity.UpdatedAt,
} }
@ -121,6 +123,10 @@ func UpdateCategoryEntityFromRequest(entity *entities.Category, req *models.Upda
if req.Order != nil { if req.Order != nil {
entity.Order = *req.Order entity.Order = *req.Order
} }
if req.OutletID != nil {
entity.OutletID = req.OutletID
}
} }
func CategoryEntitiesToModels(entities []*entities.Category) []*models.Category { func CategoryEntitiesToModels(entities []*entities.Category) []*models.Category {

View File

@ -9,10 +9,11 @@ import (
type Category struct { type Category struct {
ID uuid.UUID ID uuid.UUID
OrganizationID uuid.UUID OrganizationID uuid.UUID
OutletID *uuid.UUID
Name string Name string
Description *string Description *string
ImageURL *string ImageURL *string
Order int Order int
IsActive bool IsActive bool
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
@ -20,27 +21,30 @@ type Category struct {
type CreateCategoryRequest struct { type CreateCategoryRequest struct {
OrganizationID uuid.UUID `validate:"required"` OrganizationID uuid.UUID `validate:"required"`
Name string `validate:"required,min=1,max=255"` OutletID *uuid.UUID
Description *string `validate:"omitempty,max=1000"` Name string `validate:"required,min=1,max=255"`
ImageURL *string `validate:"omitempty,url"` Description *string `validate:"omitempty,max=1000"`
Order int `validate:"min=0"` ImageURL *string `validate:"omitempty,url"`
Order int `validate:"min=0"`
} }
type UpdateCategoryRequest struct { type UpdateCategoryRequest struct {
Name *string `validate:"omitempty,min=1,max=255"` Name *string `validate:"omitempty,min=1,max=255"`
Description *string `validate:"omitempty,max=1000"` Description *string `validate:"omitempty,max=1000"`
ImageURL *string `validate:"omitempty,url"` ImageURL *string `validate:"omitempty,url"`
Order *int `validate:"omitempty,min=0"` OutletID *uuid.UUID
Order *int `validate:"omitempty,min=0"`
IsActive *bool IsActive *bool
} }
type CategoryResponse struct { type CategoryResponse struct {
ID uuid.UUID ID uuid.UUID
OrganizationID uuid.UUID OrganizationID uuid.UUID
OutletID *uuid.UUID
Name string Name string
Description *string Description *string
ImageURL *string ImageURL *string
Order int Order int
IsActive bool IsActive bool
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time

View File

@ -85,6 +85,9 @@ func (s *CategoryServiceImpl) ListCategories(ctx context.Context, req *contract.
if req.OrganizationID != nil { if req.OrganizationID != nil {
filters["organization_id"] = *req.OrganizationID filters["organization_id"] = *req.OrganizationID
} }
if req.OutletID != nil {
filters["outlet_id"] = *req.OutletID
}
if req.BusinessType != "" { if req.BusinessType != "" {
filters["business_type"] = req.BusinessType filters["business_type"] = req.BusinessType
} }

View File

@ -7,12 +7,17 @@ import (
) )
func CreateCategoryRequestToModel(apctx *appcontext.ContextInfo, req *contract.CreateCategoryRequest) *models.CreateCategoryRequest { func CreateCategoryRequestToModel(apctx *appcontext.ContextInfo, req *contract.CreateCategoryRequest) *models.CreateCategoryRequest {
order := 0
if req.Order != nil {
order = *req.Order
}
return &models.CreateCategoryRequest{ return &models.CreateCategoryRequest{
OrganizationID: apctx.OrganizationID, OrganizationID: apctx.OrganizationID,
OutletID: req.OutletID,
Name: req.Name, Name: req.Name,
Description: req.Description, Description: req.Description,
ImageURL: nil, ImageURL: nil,
Order: *req.Order, Order: order,
} }
} }
@ -21,7 +26,8 @@ func UpdateCategoryRequestToModel(req *contract.UpdateCategoryRequest) *models.U
Name: req.Name, Name: req.Name,
Description: req.Description, Description: req.Description,
ImageURL: nil, ImageURL: nil,
Order: req.Order, OutletID: req.OutletID,
Order: req.Order,
IsActive: nil, IsActive: nil,
} }
} }
@ -34,9 +40,10 @@ func CategoryModelResponseToResponse(cat *models.CategoryResponse) *contract.Cat
return &contract.CategoryResponse{ return &contract.CategoryResponse{
ID: cat.ID, ID: cat.ID,
OrganizationID: cat.OrganizationID, OrganizationID: cat.OrganizationID,
OutletID: cat.OutletID,
Name: cat.Name, Name: cat.Name,
Description: cat.Description, Description: cat.Description,
BusinessType: "restaurant", // Default business type BusinessType: "restaurant",
Order: cat.Order, Order: cat.Order,
Metadata: map[string]interface{}{}, Metadata: map[string]interface{}{},
CreatedAt: cat.CreatedAt, CreatedAt: cat.CreatedAt,

View File

@ -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;

View File

@ -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);