Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd61ad0eb9 | ||
|
|
84222fc7f4 | ||
|
|
23ac572e3f | ||
|
|
957c1ae53d | ||
|
|
d0378b5ac4 | ||
|
|
91960f0e57 | ||
|
|
72f67cb519 | ||
|
|
35c4cf2f2f | ||
|
|
c9ef90f5ea | ||
|
|
d9b51a7616 | ||
|
|
b27e40b531 | ||
|
|
44aca7641f | ||
|
|
a89ff00d94 | ||
|
|
227f11359c | ||
|
|
7a737d7f83 | ||
|
|
312ea94e62 |
@@ -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"`
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ type OrderItemResponse struct {
|
|||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
ProductVariantID *uuid.UUID `json:"product_variant_id"`
|
ProductVariantID *uuid.UUID `json:"product_variant_id"`
|
||||||
ProductVariantName *string `json:"product_variant_name,omitempty"`
|
ProductVariantName *string `json:"product_variant_name,omitempty"`
|
||||||
|
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||||
|
CategoryName *string `json:"category_name,omitempty"`
|
||||||
Quantity int `json:"quantity"`
|
Quantity int `json:"quantity"`
|
||||||
UnitPrice float64 `json:"unit_price"`
|
UnitPrice float64 `json:"unit_price"`
|
||||||
TotalPrice float64 `json:"total_price"`
|
TotalPrice float64 `json:"total_price"`
|
||||||
@@ -108,6 +110,7 @@ type OrderItemResponse struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
PrinterType string `json:"printer_type"`
|
PrinterType string `json:"printer_type"`
|
||||||
|
PrintToChecker bool `json:"print_to_checker"`
|
||||||
PaidQuantity int `json:"paid_quantity"`
|
PaidQuantity int `json:"paid_quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
type CreateProductRequest struct {
|
type CreateProductRequest struct {
|
||||||
CategoryID uuid.UUID `json:"category_id" validate:"required"`
|
CategoryID uuid.UUID `json:"category_id" validate:"required"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
SKU *string `json:"sku,omitempty"`
|
SKU *string `json:"sku,omitempty"`
|
||||||
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"`
|
||||||
@@ -16,28 +17,30 @@ type CreateProductRequest struct {
|
|||||||
BusinessType *string `json:"business_type,omitempty"`
|
BusinessType *string `json:"business_type,omitempty"`
|
||||||
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
|
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
|
||||||
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
|
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
|
||||||
|
PrintToChecker *bool `json:"print_to_checker,omitempty"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
Variants []CreateProductVariantRequest `json:"variants,omitempty"`
|
Variants []CreateProductVariantRequest `json:"variants,omitempty"`
|
||||||
InitialStock *int `json:"initial_stock,omitempty" validate:"omitempty,min=0"` // Initial stock quantity for all outlets
|
InitialStock *int `json:"initial_stock,omitempty" validate:"omitempty,min=0"`
|
||||||
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` // Reorder level for all outlets
|
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"`
|
||||||
CreateInventory bool `json:"create_inventory,omitempty"` // Whether to create inventory records for all outlets
|
CreateInventory bool `json:"create_inventory,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateProductRequest struct {
|
type UpdateProductRequest struct {
|
||||||
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
SKU *string `json:"sku,omitempty"`
|
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
SKU *string `json:"sku,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Price *float64 `json:"price,omitempty" validate:"omitempty,min=0"`
|
Description *string `json:"description,omitempty"`
|
||||||
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
|
Price *float64 `json:"price,omitempty" validate:"omitempty,min=0"`
|
||||||
BusinessType *string `json:"business_type,omitempty"`
|
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
|
||||||
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
|
BusinessType *string `json:"business_type,omitempty"`
|
||||||
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
|
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
PrintToChecker *bool `json:"print_to_checker,omitempty"`
|
||||||
// Stock management fields
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` // Update reorder level for all existing inventory records
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateProductVariantRequest struct {
|
type CreateProductVariantRequest struct {
|
||||||
@@ -70,6 +73,7 @@ type ProductResponse struct {
|
|||||||
BusinessType string `json:"business_type"`
|
BusinessType string `json:"business_type"`
|
||||||
ImageURL *string `json:"image_url"`
|
ImageURL *string `json:"image_url"`
|
||||||
PrinterType string `json:"printer_type"`
|
PrinterType string `json:"printer_type"`
|
||||||
|
PrintToChecker bool `json:"print_to_checker"`
|
||||||
Metadata map[string]interface{} `json:"metadata"`
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
|||||||
@@ -7,23 +7,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateProductOutletPriceRequest struct {
|
type CreateProductOutletPriceRequest struct {
|
||||||
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
||||||
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
||||||
Price float64 `json:"price" validate:"required,min=0"`
|
Price float64 `json:"price" validate:"required,min=0"`
|
||||||
|
PrintToChecker bool `json:"print_to_checker"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateProductOutletPriceRequest struct {
|
type UpdateProductOutletPriceRequest struct {
|
||||||
Price float64 `json:"price" validate:"required,min=0"`
|
Price float64 `json:"price" validate:"required,min=0"`
|
||||||
|
PrintToChecker *bool `json:"print_to_checker"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductOutletPriceResponse struct {
|
type ProductOutletPriceResponse struct {
|
||||||
ID uuid.UUID `json:"id,omitempty"`
|
ID uuid.UUID `json:"id,omitempty"`
|
||||||
ProductID uuid.UUID `json:"product_id,omitempty"`
|
ProductID uuid.UUID `json:"product_id,omitempty"`
|
||||||
OutletID uuid.UUID `json:"outlet_id"`
|
OutletID uuid.UUID `json:"outlet_id"`
|
||||||
OutletName string `json:"outlet_name,omitempty"`
|
OutletName string `json:"outlet_name,omitempty"`
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
PrintToChecker bool `json:"print_to_checker"`
|
||||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListProductOutletPricesResponse struct {
|
type ListProductOutletPricesResponse struct {
|
||||||
@@ -37,6 +40,7 @@ type BulkCreateProductOutletPriceRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateProductOutletPricePerOutletRequest struct {
|
type CreateProductOutletPricePerOutletRequest struct {
|
||||||
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
||||||
Price float64 `json:"price" validate:"required,min=0"`
|
Price float64 `json:"price" validate:"required,min=0"`
|
||||||
|
PrintToChecker bool `json:"print_to_checker"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -26,13 +26,14 @@ type Product struct {
|
|||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_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"`
|
||||||
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||||
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
||||||
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
|
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
|
||||||
ProductRecipes []ProductRecipe `gorm:"foreignKey:ProductID" json:"product_recipes,omitempty"`
|
ProductRecipes []ProductRecipe `gorm:"foreignKey:ProductID" json:"product_recipes,omitempty"`
|
||||||
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"`
|
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"`
|
||||||
OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"`
|
OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"`
|
||||||
|
ProductOutletPrices []ProductOutletPrice `gorm:"foreignKey:ProductID" json:"product_outlet_prices,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Product) BeforeCreate(tx *gorm.DB) error {
|
func (p *Product) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProductOutletPrice struct {
|
type ProductOutletPrice 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"`
|
||||||
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id"`
|
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id"`
|
||||||
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id"`
|
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id"`
|
||||||
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"`
|
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
PrintToChecker bool `gorm:"not null;default:true" json:"print_to_checker"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
||||||
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||||
|
|||||||
@@ -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,11 @@ 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")
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ func (h *OrderHandler) ListOrders(c *gin.Context) {
|
|||||||
if modelReq.OutletID == nil && contextInfo.OutletID != uuid.Nil {
|
if modelReq.OutletID == nil && contextInfo.OutletID != uuid.Nil {
|
||||||
modelReq.OutletID = &contextInfo.OutletID
|
modelReq.OutletID = &contextInfo.OutletID
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := h.orderService.ListOrders(c.Request.Context(), modelReq)
|
response, err := h.orderService.ListOrders(c.Request.Context(), modelReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::ListOrders", err.Error())}), "OrderHandler::ListOrders")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::ListOrders", err.Error())}), "OrderHandler::ListOrders")
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ func (h *ProductHandler) CreateProduct(c *gin.Context) {
|
|||||||
|
|
||||||
func (h *ProductHandler) UpdateProduct(c *gin.Context) {
|
func (h *ProductHandler) UpdateProduct(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
productIDStr := c.Param("id")
|
productIDStr := c.Param("id")
|
||||||
productID, err := uuid.Parse(productIDStr)
|
productID, err := uuid.Parse(productIDStr)
|
||||||
@@ -85,7 +86,7 @@ func (h *ProductHandler) UpdateProduct(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
productResponse := h.productService.UpdateProduct(ctx, productID, &req)
|
productResponse := h.productService.UpdateProduct(ctx, contextInfo, productID, &req)
|
||||||
if productResponse.HasErrors() {
|
if productResponse.HasErrors() {
|
||||||
errorResp := productResponse.GetErrors()[0]
|
errorResp := productResponse.GetErrors()[0]
|
||||||
logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::UpdateProduct -> Failed to update product from service")
|
logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::UpdateProduct -> Failed to update product from service")
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func OrderEntityToResponse(order *entities.Order) *models.OrderResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, item := range order.OrderItems {
|
for i, item := range order.OrderItems {
|
||||||
resp := OrderItemEntityToResponse(&item)
|
resp := OrderItemEntityToResponse(&item, order.OutletID)
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
resp.PaidQuantity = paidQtyByOrderItem[item.ID]
|
resp.PaidQuantity = paidQtyByOrderItem[item.ID]
|
||||||
response.OrderItems[i] = *resp
|
response.OrderItems[i] = *resp
|
||||||
@@ -101,11 +101,20 @@ func OrderEntityToResponse(order *entities.Order) *models.OrderResponse {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemResponse {
|
func OrderItemEntityToResponse(item *entities.OrderItem, outletID uuid.UUID) *models.OrderItemResponse {
|
||||||
if item == nil {
|
if item == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve print_to_checker from preloaded outlet prices
|
||||||
|
printToChecker := true // default
|
||||||
|
for _, op := range item.Product.ProductOutletPrices {
|
||||||
|
if op.OutletID == outletID {
|
||||||
|
printToChecker = op.PrintToChecker
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response := &models.OrderItemResponse{
|
response := &models.OrderItemResponse{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
OrderID: item.OrderID,
|
OrderID: item.OrderID,
|
||||||
@@ -130,10 +139,19 @@ func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemRespon
|
|||||||
CreatedAt: item.CreatedAt,
|
CreatedAt: item.CreatedAt,
|
||||||
UpdatedAt: item.UpdatedAt,
|
UpdatedAt: item.UpdatedAt,
|
||||||
PrinterType: item.Product.PrinterType,
|
PrinterType: item.Product.PrinterType,
|
||||||
|
PrintToChecker: printToChecker,
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Product.ID != uuid.Nil {
|
if item.Product.ID != uuid.Nil {
|
||||||
response.ProductName = item.Product.Name
|
response.ProductName = item.Product.Name
|
||||||
|
if item.Product.CategoryID != uuid.Nil {
|
||||||
|
categoryID := item.Product.CategoryID
|
||||||
|
response.CategoryID = &categoryID
|
||||||
|
}
|
||||||
|
if item.Product.Category.ID != uuid.Nil {
|
||||||
|
categoryName := item.Product.Category.Name
|
||||||
|
response.CategoryName = &categoryName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.ProductVariant != nil {
|
if item.ProductVariant != nil {
|
||||||
@@ -316,14 +334,14 @@ func OrderEntitiesToResponses(orders []*entities.Order) []models.OrderResponse {
|
|||||||
return responses
|
return responses
|
||||||
}
|
}
|
||||||
|
|
||||||
func OrderItemEntitiesToResponses(items []*entities.OrderItem) []models.OrderItemResponse {
|
func OrderItemEntitiesToResponses(items []*entities.OrderItem, outletID uuid.UUID) []models.OrderItemResponse {
|
||||||
if items == nil {
|
if items == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
responses := make([]models.OrderItemResponse, len(items))
|
responses := make([]models.OrderItemResponse, len(items))
|
||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
response := OrderItemEntityToResponse(item)
|
response := OrderItemEntityToResponse(item, outletID)
|
||||||
if response != nil {
|
if response != nil {
|
||||||
responses[i] = *response
|
responses[i] = *response
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func TestOrderItemEntityToResponse_WithProductNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
result := OrderItemEntityToResponse(orderItem)
|
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.NotNil(t, result)
|
assert.NotNil(t, result)
|
||||||
@@ -89,7 +89,7 @@ func TestOrderItemEntityToResponse_WithoutProductVariant(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
result := OrderItemEntityToResponse(orderItem)
|
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.NotNil(t, result)
|
assert.NotNil(t, result)
|
||||||
@@ -129,7 +129,7 @@ func TestOrderItemEntityToResponse_WithoutProductPreload(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
result := OrderItemEntityToResponse(orderItem)
|
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.NotNil(t, result)
|
assert.NotNil(t, result)
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ func ProductOutletPriceEntityToModel(entity *entities.ProductOutletPrice) *model
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &models.ProductOutletPrice{
|
return &models.ProductOutletPrice{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
ProductID: entity.ProductID,
|
ProductID: entity.ProductID,
|
||||||
OutletID: entity.OutletID,
|
OutletID: entity.OutletID,
|
||||||
Price: entity.Price,
|
Price: entity.Price,
|
||||||
CreatedAt: entity.CreatedAt,
|
PrintToChecker: entity.PrintToChecker,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,12 +27,13 @@ func ProductOutletPriceModelToEntity(model *models.ProductOutletPrice) *entities
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &entities.ProductOutletPrice{
|
return &entities.ProductOutletPrice{
|
||||||
ID: model.ID,
|
ID: model.ID,
|
||||||
ProductID: model.ProductID,
|
ProductID: model.ProductID,
|
||||||
OutletID: model.OutletID,
|
OutletID: model.OutletID,
|
||||||
Price: model.Price,
|
Price: model.Price,
|
||||||
CreatedAt: model.CreatedAt,
|
PrintToChecker: model.PrintToChecker,
|
||||||
UpdatedAt: model.UpdatedAt,
|
CreatedAt: model.CreatedAt,
|
||||||
|
UpdatedAt: model.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ type OrderItemResponse struct {
|
|||||||
ProductName string
|
ProductName string
|
||||||
ProductVariantID *uuid.UUID
|
ProductVariantID *uuid.UUID
|
||||||
ProductVariantName *string
|
ProductVariantName *string
|
||||||
|
CategoryID *uuid.UUID
|
||||||
|
CategoryName *string
|
||||||
Quantity int
|
Quantity int
|
||||||
UnitPrice float64
|
UnitPrice float64
|
||||||
TotalPrice float64
|
TotalPrice float64
|
||||||
@@ -207,6 +209,7 @@ type OrderItemResponse struct {
|
|||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
PrinterType string
|
PrinterType string
|
||||||
|
PrintToChecker bool
|
||||||
PaidQuantity int
|
PaidQuantity int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type ProductVariant struct {
|
|||||||
|
|
||||||
type CreateProductRequest struct {
|
type CreateProductRequest struct {
|
||||||
OrganizationID uuid.UUID `validate:"required"`
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
|
OutletID uuid.UUID `validate:"omitempty"` // If set, upsert product_outlet_prices on create
|
||||||
CategoryID uuid.UUID `validate:"required"`
|
CategoryID uuid.UUID `validate:"required"`
|
||||||
SKU *string `validate:"omitempty,max=100"`
|
SKU *string `validate:"omitempty,max=100"`
|
||||||
Name string `validate:"required,min=1,max=255"`
|
Name string `validate:"required,min=1,max=255"`
|
||||||
@@ -49,6 +50,7 @@ type CreateProductRequest struct {
|
|||||||
BusinessType constants.BusinessType `validate:"required"`
|
BusinessType constants.BusinessType `validate:"required"`
|
||||||
ImageURL *string `validate:"omitempty,max=500"`
|
ImageURL *string `validate:"omitempty,max=500"`
|
||||||
PrinterType *string `validate:"omitempty,max=50"`
|
PrinterType *string `validate:"omitempty,max=50"`
|
||||||
|
PrintToChecker *bool `validate:"omitempty"`
|
||||||
UnitID *uuid.UUID `validate:"omitempty"`
|
UnitID *uuid.UUID `validate:"omitempty"`
|
||||||
HasIngredients bool `validate:"omitempty"`
|
HasIngredients bool `validate:"omitempty"`
|
||||||
Metadata map[string]interface{}
|
Metadata map[string]interface{}
|
||||||
@@ -60,6 +62,7 @@ type CreateProductRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateProductRequest struct {
|
type UpdateProductRequest struct {
|
||||||
|
OutletID uuid.UUID `validate:"omitempty"` // If set, upsert product_outlet_prices on update
|
||||||
CategoryID *uuid.UUID `validate:"omitempty"`
|
CategoryID *uuid.UUID `validate:"omitempty"`
|
||||||
SKU *string `validate:"omitempty,max=100"`
|
SKU *string `validate:"omitempty,max=100"`
|
||||||
Name *string `validate:"omitempty,min=1,max=255"`
|
Name *string `validate:"omitempty,min=1,max=255"`
|
||||||
@@ -68,6 +71,7 @@ type UpdateProductRequest struct {
|
|||||||
Cost *float64 `validate:"omitempty,min=0"`
|
Cost *float64 `validate:"omitempty,min=0"`
|
||||||
ImageURL *string `validate:"omitempty,max=500"`
|
ImageURL *string `validate:"omitempty,max=500"`
|
||||||
PrinterType *string `validate:"omitempty,max=50"`
|
PrinterType *string `validate:"omitempty,max=50"`
|
||||||
|
PrintToChecker *bool `validate:"omitempty"`
|
||||||
UnitID *uuid.UUID `validate:"omitempty"`
|
UnitID *uuid.UUID `validate:"omitempty"`
|
||||||
HasIngredients *bool `validate:"omitempty"`
|
HasIngredients *bool `validate:"omitempty"`
|
||||||
Metadata map[string]interface{}
|
Metadata map[string]interface{}
|
||||||
@@ -106,6 +110,7 @@ type ProductResponse struct {
|
|||||||
BusinessType constants.BusinessType
|
BusinessType constants.BusinessType
|
||||||
ImageURL *string
|
ImageURL *string
|
||||||
PrinterType string
|
PrinterType string
|
||||||
|
PrintToChecker bool
|
||||||
UnitID *uuid.UUID
|
UnitID *uuid.UUID
|
||||||
HasIngredients bool
|
HasIngredients bool
|
||||||
Metadata map[string]interface{}
|
Metadata map[string]interface{}
|
||||||
@@ -116,9 +121,10 @@ type ProductResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OutletPrice struct {
|
type OutletPrice struct {
|
||||||
OutletID uuid.UUID
|
OutletID uuid.UUID
|
||||||
OutletName string
|
OutletName string
|
||||||
Price float64
|
Price float64
|
||||||
|
PrintToChecker bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductVariantResponse struct {
|
type ProductVariantResponse struct {
|
||||||
|
|||||||
@@ -7,22 +7,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProductOutletPrice struct {
|
type ProductOutletPrice struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
ProductID uuid.UUID
|
ProductID uuid.UUID
|
||||||
OutletID uuid.UUID
|
OutletID uuid.UUID
|
||||||
Price float64
|
Price float64
|
||||||
CreatedAt time.Time
|
PrintToChecker bool
|
||||||
UpdatedAt time.Time
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateProductOutletPriceRequest struct {
|
type CreateProductOutletPriceRequest struct {
|
||||||
ProductID uuid.UUID `validate:"required"`
|
ProductID uuid.UUID `validate:"required"`
|
||||||
OutletID uuid.UUID `validate:"required"`
|
OutletID uuid.UUID `validate:"required"`
|
||||||
Price float64 `validate:"required,min=0"`
|
Price float64 `validate:"required,min=0"`
|
||||||
|
PrintToChecker bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateProductOutletPriceRequest struct {
|
type UpdateProductOutletPriceRequest struct {
|
||||||
Price *float64 `validate:"required,min=0"`
|
Price *float64 `validate:"required,min=0"`
|
||||||
|
PrintToChecker *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductOutletPriceResponse struct {
|
type ProductOutletPriceResponse struct {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package processor
|
package processor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"apskel-pos-be/internal/constants"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -388,31 +387,10 @@ func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID,
|
|||||||
return nil, fmt.Errorf("failed to create order item: %w", err)
|
return nil, fmt.Errorf("failed to create order item: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
itemResponse := models.OrderItemResponse{
|
itemResponse := mappers.OrderItemEntityToResponse(orderItem, order.OutletID)
|
||||||
ID: orderItem.ID,
|
if itemResponse != nil {
|
||||||
OrderID: orderItem.OrderID,
|
addedItemResponses = append(addedItemResponses, *itemResponse)
|
||||||
ProductID: orderItem.ProductID,
|
|
||||||
ProductVariantID: orderItem.ProductVariantID,
|
|
||||||
Quantity: orderItem.Quantity,
|
|
||||||
UnitPrice: orderItem.UnitPrice,
|
|
||||||
TotalPrice: orderItem.TotalPrice,
|
|
||||||
UnitCost: orderItem.UnitCost,
|
|
||||||
TotalCost: orderItem.TotalCost,
|
|
||||||
RefundAmount: orderItem.RefundAmount,
|
|
||||||
RefundQuantity: orderItem.RefundQuantity,
|
|
||||||
IsPartiallyRefunded: orderItem.IsPartiallyRefunded,
|
|
||||||
IsFullyRefunded: orderItem.IsFullyRefunded,
|
|
||||||
RefundReason: orderItem.RefundReason,
|
|
||||||
RefundedAt: orderItem.RefundedAt,
|
|
||||||
RefundedBy: orderItem.RefundedBy,
|
|
||||||
Modifiers: []map[string]interface{}(orderItem.Modifiers),
|
|
||||||
Notes: orderItem.Notes,
|
|
||||||
Metadata: map[string]interface{}(orderItem.Metadata),
|
|
||||||
Status: constants.OrderItemStatus(orderItem.Status),
|
|
||||||
CreatedAt: orderItem.CreatedAt,
|
|
||||||
UpdatedAt: orderItem.UpdatedAt,
|
|
||||||
}
|
}
|
||||||
addedItemResponses = append(addedItemResponses, itemResponse)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, orderID)
|
orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, orderID)
|
||||||
|
|||||||
@@ -46,9 +46,10 @@ func (p *ProductOutletPriceProcessorImpl) Upsert(ctx context.Context, req *model
|
|||||||
}
|
}
|
||||||
|
|
||||||
entity := &entities.ProductOutletPrice{
|
entity := &entities.ProductOutletPrice{
|
||||||
ProductID: req.ProductID,
|
ProductID: req.ProductID,
|
||||||
OutletID: req.OutletID,
|
OutletID: req.OutletID,
|
||||||
Price: req.Price,
|
Price: req.Price,
|
||||||
|
PrintToChecker: req.PrintToChecker,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.repo.Upsert(ctx, entity); err != nil {
|
if err := p.repo.Upsert(ctx, entity); err != nil {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
"apskel-pos-be/internal/mappers"
|
"apskel-pos-be/internal/mappers"
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
"apskel-pos-be/internal/repository"
|
"apskel-pos-be/internal/repository"
|
||||||
@@ -39,6 +40,7 @@ type ProductRepository interface {
|
|||||||
ExistsBySKU(ctx context.Context, organizationID uuid.UUID, sku string, excludeID *uuid.UUID) (bool, error)
|
ExistsBySKU(ctx context.Context, organizationID uuid.UUID, sku string, excludeID *uuid.UUID) (bool, error)
|
||||||
GetByName(ctx context.Context, organizationID uuid.UUID, name string) (*entities.Product, error)
|
GetByName(ctx context.Context, organizationID uuid.UUID, name string) (*entities.Product, error)
|
||||||
ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error)
|
ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error)
|
||||||
|
ExistsByNameInOutlet(ctx context.Context, organizationID uuid.UUID, outletID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error)
|
||||||
UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error
|
UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error
|
||||||
GetLowCostProducts(ctx context.Context, organizationID uuid.UUID, maxCost float64) ([]*entities.Product, error)
|
GetLowCostProducts(ctx context.Context, organizationID uuid.UUID, maxCost float64) ([]*entities.Product, error)
|
||||||
}
|
}
|
||||||
@@ -79,12 +81,12 @@ func (p *ProductProcessorImpl) CreateProduct(ctx context.Context, req *models.Cr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exists, err := p.productRepo.ExistsByName(ctx, req.OrganizationID, req.Name, nil)
|
exists, err := p.productRepo.ExistsByNameInOutlet(ctx, req.OrganizationID, req.OutletID, req.Name, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to check product name uniqueness: %w", err)
|
return nil, fmt.Errorf("failed to check product name uniqueness: %w", err)
|
||||||
}
|
}
|
||||||
if exists {
|
if exists {
|
||||||
return nil, fmt.Errorf("product with name '%s' already exists for this organization", req.Name)
|
return nil, fmt.Errorf("product with name '%s' already exists for this outlet", req.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
productEntity := mappers.CreateProductRequestToEntity(req)
|
productEntity := mappers.CreateProductRequestToEntity(req)
|
||||||
@@ -122,6 +124,23 @@ func (p *ProductProcessorImpl) CreateProduct(ctx context.Context, req *models.Cr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upsert outlet-specific price if outlet context is present
|
||||||
|
if req.OutletID != uuid.Nil {
|
||||||
|
printToChecker := true // default
|
||||||
|
if req.PrintToChecker != nil {
|
||||||
|
printToChecker = *req.PrintToChecker
|
||||||
|
}
|
||||||
|
outletPriceEntity := &entities.ProductOutletPrice{
|
||||||
|
ProductID: productEntity.ID,
|
||||||
|
OutletID: req.OutletID,
|
||||||
|
Price: req.Price,
|
||||||
|
PrintToChecker: printToChecker,
|
||||||
|
}
|
||||||
|
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)
|
productWithCategory, err := p.productRepo.GetWithCategory(ctx, productEntity.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to retrieve created product: %w", err)
|
return nil, fmt.Errorf("failed to retrieve created product: %w", err)
|
||||||
@@ -161,12 +180,12 @@ func (p *ProductProcessorImpl) UpdateProduct(ctx context.Context, id uuid.UUID,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Name != nil && *req.Name != existingProduct.Name {
|
if req.Name != nil && *req.Name != existingProduct.Name {
|
||||||
exists, err := p.productRepo.ExistsByName(ctx, existingProduct.OrganizationID, *req.Name, &id)
|
exists, err := p.productRepo.ExistsByNameInOutlet(ctx, existingProduct.OrganizationID, req.OutletID, *req.Name, &id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to check product name uniqueness: %w", err)
|
return nil, fmt.Errorf("failed to check product name uniqueness: %w", err)
|
||||||
}
|
}
|
||||||
if exists {
|
if exists {
|
||||||
return nil, fmt.Errorf("product with name '%s' already exists for this organization", *req.Name)
|
return nil, fmt.Errorf("product with name '%s' already exists for this outlet", *req.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +202,41 @@ func (p *ProductProcessorImpl) UpdateProduct(ctx context.Context, id uuid.UUID,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upsert outlet-specific price if outlet context is present and price or print_to_checker is provided
|
||||||
|
if req.OutletID != uuid.Nil && (req.Price != nil || req.PrintToChecker != nil) {
|
||||||
|
// Fetch existing outlet price to use as fallback for fields not provided
|
||||||
|
existing, _ := p.outletPriceRepo.GetByProductAndOutlet(ctx, id, req.OutletID)
|
||||||
|
|
||||||
|
price := float64(0)
|
||||||
|
if existing != nil {
|
||||||
|
price = existing.Price
|
||||||
|
}
|
||||||
|
if req.Price != nil {
|
||||||
|
price = *req.Price
|
||||||
|
}
|
||||||
|
|
||||||
|
printToChecker := true // default
|
||||||
|
if existing != nil {
|
||||||
|
printToChecker = existing.PrintToChecker
|
||||||
|
}
|
||||||
|
if req.PrintToChecker != nil {
|
||||||
|
printToChecker = *req.PrintToChecker
|
||||||
|
}
|
||||||
|
|
||||||
|
outletPriceEntity := &entities.ProductOutletPrice{
|
||||||
|
ProductID: id,
|
||||||
|
OutletID: req.OutletID,
|
||||||
|
Price: price,
|
||||||
|
PrintToChecker: printToChecker,
|
||||||
|
}
|
||||||
|
logger.FromContext(ctx).Infof("ProductProcessor::UpdateProduct -> upserting outlet price: productID=%s outletID=%s price=%f printToChecker=%v", id, req.OutletID, price, printToChecker)
|
||||||
|
if err := p.outletPriceRepo.Upsert(ctx, outletPriceEntity); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to assign outlet price: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.FromContext(ctx).Infof("ProductProcessor::UpdateProduct -> skipping outlet price upsert: outletID=%s price=%v printToChecker=%v", req.OutletID, req.Price, req.PrintToChecker)
|
||||||
|
}
|
||||||
|
|
||||||
productWithCategory, err := p.productRepo.GetWithCategory(ctx, id)
|
productWithCategory, err := p.productRepo.GetWithCategory(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to retrieve updated product: %w", err)
|
return nil, fmt.Errorf("failed to retrieve updated product: %w", err)
|
||||||
@@ -231,6 +285,7 @@ func (p *ProductProcessorImpl) GetProductByID(ctx context.Context, id uuid.UUID,
|
|||||||
outletPrice, err := p.outletPriceRepo.GetByProductAndOutlet(ctx, id, outletID)
|
outletPrice, err := p.outletPriceRepo.GetByProductAndOutlet(ctx, id, outletID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
response.OutletPrice = &outletPrice.Price
|
response.OutletPrice = &outletPrice.Price
|
||||||
|
response.PrintToChecker = outletPrice.PrintToChecker
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No outlet context — return all outlet prices for this product
|
// No outlet context — return all outlet prices for this product
|
||||||
@@ -239,9 +294,10 @@ func (p *ProductProcessorImpl) GetProductByID(ctx context.Context, id uuid.UUID,
|
|||||||
prices := make([]models.OutletPrice, len(outletPrices))
|
prices := make([]models.OutletPrice, len(outletPrices))
|
||||||
for i, op := range outletPrices {
|
for i, op := range outletPrices {
|
||||||
prices[i] = models.OutletPrice{
|
prices[i] = models.OutletPrice{
|
||||||
OutletID: op.OutletID,
|
OutletID: op.OutletID,
|
||||||
OutletName: op.Outlet.Name,
|
OutletName: op.Outlet.Name,
|
||||||
Price: op.Price,
|
Price: op.Price,
|
||||||
|
PrintToChecker: op.PrintToChecker,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response.OutletPrices = prices
|
response.OutletPrices = prices
|
||||||
@@ -278,10 +334,35 @@ func (p *ProductProcessorImpl) ListProducts(ctx context.Context, filters map[str
|
|||||||
}
|
}
|
||||||
|
|
||||||
responses := make([]models.ProductResponse, len(productEntities))
|
responses := make([]models.ProductResponse, len(productEntities))
|
||||||
for i, entity := range productEntities {
|
if outletID != uuid.Nil && len(productEntities) > 0 {
|
||||||
response := mappers.ProductEntityToResponse(entity)
|
// Bulk-fetch outlet prices to populate OutletPrice and PrintToChecker per product
|
||||||
if response != nil {
|
productIDs := make([]uuid.UUID, len(productEntities))
|
||||||
responses[i] = *response
|
for i, e := range productEntities {
|
||||||
|
productIDs[i] = e.ID
|
||||||
|
}
|
||||||
|
outletPrices, opErr := p.outletPriceRepo.GetByProductsAndOutlet(ctx, productIDs, outletID)
|
||||||
|
priceMap := make(map[uuid.UUID]*entities.ProductOutletPrice)
|
||||||
|
if opErr == nil {
|
||||||
|
for _, op := range outletPrices {
|
||||||
|
priceMap[op.ProductID] = op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, entity := range productEntities {
|
||||||
|
response := mappers.ProductEntityToResponse(entity)
|
||||||
|
if response != nil {
|
||||||
|
if op, ok := priceMap[entity.ID]; ok {
|
||||||
|
response.OutletPrice = &op.Price
|
||||||
|
response.PrintToChecker = op.PrintToChecker
|
||||||
|
}
|
||||||
|
responses[i] = *response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i, entity := range productEntities {
|
||||||
|
response := mappers.ProductEntityToResponse(entity)
|
||||||
|
if response != nil {
|
||||||
|
responses[i] = *response
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ func (r *CategoryRepositoryImpl) List(ctx context.Context, filters map[string]in
|
|||||||
case "search":
|
case "search":
|
||||||
searchValue := "%" + value.(string) + "%"
|
searchValue := "%" + value.(string) + "%"
|
||||||
query = query.Where("name ILIKE ? OR description ILIKE ?", searchValue, searchValue)
|
query = query.Where("name ILIKE ? OR description ILIKE ?", searchValue, searchValue)
|
||||||
|
case "outlet_id":
|
||||||
|
// Include outlet-specific categories AND global categories (outlet_id IS NULL)
|
||||||
|
query = query.Where("outlet_id = ? OR outlet_id IS NULL", value)
|
||||||
default:
|
default:
|
||||||
query = query.Where(key+" = ?", value)
|
query = query.Where(key+" = ?", value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ func (r *OrderRepositoryImpl) GetWithRelations(ctx context.Context, id uuid.UUID
|
|||||||
Preload("User").
|
Preload("User").
|
||||||
Preload("OrderItems").
|
Preload("OrderItems").
|
||||||
Preload("OrderItems.Product").
|
Preload("OrderItems.Product").
|
||||||
|
Preload("OrderItems.Product.Category").
|
||||||
|
Preload("OrderItems.Product.ProductOutletPrices").
|
||||||
Preload("OrderItems.ProductVariant").
|
Preload("OrderItems.ProductVariant").
|
||||||
Preload("Payments").
|
Preload("Payments").
|
||||||
Preload("Payments.PaymentMethod").
|
Preload("Payments.PaymentMethod").
|
||||||
@@ -98,36 +100,54 @@ func (r *OrderRepositoryImpl) List(ctx context.Context, filters map[string]inter
|
|||||||
var orders []*entities.Order
|
var orders []*entities.Order
|
||||||
var total int64
|
var total int64
|
||||||
|
|
||||||
query := r.db.WithContext(ctx).Model(&entities.Order{}).
|
// organization_id is mandatory to prevent cross-org data leaks
|
||||||
|
organizationID, ok := filters["organization_id"]
|
||||||
|
if !ok {
|
||||||
|
return nil, 0, fmt.Errorf("organization_id is required for listing orders")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseQuery := r.db.WithContext(ctx).Model(&entities.Order{}).
|
||||||
|
Where("organization_id = ?", organizationID)
|
||||||
|
|
||||||
|
// outlet_id is optional — if present, scope to that outlet; otherwise return all outlets in the org
|
||||||
|
if outletID, exists := filters["outlet_id"]; exists {
|
||||||
|
baseQuery = baseQuery.Where("outlet_id = ?", outletID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range filters {
|
||||||
|
switch key {
|
||||||
|
case "organization_id", "outlet_id":
|
||||||
|
// already handled above
|
||||||
|
case "search":
|
||||||
|
searchValue := "%" + value.(string) + "%"
|
||||||
|
baseQuery = baseQuery.Where("order_number ILIKE ?", searchValue)
|
||||||
|
case "date_from":
|
||||||
|
baseQuery = baseQuery.Where("created_at >= ?", value)
|
||||||
|
case "date_to":
|
||||||
|
baseQuery = baseQuery.Where("created_at <= ?", value)
|
||||||
|
default:
|
||||||
|
baseQuery = baseQuery.Where(key+" = ?", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use separate queries for count and find to avoid GORM state mutation issues
|
||||||
|
if err := baseQuery.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := baseQuery.
|
||||||
Preload("Organization").
|
Preload("Organization").
|
||||||
Preload("Outlet").
|
Preload("Outlet").
|
||||||
Preload("User").
|
Preload("User").
|
||||||
Preload("OrderItems").
|
Preload("OrderItems").
|
||||||
Preload("OrderItems.Product").
|
Preload("OrderItems.Product").
|
||||||
|
Preload("OrderItems.Product.Category").
|
||||||
|
Preload("OrderItems.Product.ProductOutletPrices").
|
||||||
Preload("OrderItems.ProductVariant").
|
Preload("OrderItems.ProductVariant").
|
||||||
Preload("Payments").
|
Preload("Payments").
|
||||||
Preload("Payments.PaymentMethod").
|
Preload("Payments.PaymentMethod").
|
||||||
Preload("Payments.PaymentOrderItems")
|
Preload("Payments.PaymentOrderItems").
|
||||||
|
Limit(limit).Offset(offset).Order("created_at DESC").Find(&orders).Error
|
||||||
for key, value := range filters {
|
|
||||||
switch key {
|
|
||||||
case "search":
|
|
||||||
searchValue := "%" + value.(string) + "%"
|
|
||||||
query = query.Where("order_number ILIKE ?", searchValue)
|
|
||||||
case "date_from":
|
|
||||||
query = query.Where("created_at >= ?", value)
|
|
||||||
case "date_to":
|
|
||||||
query = query.Where("created_at <= ?", value)
|
|
||||||
default:
|
|
||||||
query = query.Where(key+" = ?", value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := query.Count(&total).Error; err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&orders).Error
|
|
||||||
return orders, total, err
|
return orders, total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +159,8 @@ func (r *OrderRepositoryImpl) ListBySessionID(ctx context.Context, sessionID str
|
|||||||
Preload("User").
|
Preload("User").
|
||||||
Preload("OrderItems").
|
Preload("OrderItems").
|
||||||
Preload("OrderItems.Product").
|
Preload("OrderItems.Product").
|
||||||
|
Preload("OrderItems.Product.Category").
|
||||||
|
Preload("OrderItems.Product.ProductOutletPrices").
|
||||||
Preload("OrderItems.ProductVariant").
|
Preload("OrderItems.ProductVariant").
|
||||||
Preload("Payments").
|
Preload("Payments").
|
||||||
Preload("Payments.PaymentMethod").
|
Preload("Payments.PaymentMethod").
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ func (r *OrganizationRepositoryImpl) GetTotalOmset(ctx context.Context, organiza
|
|||||||
var total float64
|
var total float64
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Table("orders").
|
Table("orders").
|
||||||
Where("organization_id = ? AND payment_status = ?", organizationID, "completed").
|
Where("organization_id = ? AND payment_status = ? AND is_void = ? AND is_refund = ?", organizationID, "completed", false, false).
|
||||||
Select("COALESCE(SUM(total_amount), 0)").
|
Select("COALESCE(SUM(total_amount), 0)").
|
||||||
Scan(&total).Error
|
Scan(&total).Error
|
||||||
return total, err
|
return total, err
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProductOutletPriceRepository interface {
|
type ProductOutletPriceRepository interface {
|
||||||
@@ -53,10 +52,18 @@ func (r *ProductOutletPriceRepositoryImpl) GetByOutlet(ctx context.Context, outl
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProductOutletPriceRepositoryImpl) Upsert(ctx context.Context, price *entities.ProductOutletPrice) error {
|
func (r *ProductOutletPriceRepositoryImpl) Upsert(ctx context.Context, price *entities.ProductOutletPrice) error {
|
||||||
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
if price.ID == uuid.Nil {
|
||||||
Columns: []clause.Column{{Name: "product_id"}, {Name: "outlet_id"}},
|
price.ID = uuid.New()
|
||||||
DoUpdates: clause.AssignmentColumns([]string{"price", "updated_at"}),
|
}
|
||||||
}).Create(price).Error
|
return r.db.WithContext(ctx).Exec(`
|
||||||
|
INSERT INTO product_outlet_prices (id, product_id, outlet_id, price, print_to_checker, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, NOW(), NOW())
|
||||||
|
ON CONFLICT (product_id, outlet_id)
|
||||||
|
DO UPDATE SET
|
||||||
|
price = EXCLUDED.price,
|
||||||
|
print_to_checker = EXCLUDED.print_to_checker,
|
||||||
|
updated_at = NOW()
|
||||||
|
`, price.ID, price.ProductID, price.OutletID, price.Price, price.PrintToChecker).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProductOutletPriceRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
func (r *ProductOutletPriceRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
|||||||
@@ -178,6 +178,26 @@ func (r *ProductRepositoryImpl) ExistsByName(ctx context.Context, organizationID
|
|||||||
return count > 0, err
|
return count > 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExistsByNameInOutlet checks name uniqueness scoped to a specific outlet via product_outlet_prices.
|
||||||
|
// Falls back to organization-scoped check when outletID is zero.
|
||||||
|
func (r *ProductRepositoryImpl) ExistsByNameInOutlet(ctx context.Context, organizationID uuid.UUID, outletID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) {
|
||||||
|
if outletID == uuid.Nil {
|
||||||
|
return r.ExistsByName(ctx, organizationID, name, excludeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := r.db.WithContext(ctx).Model(&entities.Product{}).
|
||||||
|
Joins("INNER JOIN product_outlet_prices pop ON pop.product_id = products.id AND pop.outlet_id = ?", outletID).
|
||||||
|
Where("products.organization_id = ? AND products.name = ?", organizationID, name)
|
||||||
|
|
||||||
|
if excludeID != nil {
|
||||||
|
query = query.Where("products.id != ?", *excludeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
err := query.Count(&count).Error
|
||||||
|
return count > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ProductRepositoryImpl) UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error {
|
func (r *ProductRepositoryImpl) UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error {
|
||||||
return r.db.WithContext(ctx).Model(&entities.Product{}).
|
return r.db.WithContext(ctx).Model(&entities.Product{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,9 +105,10 @@ func (s *ProductOutletPriceServiceImpl) BulkUpsert(ctx context.Context, req *con
|
|||||||
prices := make([]models.CreateProductOutletPriceRequest, len(req.Prices))
|
prices := make([]models.CreateProductOutletPriceRequest, len(req.Prices))
|
||||||
for i, p := range req.Prices {
|
for i, p := range req.Prices {
|
||||||
prices[i] = models.CreateProductOutletPriceRequest{
|
prices[i] = models.CreateProductOutletPriceRequest{
|
||||||
ProductID: req.ProductID,
|
ProductID: req.ProductID,
|
||||||
OutletID: p.OutletID,
|
OutletID: p.OutletID,
|
||||||
Price: p.Price,
|
Price: p.Price,
|
||||||
|
PrintToChecker: p.PrintToChecker,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
type ProductService interface {
|
type ProductService interface {
|
||||||
CreateProduct(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateProductRequest) *contract.Response
|
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
|
DeleteProduct(ctx context.Context, id uuid.UUID) *contract.Response
|
||||||
GetProductByID(ctx context.Context, id uuid.UUID, outletID 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
|
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)
|
return contract.BuildSuccessResponse(contractResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProductServiceImpl) UpdateProduct(ctx context.Context, id uuid.UUID, req *contract.UpdateProductRequest) *contract.Response {
|
func (s *ProductServiceImpl) UpdateProduct(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdateProductRequest) *contract.Response {
|
||||||
modelReq := transformer.UpdateProductRequestToModel(req)
|
modelReq := transformer.UpdateProductRequestToModel(apctx, req)
|
||||||
|
|
||||||
productResponse, err := s.productProcessor.UpdateProduct(ctx, id, modelReq)
|
productResponse, err := s.productProcessor.UpdateProduct(ctx, id, modelReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ func OrderModelToContract(resp *models.OrderResponse) *contract.OrderResponse {
|
|||||||
ProductName: item.ProductName,
|
ProductName: item.ProductName,
|
||||||
ProductVariantID: item.ProductVariantID,
|
ProductVariantID: item.ProductVariantID,
|
||||||
ProductVariantName: item.ProductVariantName,
|
ProductVariantName: item.ProductVariantName,
|
||||||
|
CategoryID: item.CategoryID,
|
||||||
|
CategoryName: item.CategoryName,
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
UnitPrice: item.UnitPrice,
|
UnitPrice: item.UnitPrice,
|
||||||
TotalPrice: item.TotalPrice,
|
TotalPrice: item.TotalPrice,
|
||||||
@@ -110,6 +112,7 @@ func OrderModelToContract(resp *models.OrderResponse) *contract.OrderResponse {
|
|||||||
CreatedAt: item.CreatedAt,
|
CreatedAt: item.CreatedAt,
|
||||||
UpdatedAt: item.UpdatedAt,
|
UpdatedAt: item.UpdatedAt,
|
||||||
PrinterType: item.PrinterType,
|
PrinterType: item.PrinterType,
|
||||||
|
PrintToChecker: item.PrintToChecker,
|
||||||
PaidQuantity: item.PaidQuantity,
|
PaidQuantity: item.PaidQuantity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,6 +171,8 @@ func AddToOrderModelToContract(resp *models.AddToOrderResponse) *contract.AddToO
|
|||||||
ProductName: item.ProductName,
|
ProductName: item.ProductName,
|
||||||
ProductVariantID: item.ProductVariantID,
|
ProductVariantID: item.ProductVariantID,
|
||||||
ProductVariantName: item.ProductVariantName,
|
ProductVariantName: item.ProductVariantName,
|
||||||
|
CategoryID: item.CategoryID,
|
||||||
|
CategoryName: item.CategoryName,
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
UnitPrice: item.UnitPrice,
|
UnitPrice: item.UnitPrice,
|
||||||
TotalPrice: item.TotalPrice,
|
TotalPrice: item.TotalPrice,
|
||||||
@@ -177,6 +182,7 @@ func AddToOrderModelToContract(resp *models.AddToOrderResponse) *contract.AddToO
|
|||||||
Status: string(item.Status),
|
Status: string(item.Status),
|
||||||
CreatedAt: item.CreatedAt,
|
CreatedAt: item.CreatedAt,
|
||||||
UpdatedAt: item.UpdatedAt,
|
UpdatedAt: item.UpdatedAt,
|
||||||
|
PrintToChecker: item.PrintToChecker,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &contract.AddToOrderResponse{
|
return &contract.AddToOrderResponse{
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ func CreateProductOutletPriceRequestToModel(req *contract.CreateProductOutletPri
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &models.CreateProductOutletPriceRequest{
|
return &models.CreateProductOutletPriceRequest{
|
||||||
ProductID: req.ProductID,
|
ProductID: req.ProductID,
|
||||||
OutletID: req.OutletID,
|
OutletID: req.OutletID,
|
||||||
Price: req.Price,
|
Price: req.Price,
|
||||||
|
PrintToChecker: req.PrintToChecker,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +24,8 @@ func UpdateProductOutletPriceRequestToModel(req *contract.UpdateProductOutletPri
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &models.UpdateProductOutletPriceRequest{
|
return &models.UpdateProductOutletPriceRequest{
|
||||||
Price: &req.Price,
|
Price: &req.Price,
|
||||||
|
PrintToChecker: req.PrintToChecker,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,12 +35,13 @@ func ProductOutletPriceModelToResponse(m *models.ProductOutletPrice) *contract.P
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &contract.ProductOutletPriceResponse{
|
return &contract.ProductOutletPriceResponse{
|
||||||
ID: m.ID,
|
ID: m.ID,
|
||||||
ProductID: m.ProductID,
|
ProductID: m.ProductID,
|
||||||
OutletID: m.OutletID,
|
OutletID: m.OutletID,
|
||||||
Price: m.Price,
|
Price: m.Price,
|
||||||
CreatedAt: m.CreatedAt,
|
PrintToChecker: m.PrintToChecker,
|
||||||
UpdatedAt: m.UpdatedAt,
|
CreatedAt: m.CreatedAt,
|
||||||
|
UpdatedAt: m.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"apskel-pos-be/internal/constants"
|
"apskel-pos-be/internal/constants"
|
||||||
"apskel-pos-be/internal/contract"
|
"apskel-pos-be/internal/contract"
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.CreateProductRequest) *models.CreateProductRequest {
|
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{})
|
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{
|
return &models.CreateProductRequest{
|
||||||
OrganizationID: apctx.OrganizationID,
|
OrganizationID: apctx.OrganizationID,
|
||||||
|
OutletID: outletID,
|
||||||
CategoryID: req.CategoryID,
|
CategoryID: req.CategoryID,
|
||||||
SKU: req.SKU,
|
SKU: req.SKU,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
@@ -48,28 +57,37 @@ func CreateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.Cr
|
|||||||
BusinessType: businessType,
|
BusinessType: businessType,
|
||||||
ImageURL: req.ImageURL,
|
ImageURL: req.ImageURL,
|
||||||
PrinterType: req.PrinterType,
|
PrinterType: req.PrinterType,
|
||||||
|
PrintToChecker: req.PrintToChecker,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
Variants: variants,
|
Variants: variants,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateProductRequestToModel(req *contract.UpdateProductRequest) *models.UpdateProductRequest {
|
func UpdateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.UpdateProductRequest) *models.UpdateProductRequest {
|
||||||
metadata := req.Metadata
|
metadata := req.Metadata
|
||||||
if metadata == nil {
|
if metadata == nil {
|
||||||
metadata = make(map[string]interface{})
|
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{
|
return &models.UpdateProductRequest{
|
||||||
CategoryID: req.CategoryID,
|
OutletID: outletID,
|
||||||
SKU: req.SKU,
|
CategoryID: req.CategoryID,
|
||||||
Name: req.Name,
|
SKU: req.SKU,
|
||||||
Description: req.Description,
|
Name: req.Name,
|
||||||
Price: req.Price,
|
Description: req.Description,
|
||||||
Cost: req.Cost,
|
Price: req.Price,
|
||||||
ImageURL: req.ImageURL,
|
Cost: req.Cost,
|
||||||
PrinterType: req.PrinterType,
|
ImageURL: req.ImageURL,
|
||||||
Metadata: metadata,
|
PrinterType: req.PrinterType,
|
||||||
IsActive: req.IsActive,
|
PrintToChecker: req.PrintToChecker,
|
||||||
|
Metadata: metadata,
|
||||||
|
IsActive: req.IsActive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,9 +121,10 @@ func ProductModelResponseToResponse(prod *models.ProductResponse) *contract.Prod
|
|||||||
outletPriceResponses = make([]contract.ProductOutletPriceResponse, len(prod.OutletPrices))
|
outletPriceResponses = make([]contract.ProductOutletPriceResponse, len(prod.OutletPrices))
|
||||||
for i, op := range prod.OutletPrices {
|
for i, op := range prod.OutletPrices {
|
||||||
outletPriceResponses[i] = contract.ProductOutletPriceResponse{
|
outletPriceResponses[i] = contract.ProductOutletPriceResponse{
|
||||||
OutletID: op.OutletID,
|
OutletID: op.OutletID,
|
||||||
OutletName: op.OutletName,
|
OutletName: op.OutletName,
|
||||||
Price: op.Price,
|
Price: op.Price,
|
||||||
|
PrintToChecker: op.PrintToChecker,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,6 +144,7 @@ func ProductModelResponseToResponse(prod *models.ProductResponse) *contract.Prod
|
|||||||
BusinessType: string(prod.BusinessType),
|
BusinessType: string(prod.BusinessType),
|
||||||
ImageURL: prod.ImageURL,
|
ImageURL: prod.ImageURL,
|
||||||
PrinterType: prod.PrinterType,
|
PrinterType: prod.PrinterType,
|
||||||
|
PrintToChecker: prod.PrintToChecker,
|
||||||
Metadata: prod.Metadata,
|
Metadata: prod.Metadata,
|
||||||
IsActive: prod.IsActive,
|
IsActive: prod.IsActive,
|
||||||
CreatedAt: prod.CreatedAt,
|
CreatedAt: prod.CreatedAt,
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE product_outlet_prices DROP COLUMN IF EXISTS print_to_checker;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE product_outlet_prices ADD COLUMN print_to_checker BOOLEAN NOT NULL DEFAULT TRUE;
|
||||||
Reference in New Issue
Block a user