Add category table
This commit is contained in:
parent
094e8b2a47
commit
69d8c8ce5e
@ -107,6 +107,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
validators.vendorValidator,
|
validators.vendorValidator,
|
||||||
services.purchaseOrderService,
|
services.purchaseOrderService,
|
||||||
validators.purchaseOrderValidator,
|
validators.purchaseOrderValidator,
|
||||||
|
services.purchaseCategoryService,
|
||||||
|
validators.purchaseCategoryValidator,
|
||||||
services.unitConverterService,
|
services.unitConverterService,
|
||||||
validators.unitConverterValidator,
|
validators.unitConverterValidator,
|
||||||
services.chartOfAccountTypeService,
|
services.chartOfAccountTypeService,
|
||||||
@ -214,6 +216,7 @@ type repositories struct {
|
|||||||
productRecipeRepo *repository.ProductRecipeRepository
|
productRecipeRepo *repository.ProductRecipeRepository
|
||||||
vendorRepo *repository.VendorRepositoryImpl
|
vendorRepo *repository.VendorRepositoryImpl
|
||||||
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
|
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
|
||||||
|
purchaseCategoryRepo *repository.PurchaseCategoryRepositoryImpl
|
||||||
unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl
|
unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl
|
||||||
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
|
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
|
||||||
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
||||||
@ -267,6 +270,7 @@ func (a *App) initRepositories() *repositories {
|
|||||||
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
||||||
vendorRepo: repository.NewVendorRepositoryImpl(a.db),
|
vendorRepo: repository.NewVendorRepositoryImpl(a.db),
|
||||||
purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db),
|
purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db),
|
||||||
|
purchaseCategoryRepo: repository.NewPurchaseCategoryRepositoryImpl(a.db),
|
||||||
unitConverterRepo: repository.NewIngredientUnitConverterRepositoryImpl(a.db).(*repository.IngredientUnitConverterRepositoryImpl),
|
unitConverterRepo: repository.NewIngredientUnitConverterRepositoryImpl(a.db).(*repository.IngredientUnitConverterRepositoryImpl),
|
||||||
chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db),
|
chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db),
|
||||||
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
|
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
|
||||||
@ -315,6 +319,7 @@ type processors struct {
|
|||||||
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
||||||
vendorProcessor *processor.VendorProcessorImpl
|
vendorProcessor *processor.VendorProcessorImpl
|
||||||
purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl
|
purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl
|
||||||
|
purchaseCategoryProcessor *processor.PurchaseCategoryProcessorImpl
|
||||||
unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl
|
unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl
|
||||||
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
|
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
|
||||||
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
||||||
@ -366,6 +371,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
||||||
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
|
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
|
||||||
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
||||||
|
purchaseCategoryProcessor: processor.NewPurchaseCategoryProcessorImpl(repos.purchaseCategoryRepo),
|
||||||
unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
|
unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
|
||||||
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
|
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
|
||||||
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
|
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
|
||||||
@ -414,6 +420,7 @@ type services struct {
|
|||||||
productRecipeService *service.ProductRecipeServiceImpl
|
productRecipeService *service.ProductRecipeServiceImpl
|
||||||
vendorService *service.VendorServiceImpl
|
vendorService *service.VendorServiceImpl
|
||||||
purchaseOrderService *service.PurchaseOrderServiceImpl
|
purchaseOrderService *service.PurchaseOrderServiceImpl
|
||||||
|
purchaseCategoryService service.PurchaseCategoryService
|
||||||
unitConverterService *service.IngredientUnitConverterServiceImpl
|
unitConverterService *service.IngredientUnitConverterServiceImpl
|
||||||
chartOfAccountTypeService service.ChartOfAccountTypeService
|
chartOfAccountTypeService service.ChartOfAccountTypeService
|
||||||
chartOfAccountService service.ChartOfAccountService
|
chartOfAccountService service.ChartOfAccountService
|
||||||
@ -453,6 +460,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor)
|
productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor)
|
||||||
vendorService := service.NewVendorService(processors.vendorProcessor)
|
vendorService := service.NewVendorService(processors.vendorProcessor)
|
||||||
purchaseOrderService := service.NewPurchaseOrderService(processors.purchaseOrderProcessor)
|
purchaseOrderService := service.NewPurchaseOrderService(processors.purchaseOrderProcessor)
|
||||||
|
purchaseCategoryService := service.NewPurchaseCategoryService(processors.purchaseCategoryProcessor)
|
||||||
unitConverterService := service.NewIngredientUnitConverterService(processors.unitConverterProcessor)
|
unitConverterService := service.NewIngredientUnitConverterService(processors.unitConverterProcessor)
|
||||||
chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor)
|
chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor)
|
||||||
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
|
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
|
||||||
@ -492,6 +500,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
productRecipeService: productRecipeService,
|
productRecipeService: productRecipeService,
|
||||||
vendorService: vendorService,
|
vendorService: vendorService,
|
||||||
purchaseOrderService: purchaseOrderService,
|
purchaseOrderService: purchaseOrderService,
|
||||||
|
purchaseCategoryService: purchaseCategoryService,
|
||||||
unitConverterService: unitConverterService,
|
unitConverterService: unitConverterService,
|
||||||
chartOfAccountTypeService: chartOfAccountTypeService,
|
chartOfAccountTypeService: chartOfAccountTypeService,
|
||||||
chartOfAccountService: chartOfAccountService,
|
chartOfAccountService: chartOfAccountService,
|
||||||
@ -537,6 +546,7 @@ type validators struct {
|
|||||||
tableValidator *validator.TableValidator
|
tableValidator *validator.TableValidator
|
||||||
vendorValidator *validator.VendorValidatorImpl
|
vendorValidator *validator.VendorValidatorImpl
|
||||||
purchaseOrderValidator *validator.PurchaseOrderValidatorImpl
|
purchaseOrderValidator *validator.PurchaseOrderValidatorImpl
|
||||||
|
purchaseCategoryValidator *validator.PurchaseCategoryValidatorImpl
|
||||||
unitConverterValidator *validator.IngredientUnitConverterValidatorImpl
|
unitConverterValidator *validator.IngredientUnitConverterValidatorImpl
|
||||||
chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl
|
chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl
|
||||||
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
||||||
@ -568,6 +578,7 @@ func (a *App) initValidators() *validators {
|
|||||||
tableValidator: validator.NewTableValidator(),
|
tableValidator: validator.NewTableValidator(),
|
||||||
vendorValidator: validator.NewVendorValidator(),
|
vendorValidator: validator.NewVendorValidator(),
|
||||||
purchaseOrderValidator: validator.NewPurchaseOrderValidator(),
|
purchaseOrderValidator: validator.NewPurchaseOrderValidator(),
|
||||||
|
purchaseCategoryValidator: validator.NewPurchaseCategoryValidator(),
|
||||||
unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl),
|
unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl),
|
||||||
chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl),
|
chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl),
|
||||||
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
|
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
|
||||||
|
|||||||
@ -40,6 +40,7 @@ const (
|
|||||||
OutletServiceEntity = "outlet_service"
|
OutletServiceEntity = "outlet_service"
|
||||||
VendorServiceEntity = "vendor_service"
|
VendorServiceEntity = "vendor_service"
|
||||||
PurchaseOrderServiceEntity = "purchase_order_service"
|
PurchaseOrderServiceEntity = "purchase_order_service"
|
||||||
|
PurchaseCategoryServiceEntity = "purchase_category_service"
|
||||||
IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service"
|
IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service"
|
||||||
IngredientCompositionServiceEntity = "ingredient_composition_service"
|
IngredientCompositionServiceEntity = "ingredient_composition_service"
|
||||||
TableEntity = "table"
|
TableEntity = "table"
|
||||||
|
|||||||
57
internal/contract/purchase_category_contract.go
Normal file
57
internal/contract/purchase_category_contract.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreatePurchaseCategoryRequest struct {
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Code *string `json:"code,omitempty"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=raw_material non_inventory"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePurchaseCategoryRequest struct {
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Code *string `json:"code,omitempty"`
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
|
Type *string `json:"type,omitempty" validate:"omitempty,oneof=raw_material non_inventory"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPurchaseCategoriesRequest struct {
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Type string `json:"type,omitempty" validate:"omitempty,oneof=raw_material non_inventory"`
|
||||||
|
Search string `json:"search,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
Page int `json:"page" validate:"required,min=1"`
|
||||||
|
Limit int `json:"limit" validate:"required,min=1,max=100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategoryResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
PresetID *uuid.UUID `json:"preset_id"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
IsSystem bool `json:"is_system"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPurchaseCategoriesResponse struct {
|
||||||
|
PurchaseCategories []PurchaseCategoryResponse `json:"purchase_categories"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
71
internal/entities/purchase_category.go
Normal file
71
internal/entities/purchase_category.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PurchaseCategoryTypeRawMaterial PurchaseCategoryType = "raw_material"
|
||||||
|
PurchaseCategoryTypeNonInventory PurchaseCategoryType = "non_inventory"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryPreset struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
ParentID *uuid.UUID `gorm:"type:uuid;index" json:"parent_id"`
|
||||||
|
Code string `gorm:"not null;unique;size:100" json:"code"`
|
||||||
|
Name string `gorm:"not null;size:255" json:"name"`
|
||||||
|
Type PurchaseCategoryType `gorm:"not null;size:20" json:"type"`
|
||||||
|
SortOrder int `gorm:"not null;default:0" json:"sort_order"`
|
||||||
|
IsActive bool `gorm:"not null;default:true" json:"is_active"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Parent *PurchaseCategoryPreset `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PurchaseCategoryPreset) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if p.ID == uuid.Nil {
|
||||||
|
p.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PurchaseCategoryPreset) TableName() string {
|
||||||
|
return "purchase_category_presets"
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategory struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"`
|
||||||
|
PresetID *uuid.UUID `gorm:"type:uuid;index" json:"preset_id"`
|
||||||
|
ParentID *uuid.UUID `gorm:"type:uuid;index" json:"parent_id"`
|
||||||
|
Code string `gorm:"not null;size:100" json:"code"`
|
||||||
|
Name string `gorm:"not null;size:255" json:"name"`
|
||||||
|
Type PurchaseCategoryType `gorm:"not null;size:20" json:"type"`
|
||||||
|
SortOrder int `gorm:"not null;default:0" json:"sort_order"`
|
||||||
|
IsSystem bool `gorm:"not null;default:false" json:"is_system"`
|
||||||
|
IsActive bool `gorm:"not null;default:true" json:"is_active"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||||
|
Preset *PurchaseCategoryPreset `gorm:"foreignKey:PresetID" json:"preset,omitempty"`
|
||||||
|
Parent *PurchaseCategory `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||||
|
Children []PurchaseCategory `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PurchaseCategory) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if c.ID == uuid.Nil {
|
||||||
|
c.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PurchaseCategory) TableName() string {
|
||||||
|
return "purchase_categories"
|
||||||
|
}
|
||||||
160
internal/handler/purchase_category_handler.go
Normal file
160
internal/handler/purchase_category_handler.go
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/appcontext"
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/service"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
"apskel-pos-be/internal/validator"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryHandler struct {
|
||||||
|
purchaseCategoryService service.PurchaseCategoryService
|
||||||
|
purchaseCategoryValidator validator.PurchaseCategoryValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseCategoryHandler(purchaseCategoryService service.PurchaseCategoryService, purchaseCategoryValidator validator.PurchaseCategoryValidator) *PurchaseCategoryHandler {
|
||||||
|
return &PurchaseCategoryHandler{
|
||||||
|
purchaseCategoryService: purchaseCategoryService,
|
||||||
|
purchaseCategoryValidator: purchaseCategoryValidator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) CreatePurchaseCategory(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.CreatePurchaseCategoryRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("PurchaseCategoryHandler::CreatePurchaseCategory -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::CreatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationError, validationErrorCode := h.purchaseCategoryValidator.ValidateCreatePurchaseCategoryRequest(&req); validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::CreatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.CreatePurchaseCategory(ctx, contextInfo, &req)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::CreatePurchaseCategory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) UpdatePurchaseCategory(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
categoryID, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase category ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.UpdatePurchaseCategoryRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("PurchaseCategoryHandler::UpdatePurchaseCategory -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationError, validationErrorCode := h.purchaseCategoryValidator.ValidateUpdatePurchaseCategoryRequest(&req); validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.UpdatePurchaseCategory(ctx, contextInfo, categoryID, &req)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) DeletePurchaseCategory(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
categoryID, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase category ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::DeletePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.DeletePurchaseCategory(ctx, contextInfo, categoryID)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::DeletePurchaseCategory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) GetPurchaseCategory(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
categoryID, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase category ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::GetPurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.GetPurchaseCategoryByID(ctx, contextInfo, categoryID)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::GetPurchaseCategory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) ListPurchaseCategories(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
req := &contract.ListPurchaseCategoriesRequest{
|
||||||
|
Page: 1,
|
||||||
|
Limit: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageStr := c.Query("page"); pageStr != "" {
|
||||||
|
if page, err := strconv.Atoi(pageStr); err == nil {
|
||||||
|
req.Page = page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if limitStr := c.Query("limit"); limitStr != "" {
|
||||||
|
if limit, err := strconv.Atoi(limitStr); err == nil {
|
||||||
|
req.Limit = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parentIDStr := c.Query("parent_id"); parentIDStr != "" {
|
||||||
|
if parentID, err := uuid.Parse(parentIDStr); err == nil {
|
||||||
|
req.ParentID = &parentID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if categoryType := c.Query("type"); categoryType != "" {
|
||||||
|
req.Type = categoryType
|
||||||
|
}
|
||||||
|
|
||||||
|
if search := c.Query("search"); search != "" {
|
||||||
|
req.Search = search
|
||||||
|
}
|
||||||
|
|
||||||
|
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
|
||||||
|
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
||||||
|
req.IsActive = &isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationError, validationErrorCode := h.purchaseCategoryValidator.ValidateListPurchaseCategoriesRequest(req); validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::ListPurchaseCategories")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.ListPurchaseCategories(ctx, contextInfo, req)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::ListPurchaseCategories")
|
||||||
|
}
|
||||||
53
internal/mappers/purchase_category_mapper.go
Normal file
53
internal/mappers/purchase_category_mapper.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreatePurchaseCategoryRequestToEntity(req *models.CreatePurchaseCategoryRequest) *entities.PurchaseCategory {
|
||||||
|
if req == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entities.PurchaseCategory{
|
||||||
|
OrganizationID: req.OrganizationID,
|
||||||
|
ParentID: req.ParentID,
|
||||||
|
Name: req.Name,
|
||||||
|
Type: entities.PurchaseCategoryType(req.Type),
|
||||||
|
SortOrder: req.SortOrder,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PurchaseCategoryEntityToResponse(entity *entities.PurchaseCategory) *models.PurchaseCategoryResponse {
|
||||||
|
if entity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.PurchaseCategoryResponse{
|
||||||
|
ID: entity.ID,
|
||||||
|
OrganizationID: entity.OrganizationID,
|
||||||
|
PresetID: entity.PresetID,
|
||||||
|
ParentID: entity.ParentID,
|
||||||
|
Code: entity.Code,
|
||||||
|
Name: entity.Name,
|
||||||
|
Type: string(entity.Type),
|
||||||
|
SortOrder: entity.SortOrder,
|
||||||
|
IsSystem: entity.IsSystem,
|
||||||
|
IsActive: entity.IsActive,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PurchaseCategoryEntitiesToResponses(categoryEntities []*entities.PurchaseCategory) []models.PurchaseCategoryResponse {
|
||||||
|
responses := make([]models.PurchaseCategoryResponse, len(categoryEntities))
|
||||||
|
for i, entity := range categoryEntities {
|
||||||
|
response := PurchaseCategoryEntityToResponse(entity)
|
||||||
|
if response != nil {
|
||||||
|
responses[i] = *response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
51
internal/models/purchase_category.go
Normal file
51
internal/models/purchase_category.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryResponse struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
PresetID *uuid.UUID
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
Code string
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
SortOrder int
|
||||||
|
IsSystem bool
|
||||||
|
IsActive bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePurchaseCategoryRequest struct {
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
Code *string
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
SortOrder int
|
||||||
|
IsActive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePurchaseCategoryRequest struct {
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
Code *string
|
||||||
|
Name *string
|
||||||
|
Type *string
|
||||||
|
SortOrder *int
|
||||||
|
IsActive *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPurchaseCategoriesRequest struct {
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
Type string
|
||||||
|
Search string
|
||||||
|
IsActive *bool
|
||||||
|
Page int
|
||||||
|
Limit int
|
||||||
|
}
|
||||||
210
internal/processor/purchase_category_processor.go
Normal file
210
internal/processor/purchase_category_processor.go
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/mappers"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryProcessor interface {
|
||||||
|
CreatePurchaseCategory(ctx context.Context, req *models.CreatePurchaseCategoryRequest) (*models.PurchaseCategoryResponse, error)
|
||||||
|
UpdatePurchaseCategory(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdatePurchaseCategoryRequest) (*models.PurchaseCategoryResponse, error)
|
||||||
|
DeletePurchaseCategory(ctx context.Context, id, organizationID uuid.UUID) error
|
||||||
|
GetPurchaseCategoryByID(ctx context.Context, id, organizationID uuid.UUID) (*models.PurchaseCategoryResponse, error)
|
||||||
|
ListPurchaseCategories(ctx context.Context, req *models.ListPurchaseCategoriesRequest) ([]models.PurchaseCategoryResponse, int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategoryRepository interface {
|
||||||
|
Create(ctx context.Context, category *entities.PurchaseCategory) error
|
||||||
|
GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.PurchaseCategory, error)
|
||||||
|
Update(ctx context.Context, category *entities.PurchaseCategory) error
|
||||||
|
SoftDelete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||||
|
List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.PurchaseCategory, int64, error)
|
||||||
|
ExistsByCode(ctx context.Context, organizationID uuid.UUID, code string, excludeID *uuid.UUID) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategoryProcessorImpl struct {
|
||||||
|
purchaseCategoryRepo PurchaseCategoryRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseCategoryProcessorImpl(purchaseCategoryRepo PurchaseCategoryRepository) *PurchaseCategoryProcessorImpl {
|
||||||
|
return &PurchaseCategoryProcessorImpl{purchaseCategoryRepo: purchaseCategoryRepo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PurchaseCategoryProcessorImpl) CreatePurchaseCategory(ctx context.Context, req *models.CreatePurchaseCategoryRequest) (*models.PurchaseCategoryResponse, error) {
|
||||||
|
code := ""
|
||||||
|
if req.Code != nil && strings.TrimSpace(*req.Code) != "" {
|
||||||
|
code = normalizePurchaseCategoryCode(*req.Code)
|
||||||
|
} else {
|
||||||
|
code = normalizePurchaseCategoryCode(req.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if code == "" {
|
||||||
|
return nil, fmt.Errorf("purchase category code cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := p.purchaseCategoryRepo.ExistsByCode(ctx, req.OrganizationID, code, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check purchase category code uniqueness: %w", err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return nil, fmt.Errorf("purchase category with code '%s' already exists", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ParentID != nil {
|
||||||
|
parent, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, *req.ParentID, req.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parent purchase category not found: %w", err)
|
||||||
|
}
|
||||||
|
if string(parent.Type) != req.Type {
|
||||||
|
return nil, fmt.Errorf("parent purchase category type must match child type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
category := mappers.CreatePurchaseCategoryRequestToEntity(req)
|
||||||
|
category.Code = code
|
||||||
|
|
||||||
|
if err := p.purchaseCategoryRepo.Create(ctx, category); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create purchase category: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappers.PurchaseCategoryEntityToResponse(category), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PurchaseCategoryProcessorImpl) UpdatePurchaseCategory(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdatePurchaseCategoryRequest) (*models.PurchaseCategoryResponse, error) {
|
||||||
|
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("purchase category not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newType := string(category.Type)
|
||||||
|
if req.Type != nil {
|
||||||
|
newType = *req.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ParentID != nil {
|
||||||
|
if *req.ParentID == id {
|
||||||
|
return nil, fmt.Errorf("purchase category cannot be its own parent")
|
||||||
|
}
|
||||||
|
|
||||||
|
parent, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, *req.ParentID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parent purchase category not found: %w", err)
|
||||||
|
}
|
||||||
|
if string(parent.Type) != newType {
|
||||||
|
return nil, fmt.Errorf("parent purchase category type must match child type")
|
||||||
|
}
|
||||||
|
category.ParentID = req.ParentID
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code != nil {
|
||||||
|
code := normalizePurchaseCategoryCode(*req.Code)
|
||||||
|
if code == "" {
|
||||||
|
return nil, fmt.Errorf("purchase category code cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if code != category.Code {
|
||||||
|
exists, err := p.purchaseCategoryRepo.ExistsByCode(ctx, organizationID, code, &id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check purchase category code uniqueness: %w", err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return nil, fmt.Errorf("purchase category with code '%s' already exists", code)
|
||||||
|
}
|
||||||
|
category.Code = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name != nil {
|
||||||
|
category.Name = strings.TrimSpace(*req.Name)
|
||||||
|
}
|
||||||
|
if req.Type != nil {
|
||||||
|
category.Type = entities.PurchaseCategoryType(*req.Type)
|
||||||
|
}
|
||||||
|
if req.SortOrder != nil {
|
||||||
|
category.SortOrder = *req.SortOrder
|
||||||
|
}
|
||||||
|
if req.IsActive != nil {
|
||||||
|
category.IsActive = *req.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.purchaseCategoryRepo.Update(ctx, category); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update purchase category: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappers.PurchaseCategoryEntityToResponse(category), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PurchaseCategoryProcessorImpl) DeletePurchaseCategory(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||||
|
_, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("purchase category not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.purchaseCategoryRepo.SoftDelete(ctx, id, organizationID); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete purchase category: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PurchaseCategoryProcessorImpl) GetPurchaseCategoryByID(ctx context.Context, id, organizationID uuid.UUID) (*models.PurchaseCategoryResponse, error) {
|
||||||
|
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("purchase category not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappers.PurchaseCategoryEntityToResponse(category), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PurchaseCategoryProcessorImpl) ListPurchaseCategories(ctx context.Context, req *models.ListPurchaseCategoriesRequest) ([]models.PurchaseCategoryResponse, int, error) {
|
||||||
|
filters := make(map[string]interface{})
|
||||||
|
if req.ParentID != nil {
|
||||||
|
filters["parent_id"] = *req.ParentID
|
||||||
|
}
|
||||||
|
if req.Type != "" {
|
||||||
|
filters["type"] = req.Type
|
||||||
|
}
|
||||||
|
if req.Search != "" {
|
||||||
|
filters["search"] = req.Search
|
||||||
|
}
|
||||||
|
if req.IsActive != nil {
|
||||||
|
filters["is_active"] = *req.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (req.Page - 1) * req.Limit
|
||||||
|
categories, total, err := p.purchaseCategoryRepo.List(ctx, req.OrganizationID, filters, req.Limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("failed to list purchase categories: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappers.PurchaseCategoryEntitiesToResponses(categories), int(total), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePurchaseCategoryCode(value string) string {
|
||||||
|
value = strings.TrimSpace(strings.ToLower(value))
|
||||||
|
var builder strings.Builder
|
||||||
|
lastUnderscore := false
|
||||||
|
|
||||||
|
for _, r := range value {
|
||||||
|
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||||
|
builder.WriteRune(r)
|
||||||
|
lastUnderscore = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !lastUnderscore {
|
||||||
|
builder.WriteRune('_')
|
||||||
|
lastUnderscore = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Trim(builder.String(), "_")
|
||||||
|
}
|
||||||
91
internal/repository/purchase_category_repository.go
Normal file
91
internal/repository/purchase_category_repository.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryRepositoryImpl struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseCategoryRepositoryImpl(db *gorm.DB) *PurchaseCategoryRepositoryImpl {
|
||||||
|
return &PurchaseCategoryRepositoryImpl{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseCategoryRepositoryImpl) Create(ctx context.Context, category *entities.PurchaseCategory) error {
|
||||||
|
return r.db.WithContext(ctx).Create(category).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseCategoryRepositoryImpl) GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.PurchaseCategory, error) {
|
||||||
|
var category entities.PurchaseCategory
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
First(&category, "id = ? AND organization_id = ?", id, organizationID).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &category, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseCategoryRepositoryImpl) Update(ctx context.Context, category *entities.PurchaseCategory) error {
|
||||||
|
return r.db.WithContext(ctx).Save(category).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseCategoryRepositoryImpl) SoftDelete(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Model(&entities.PurchaseCategory{}).
|
||||||
|
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||||
|
Update("is_active", false).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseCategoryRepositoryImpl) List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.PurchaseCategory, int64, error) {
|
||||||
|
var categories []*entities.PurchaseCategory
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := r.db.WithContext(ctx).
|
||||||
|
Model(&entities.PurchaseCategory{}).
|
||||||
|
Where("organization_id = ?", organizationID)
|
||||||
|
|
||||||
|
for key, value := range filters {
|
||||||
|
switch key {
|
||||||
|
case "search":
|
||||||
|
searchValue := "%" + value.(string) + "%"
|
||||||
|
query = query.Where("name ILIKE ? OR code ILIKE ?", searchValue, searchValue)
|
||||||
|
case "parent_id":
|
||||||
|
query = query.Where("parent_id = ?", value)
|
||||||
|
case "type":
|
||||||
|
query = query.Where("type = ?", value)
|
||||||
|
case "is_active":
|
||||||
|
query = query.Where("is_active = ?", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.
|
||||||
|
Order("parent_id NULLS FIRST, sort_order ASC, name ASC").
|
||||||
|
Limit(limit).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&categories).Error
|
||||||
|
return categories, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseCategoryRepositoryImpl) ExistsByCode(ctx context.Context, organizationID uuid.UUID, code string, excludeID *uuid.UUID) (bool, error) {
|
||||||
|
query := r.db.WithContext(ctx).
|
||||||
|
Model(&entities.PurchaseCategory{}).
|
||||||
|
Where("organization_id = ? AND code = ?", organizationID, code)
|
||||||
|
|
||||||
|
if excludeID != nil {
|
||||||
|
query = query.Where("id != ?", *excludeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
err := query.Count(&count).Error
|
||||||
|
return count > 0, err
|
||||||
|
}
|
||||||
@ -35,6 +35,7 @@ type Router struct {
|
|||||||
productRecipeHandler *handler.ProductRecipeHandler
|
productRecipeHandler *handler.ProductRecipeHandler
|
||||||
vendorHandler *handler.VendorHandler
|
vendorHandler *handler.VendorHandler
|
||||||
purchaseOrderHandler *handler.PurchaseOrderHandler
|
purchaseOrderHandler *handler.PurchaseOrderHandler
|
||||||
|
purchaseCategoryHandler *handler.PurchaseCategoryHandler
|
||||||
unitConverterHandler *handler.IngredientUnitConverterHandler
|
unitConverterHandler *handler.IngredientUnitConverterHandler
|
||||||
chartOfAccountTypeHandler *handler.ChartOfAccountTypeHandler
|
chartOfAccountTypeHandler *handler.ChartOfAccountTypeHandler
|
||||||
chartOfAccountHandler *handler.ChartOfAccountHandler
|
chartOfAccountHandler *handler.ChartOfAccountHandler
|
||||||
@ -55,7 +56,7 @@ type Router struct {
|
|||||||
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, userDeviceService service.UserDeviceService, userDeviceValidator validator.UserDeviceValidator, notificationService service.NotificationService, notificationValidator validator.NotificationValidator, productOutletPriceService service.ProductOutletPriceService, productOutletPriceValidator validator.ProductOutletPriceValidator, selfOrderHandler *handler.SelfOrderHandler, expenseService *service.ExpenseServiceImpl, expenseValidator *validator.ExpenseValidatorImpl) *Router {
|
func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, purchaseCategoryService service.PurchaseCategoryService, purchaseCategoryValidator validator.PurchaseCategoryValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, userDeviceService service.UserDeviceService, userDeviceValidator validator.UserDeviceValidator, notificationService service.NotificationService, notificationValidator validator.NotificationValidator, productOutletPriceService service.ProductOutletPriceService, productOutletPriceValidator validator.ProductOutletPriceValidator, selfOrderHandler *handler.SelfOrderHandler, expenseService *service.ExpenseServiceImpl, expenseValidator *validator.ExpenseValidatorImpl) *Router {
|
||||||
|
|
||||||
return &Router{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@ -80,6 +81,7 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer
|
|||||||
productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService),
|
productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService),
|
||||||
vendorHandler: handler.NewVendorHandler(vendorService, vendorValidator),
|
vendorHandler: handler.NewVendorHandler(vendorService, vendorValidator),
|
||||||
purchaseOrderHandler: handler.NewPurchaseOrderHandler(purchaseOrderService, purchaseOrderValidator),
|
purchaseOrderHandler: handler.NewPurchaseOrderHandler(purchaseOrderService, purchaseOrderValidator),
|
||||||
|
purchaseCategoryHandler: handler.NewPurchaseCategoryHandler(purchaseCategoryService, purchaseCategoryValidator),
|
||||||
unitConverterHandler: handler.NewIngredientUnitConverterHandler(unitConverterService, unitConverterValidator),
|
unitConverterHandler: handler.NewIngredientUnitConverterHandler(unitConverterService, unitConverterValidator),
|
||||||
chartOfAccountTypeHandler: handler.NewChartOfAccountTypeHandler(chartOfAccountTypeService, chartOfAccountTypeValidator),
|
chartOfAccountTypeHandler: handler.NewChartOfAccountTypeHandler(chartOfAccountTypeService, chartOfAccountTypeValidator),
|
||||||
chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator),
|
chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator),
|
||||||
@ -384,6 +386,16 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
purchaseOrders.DELETE("/:id", r.purchaseOrderHandler.DeletePurchaseOrder)
|
purchaseOrders.DELETE("/:id", r.purchaseOrderHandler.DeletePurchaseOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
purchaseCategories := protected.Group("/purchase-categories")
|
||||||
|
purchaseCategories.Use(r.authMiddleware.RequireAdminOrManager())
|
||||||
|
{
|
||||||
|
purchaseCategories.POST("", r.purchaseCategoryHandler.CreatePurchaseCategory)
|
||||||
|
purchaseCategories.GET("", r.purchaseCategoryHandler.ListPurchaseCategories)
|
||||||
|
purchaseCategories.GET("/:id", r.purchaseCategoryHandler.GetPurchaseCategory)
|
||||||
|
purchaseCategories.PUT("/:id", r.purchaseCategoryHandler.UpdatePurchaseCategory)
|
||||||
|
purchaseCategories.DELETE("/:id", r.purchaseCategoryHandler.DeletePurchaseCategory)
|
||||||
|
}
|
||||||
|
|
||||||
unitConverters := protected.Group("/unit-converters")
|
unitConverters := protected.Group("/unit-converters")
|
||||||
unitConverters.Use(r.authMiddleware.RequireAdminOrManager())
|
unitConverters.Use(r.authMiddleware.RequireAdminOrManager())
|
||||||
{
|
{
|
||||||
|
|||||||
107
internal/service/purchase_category_service.go
Normal file
107
internal/service/purchase_category_service.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/appcontext"
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"apskel-pos-be/internal/processor"
|
||||||
|
"apskel-pos-be/internal/transformer"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryService interface {
|
||||||
|
CreatePurchaseCategory(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreatePurchaseCategoryRequest) *contract.Response
|
||||||
|
UpdatePurchaseCategory(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdatePurchaseCategoryRequest) *contract.Response
|
||||||
|
DeletePurchaseCategory(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
||||||
|
GetPurchaseCategoryByID(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
||||||
|
ListPurchaseCategories(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListPurchaseCategoriesRequest) *contract.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategoryServiceImpl struct {
|
||||||
|
purchaseCategoryProcessor processor.PurchaseCategoryProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseCategoryService(purchaseCategoryProcessor processor.PurchaseCategoryProcessor) *PurchaseCategoryServiceImpl {
|
||||||
|
return &PurchaseCategoryServiceImpl{purchaseCategoryProcessor: purchaseCategoryProcessor}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseCategoryServiceImpl) CreatePurchaseCategory(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreatePurchaseCategoryRequest) *contract.Response {
|
||||||
|
modelReq := transformer.CreatePurchaseCategoryRequestToModel(apctx, req)
|
||||||
|
|
||||||
|
category, err := s.purchaseCategoryProcessor.CreatePurchaseCategory(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseCategoryServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(transformer.PurchaseCategoryModelResponseToResponse(category))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseCategoryServiceImpl) UpdatePurchaseCategory(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdatePurchaseCategoryRequest) *contract.Response {
|
||||||
|
modelReq := transformer.UpdatePurchaseCategoryRequestToModel(req)
|
||||||
|
|
||||||
|
category, err := s.purchaseCategoryProcessor.UpdatePurchaseCategory(ctx, id, apctx.OrganizationID, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseCategoryServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(transformer.PurchaseCategoryModelResponseToResponse(category))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseCategoryServiceImpl) DeletePurchaseCategory(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response {
|
||||||
|
err := s.purchaseCategoryProcessor.DeletePurchaseCategory(ctx, id, apctx.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseCategoryServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(map[string]interface{}{
|
||||||
|
"message": "Purchase category deleted successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseCategoryServiceImpl) GetPurchaseCategoryByID(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response {
|
||||||
|
category, err := s.purchaseCategoryProcessor.GetPurchaseCategoryByID(ctx, id, apctx.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseCategoryServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(transformer.PurchaseCategoryModelResponseToResponse(category))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseCategoryServiceImpl) ListPurchaseCategories(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListPurchaseCategoriesRequest) *contract.Response {
|
||||||
|
modelReq := &models.ListPurchaseCategoriesRequest{
|
||||||
|
OrganizationID: apctx.OrganizationID,
|
||||||
|
ParentID: req.ParentID,
|
||||||
|
Type: req.Type,
|
||||||
|
Search: req.Search,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
Page: req.Page,
|
||||||
|
Limit: req.Limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
categories, totalCount, err := s.purchaseCategoryProcessor.ListPurchaseCategories(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseCategoryServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := totalCount / req.Limit
|
||||||
|
if totalCount%req.Limit > 0 {
|
||||||
|
totalPages++
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(&contract.ListPurchaseCategoriesResponse{
|
||||||
|
PurchaseCategories: transformer.PurchaseCategoryModelResponsesToResponses(categories),
|
||||||
|
TotalCount: totalCount,
|
||||||
|
Page: req.Page,
|
||||||
|
Limit: req.Limit,
|
||||||
|
TotalPages: totalPages,
|
||||||
|
})
|
||||||
|
}
|
||||||
72
internal/transformer/purchase_category_transformer.go
Normal file
72
internal/transformer/purchase_category_transformer.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package transformer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/appcontext"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreatePurchaseCategoryRequestToModel(apctx *appcontext.ContextInfo, req *contract.CreatePurchaseCategoryRequest) *models.CreatePurchaseCategoryRequest {
|
||||||
|
sortOrder := 0
|
||||||
|
if req.SortOrder != nil {
|
||||||
|
sortOrder = *req.SortOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive := true
|
||||||
|
if req.IsActive != nil {
|
||||||
|
isActive = *req.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.CreatePurchaseCategoryRequest{
|
||||||
|
OrganizationID: apctx.OrganizationID,
|
||||||
|
ParentID: req.ParentID,
|
||||||
|
Code: req.Code,
|
||||||
|
Name: req.Name,
|
||||||
|
Type: req.Type,
|
||||||
|
SortOrder: sortOrder,
|
||||||
|
IsActive: isActive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdatePurchaseCategoryRequestToModel(req *contract.UpdatePurchaseCategoryRequest) *models.UpdatePurchaseCategoryRequest {
|
||||||
|
return &models.UpdatePurchaseCategoryRequest{
|
||||||
|
ParentID: req.ParentID,
|
||||||
|
Code: req.Code,
|
||||||
|
Name: req.Name,
|
||||||
|
Type: req.Type,
|
||||||
|
SortOrder: req.SortOrder,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PurchaseCategoryModelResponseToResponse(category *models.PurchaseCategoryResponse) *contract.PurchaseCategoryResponse {
|
||||||
|
if category == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.PurchaseCategoryResponse{
|
||||||
|
ID: category.ID,
|
||||||
|
OrganizationID: category.OrganizationID,
|
||||||
|
PresetID: category.PresetID,
|
||||||
|
ParentID: category.ParentID,
|
||||||
|
Code: category.Code,
|
||||||
|
Name: category.Name,
|
||||||
|
Type: category.Type,
|
||||||
|
SortOrder: category.SortOrder,
|
||||||
|
IsSystem: category.IsSystem,
|
||||||
|
IsActive: category.IsActive,
|
||||||
|
CreatedAt: category.CreatedAt,
|
||||||
|
UpdatedAt: category.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PurchaseCategoryModelResponsesToResponses(categories []models.PurchaseCategoryResponse) []contract.PurchaseCategoryResponse {
|
||||||
|
responses := make([]contract.PurchaseCategoryResponse, len(categories))
|
||||||
|
for i, category := range categories {
|
||||||
|
response := PurchaseCategoryModelResponseToResponse(&category)
|
||||||
|
if response != nil {
|
||||||
|
responses[i] = *response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
103
internal/validator/purchase_category_validator.go
Normal file
103
internal/validator/purchase_category_validator.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryValidator interface {
|
||||||
|
ValidateCreatePurchaseCategoryRequest(req *contract.CreatePurchaseCategoryRequest) (error, string)
|
||||||
|
ValidateUpdatePurchaseCategoryRequest(req *contract.UpdatePurchaseCategoryRequest) (error, string)
|
||||||
|
ValidateListPurchaseCategoriesRequest(req *contract.ListPurchaseCategoriesRequest) (error, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategoryValidatorImpl struct{}
|
||||||
|
|
||||||
|
func NewPurchaseCategoryValidator() *PurchaseCategoryValidatorImpl {
|
||||||
|
return &PurchaseCategoryValidatorImpl{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *PurchaseCategoryValidatorImpl) ValidateCreatePurchaseCategoryRequest(req *contract.CreatePurchaseCategoryRequest) (error, string) {
|
||||||
|
if req == nil {
|
||||||
|
return errors.New("request body is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(req.Name) == "" {
|
||||||
|
return errors.New("name is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Name) > 255 {
|
||||||
|
return errors.New("name cannot exceed 255 characters"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidPurchaseCategoryType(req.Type) {
|
||||||
|
return errors.New("type must be raw_material or non_inventory"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code != nil && len(strings.TrimSpace(*req.Code)) > 100 {
|
||||||
|
return errors.New("code cannot exceed 100 characters"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *PurchaseCategoryValidatorImpl) ValidateUpdatePurchaseCategoryRequest(req *contract.UpdatePurchaseCategoryRequest) (error, string) {
|
||||||
|
if req == nil {
|
||||||
|
return errors.New("request body is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ParentID == nil && req.Code == nil && req.Name == nil && req.Type == nil && req.SortOrder == nil && req.IsActive == nil {
|
||||||
|
return errors.New("at least one field must be provided for update"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name != nil {
|
||||||
|
if strings.TrimSpace(*req.Name) == "" {
|
||||||
|
return errors.New("name cannot be empty"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
if len(*req.Name) > 255 {
|
||||||
|
return errors.New("name cannot exceed 255 characters"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Type != nil && !isValidPurchaseCategoryType(*req.Type) {
|
||||||
|
return errors.New("type must be raw_material or non_inventory"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code != nil && len(strings.TrimSpace(*req.Code)) > 100 {
|
||||||
|
return errors.New("code cannot exceed 100 characters"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *PurchaseCategoryValidatorImpl) ValidateListPurchaseCategoriesRequest(req *contract.ListPurchaseCategoriesRequest) (error, string) {
|
||||||
|
if req == nil {
|
||||||
|
return errors.New("request is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Page < 1 {
|
||||||
|
return errors.New("page must be at least 1"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Limit < 1 || req.Limit > 100 {
|
||||||
|
return errors.New("limit must be between 1 and 100"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Type != "" && !isValidPurchaseCategoryType(req.Type) {
|
||||||
|
return errors.New("type must be raw_material or non_inventory"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidPurchaseCategoryType(categoryType string) bool {
|
||||||
|
switch categoryType {
|
||||||
|
case "raw_material", "non_inventory":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
2
migrations/000075_create_purchase_categories.down.sql
Normal file
2
migrations/000075_create_purchase_categories.down.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE IF EXISTS purchase_categories;
|
||||||
|
DROP TABLE IF EXISTS purchase_category_presets;
|
||||||
64
migrations/000075_create_purchase_categories.up.sql
Normal file
64
migrations/000075_create_purchase_categories.up.sql
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
CREATE TABLE purchase_category_presets (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
parent_id UUID REFERENCES purchase_category_presets(id) ON DELETE SET NULL,
|
||||||
|
code VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'non_inventory')),
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE purchase_categories (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
preset_id UUID REFERENCES purchase_category_presets(id) ON DELETE SET NULL,
|
||||||
|
parent_id UUID REFERENCES purchase_categories(id) ON DELETE SET NULL,
|
||||||
|
code VARCHAR(100) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'non_inventory')),
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_system BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
UNIQUE (organization_id, code)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_purchase_category_presets_parent_id ON purchase_category_presets(parent_id);
|
||||||
|
CREATE INDEX idx_purchase_category_presets_type ON purchase_category_presets(type);
|
||||||
|
CREATE INDEX idx_purchase_category_presets_is_active ON purchase_category_presets(is_active);
|
||||||
|
|
||||||
|
CREATE INDEX idx_purchase_categories_organization_id ON purchase_categories(organization_id);
|
||||||
|
CREATE INDEX idx_purchase_categories_preset_id ON purchase_categories(preset_id);
|
||||||
|
CREATE INDEX idx_purchase_categories_parent_id ON purchase_categories(parent_id);
|
||||||
|
CREATE INDEX idx_purchase_categories_type ON purchase_categories(type);
|
||||||
|
CREATE INDEX idx_purchase_categories_is_active ON purchase_categories(is_active);
|
||||||
|
|
||||||
|
INSERT INTO purchase_category_presets (code, name, type, sort_order)
|
||||||
|
VALUES
|
||||||
|
('hpp', 'HPP', 'raw_material', 1),
|
||||||
|
('biaya_lain_lain', 'Biaya Lain-lain', 'non_inventory', 2)
|
||||||
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO purchase_category_presets (parent_id, code, name, type, sort_order)
|
||||||
|
SELECT parent.id, child.code, child.name, child.type, child.sort_order
|
||||||
|
FROM purchase_category_presets parent
|
||||||
|
JOIN (
|
||||||
|
VALUES
|
||||||
|
('hpp', 'hpp_bakso_mie_ayam', 'Bakso & Mie Ayam', 'raw_material', 1),
|
||||||
|
('hpp', 'hpp_nusantara', 'Nusantara', 'raw_material', 2),
|
||||||
|
('hpp', 'hpp_ramen', 'Ramen', 'raw_material', 3),
|
||||||
|
('hpp', 'hpp_minuman_kopi', 'Minuman/Kopi', 'raw_material', 4),
|
||||||
|
('biaya_lain_lain', 'biaya_atk_perlengkapan', 'ATK & Perlengkapan', 'non_inventory', 1),
|
||||||
|
('biaya_lain_lain', 'biaya_makan_karyawan', 'Makan Karyawan', 'non_inventory', 2),
|
||||||
|
('biaya_lain_lain', 'biaya_bensin_parkir', 'Bensin & Parkir', 'non_inventory', 3),
|
||||||
|
('biaya_lain_lain', 'biaya_kebersihan_keamanan', 'Kebersihan & Keamanan', 'non_inventory', 4),
|
||||||
|
('biaya_lain_lain', 'biaya_gaji_dw', 'Gaji DW', 'non_inventory', 5),
|
||||||
|
('biaya_lain_lain', 'biaya_gaji_staff', 'Gaji Staff', 'non_inventory', 6),
|
||||||
|
('biaya_lain_lain', 'biaya_internet_server', 'Internet & Server', 'non_inventory', 7),
|
||||||
|
('biaya_lain_lain', 'biaya_air_listrik', 'Air & Listrik', 'non_inventory', 8),
|
||||||
|
('biaya_lain_lain', 'biaya_promosi', 'Promosi', 'non_inventory', 9)
|
||||||
|
) AS child(parent_code, code, name, type, sort_order) ON parent.code = child.parent_code
|
||||||
|
ON CONFLICT (code) DO NOTHING;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
DROP TRIGGER IF EXISTS trigger_create_default_purchase_categories ON organizations;
|
||||||
|
DROP FUNCTION IF EXISTS create_default_purchase_categories();
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
CREATE OR REPLACE FUNCTION create_default_purchase_categories()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO purchase_categories (organization_id, preset_id, parent_id, code, name, type, sort_order, is_system, is_active, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
NEW.id,
|
||||||
|
preset.id,
|
||||||
|
NULL,
|
||||||
|
preset.code,
|
||||||
|
preset.name,
|
||||||
|
preset.type,
|
||||||
|
preset.sort_order,
|
||||||
|
true,
|
||||||
|
preset.is_active,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
FROM purchase_category_presets preset
|
||||||
|
WHERE preset.parent_id IS NULL
|
||||||
|
AND preset.is_active = true
|
||||||
|
ON CONFLICT (organization_id, code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO purchase_categories (organization_id, preset_id, parent_id, code, name, type, sort_order, is_system, is_active, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
NEW.id,
|
||||||
|
child_preset.id,
|
||||||
|
parent_category.id,
|
||||||
|
child_preset.code,
|
||||||
|
child_preset.name,
|
||||||
|
child_preset.type,
|
||||||
|
child_preset.sort_order,
|
||||||
|
true,
|
||||||
|
child_preset.is_active,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
FROM purchase_category_presets child_preset
|
||||||
|
JOIN purchase_category_presets parent_preset ON child_preset.parent_id = parent_preset.id
|
||||||
|
JOIN purchase_categories parent_category ON parent_category.organization_id = NEW.id
|
||||||
|
AND parent_category.code = parent_preset.code
|
||||||
|
WHERE child_preset.is_active = true
|
||||||
|
ON CONFLICT (organization_id, code) DO NOTHING;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_create_default_purchase_categories
|
||||||
|
AFTER INSERT ON organizations
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION create_default_purchase_categories();
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
DELETE FROM purchase_categories
|
||||||
|
WHERE is_system = true
|
||||||
|
AND preset_id IS NOT NULL;
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
INSERT INTO purchase_categories (organization_id, preset_id, parent_id, code, name, type, sort_order, is_system, is_active, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
org.id,
|
||||||
|
preset.id,
|
||||||
|
NULL,
|
||||||
|
preset.code,
|
||||||
|
preset.name,
|
||||||
|
preset.type,
|
||||||
|
preset.sort_order,
|
||||||
|
true,
|
||||||
|
preset.is_active,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
FROM organizations org
|
||||||
|
CROSS JOIN purchase_category_presets preset
|
||||||
|
WHERE preset.parent_id IS NULL
|
||||||
|
AND preset.is_active = true
|
||||||
|
ON CONFLICT (organization_id, code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO purchase_categories (organization_id, preset_id, parent_id, code, name, type, sort_order, is_system, is_active, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
org.id,
|
||||||
|
child_preset.id,
|
||||||
|
parent_category.id,
|
||||||
|
child_preset.code,
|
||||||
|
child_preset.name,
|
||||||
|
child_preset.type,
|
||||||
|
child_preset.sort_order,
|
||||||
|
true,
|
||||||
|
child_preset.is_active,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
FROM organizations org
|
||||||
|
JOIN purchase_category_presets child_preset ON child_preset.parent_id IS NOT NULL
|
||||||
|
JOIN purchase_category_presets parent_preset ON child_preset.parent_id = parent_preset.id
|
||||||
|
JOIN purchase_categories parent_category ON parent_category.organization_id = org.id
|
||||||
|
AND parent_category.code = parent_preset.code
|
||||||
|
WHERE child_preset.is_active = true
|
||||||
|
ON CONFLICT (organization_id, code) DO NOTHING;
|
||||||
Loading…
x
Reference in New Issue
Block a user