diff --git a/cmd/main.go b/cmd/main.go index b3590a5..99f42af 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -10,6 +10,7 @@ import ( "github.com/Caknoooo/go-gin-clean-starter/docs" "github.com/Caknoooo/go-gin-clean-starter/middlewares" "github.com/Caknoooo/go-gin-clean-starter/modules/auth" + "github.com/Caknoooo/go-gin-clean-starter/modules/category" "github.com/Caknoooo/go-gin-clean-starter/modules/client" maintenancegroup "github.com/Caknoooo/go-gin-clean-starter/modules/maintenance_group" "github.com/Caknoooo/go-gin-clean-starter/modules/menu" @@ -149,6 +150,7 @@ func main() { client.RegisterRoutes(server, injector) permissions.RegisterRoutes(server, injector) product.RegisterRoutes(server, injector) + category.RegisterRoutes(server, injector) // register swagger route server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) diff --git a/database/entities/m_category_entity.go b/database/entities/m_category_entity.go new file mode 100644 index 0000000..dbaef1c --- /dev/null +++ b/database/entities/m_category_entity.go @@ -0,0 +1,23 @@ +package entities + +import ( + "github.com/google/uuid" +) + +type MCategoryEntity struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:uuid_generate_v4()" json:"id"` + Name string `gorm:"type:varchar(100);not null;" json:"name"` + SearchKey string `gorm:"type:varchar(100);not null;" json:"search_key"` + Description string `gorm:"type:text" json:"description"` + IsActive bool `gorm:"type:boolean;default:true" json:"is_active"` + + ClientID uuid.UUID `gorm:"type:uuid;index;" json:"client_id"` + + Client M_Client `gorm:"foreignKey:ClientID;references:ID"` + + FullAuditTrail +} + +func (MCategoryEntity) TableName() string { + return "m_categories" +} diff --git a/database/migration.go b/database/migration.go index 1fe32ed..6a104be 100644 --- a/database/migration.go +++ b/database/migration.go @@ -21,6 +21,7 @@ func Migrate(db *gorm.DB) error { &entities.M_MaintenanceGroupRole{}, &entities.M_MaintenanceGroupRoleUser{}, &entities.MProductEntity{}, + &entities.MCategoryEntity{}, ); err != nil { return err } @@ -44,6 +45,7 @@ func MigrateFresh(db *gorm.DB) error { &entities.M_MaintenanceGroup{}, &entities.M_MaintenanceGroupRole{}, &entities.M_MaintenanceGroupRoleUser{}, + &entities.MCategoryEntity{}, &entities.MProductEntity{}, ); err != nil { return err diff --git a/modules/category/controller/category_controller.go b/modules/category/controller/category_controller.go new file mode 100644 index 0000000..2007cf8 --- /dev/null +++ b/modules/category/controller/category_controller.go @@ -0,0 +1,115 @@ +package controller + +import ( + "net/http" + + "github.com/Caknoooo/go-gin-clean-starter/modules/category/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/category/query" + "github.com/Caknoooo/go-gin-clean-starter/modules/category/service" + "github.com/Caknoooo/go-gin-clean-starter/pkg/constants" + "github.com/Caknoooo/go-gin-clean-starter/pkg/utils" + "github.com/gin-gonic/gin" + "github.com/samber/do" + "gorm.io/gorm" +) + +type CategoryController interface { + Create(ctx *gin.Context) + Update(ctx *gin.Context) + Delete(ctx *gin.Context) + GetById(ctx *gin.Context) + GetAll(ctx *gin.Context) +} + +type categoryController struct { + categoryService service.CategoryService + db *gorm.DB +} + +func NewCategoryController(i *do.Injector, categoryService service.CategoryService) CategoryController { + db := do.MustInvokeNamed[*gorm.DB](i, constants.DB) + return &categoryController{ + categoryService: categoryService, + db: db, + } +} + +func (c *categoryController) Create(ctx *gin.Context) { + var req dto.CategoryCreateRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + created, err := c.categoryService.Create(ctx, req) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_CATEGORY, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_CREATE_CATEGORY, created) + ctx.JSON(http.StatusOK, res) +} + +func (c *categoryController) Update(ctx *gin.Context) { + id := ctx.Param("id") + var req dto.CategoryUpdateRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + updated, err := c.categoryService.Update(ctx, req, id) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_UPDATE_CATEGORY, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_UPDATE_CATEGORY, updated) + ctx.JSON(http.StatusOK, res) +} + +func (c *categoryController) Delete(ctx *gin.Context) { + id := ctx.Param("id") + if err := c.categoryService.Delete(ctx, id); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_DELETE_CATEGORY, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_DELETE_CATEGORY, nil) + ctx.JSON(http.StatusOK, res) +} + +func (c *categoryController) GetById(ctx *gin.Context) { + id := ctx.Param("id") + category, err := c.categoryService.GetById(ctx, id) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_CATEGORY, err.Error(), nil) + ctx.JSON(http.StatusNotFound, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_CATEGORY, category) + ctx.JSON(http.StatusOK, res) +} + +func (c *categoryController) GetAll(ctx *gin.Context) { + var filter query.CategoryFilter + if err := ctx.ShouldBindQuery(&filter); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_CATEGORY, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + perPage := utils.ParseInt(ctx.DefaultQuery("per_page", "10")) + page := utils.ParseInt(ctx.DefaultQuery("page", "1")) + filter.PerPage = perPage + filter.Page = (page - 1) * perPage + categories, total, err := c.categoryService.GetAll(ctx, filter) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_CATEGORY, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + paginationResponse := utils.BuildPaginationResponse(perPage, page, total) + response := utils.BuildResponseSuccessWithPagination(http.StatusOK, dto.MESSAGE_SUCCESS_GET_CATEGORY, categories, paginationResponse) + ctx.JSON(http.StatusOK, response) +} diff --git a/modules/category/dto/category_dto.go b/modules/category/dto/category_dto.go new file mode 100644 index 0000000..bb4bffa --- /dev/null +++ b/modules/category/dto/category_dto.go @@ -0,0 +1,52 @@ +package dto + +import ( + "errors" + + // staticDto "github.com/Caknoooo/go-gin-clean-starter/modules/static/dto" + "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" +) + +const ( + MESSAGE_FAILED_CREATE_CATEGORY = "failed create category" + MESSAGE_SUCCESS_CREATE_CATEGORY = "success create category" + MESSAGE_FAILED_GET_CATEGORY = "failed get category" + MESSAGE_SUCCESS_GET_CATEGORY = "success get category" + MESSAGE_FAILED_UPDATE_CATEGORY = "failed update category" + MESSAGE_SUCCESS_UPDATE_CATEGORY = "success update category" + MESSAGE_FAILED_DELETE_CATEGORY = "failed delete category" + MESSAGE_SUCCESS_DELETE_CATEGORY = "success delete category" + MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body" +) + +var ( + ErrCreateCategory = errors.New("failed to create category") + ErrGetCategoryById = errors.New("failed to get category by id") + ErrUpdateCategory = errors.New("failed to update category") + ErrDeleteCategory = errors.New("failed to delete category") +) + +type CategoryCreateRequest struct { + Name string `json:"name" binding:"required"` + SearchKey string `json:"search_key" binding:"required"` + Description string `json:"description"` + IsActive bool `json:"is_active"` + ClientID string `json:"client_id" binding:"required"` +} + +type CategoryUpdateRequest struct { + Name string `json:"name"` + SearchKey string `json:"search_key"` + Description string `json:"description"` + IsActive bool `json:"is_active"` + ClientID string `json:"client_id"` +} + +type CategoryResponse struct { + ID string `json:"id"` + Name string `json:"name"` + SearchKey string `json:"search_key"` + Description string `json:"description"` + IsActive bool `json:"is_active"` + Client dto.ClientResponse `json:"client"` +} diff --git a/modules/category/query/category_query.go b/modules/category/query/category_query.go new file mode 100644 index 0000000..519be68 --- /dev/null +++ b/modules/category/query/category_query.go @@ -0,0 +1,30 @@ +package query + +import ( + "gorm.io/gorm" +) + +type CategoryFilter struct { + Name string `form:"name"` + SearchKey string `form:"search_key"` + ClientID string `form:"client_id"` + IsActive *bool `form:"is_active"` + PerPage int `form:"per_page"` + Page int `form:"page"` +} + +func ApplyCategoryFilters(db *gorm.DB, filter CategoryFilter) *gorm.DB { + if filter.Name != "" { + db = db.Where("name ILIKE ?", "%"+filter.Name+"%") + } + if filter.SearchKey != "" { + db = db.Where("search_key ILIKE ?", "%"+filter.SearchKey+"%") + } + if filter.ClientID != "" { + db = db.Where("client_id = ?", filter.ClientID) + } + if filter.IsActive != nil { + db = db.Where("is_active = ?", *filter.IsActive) + } + return db +} diff --git a/modules/category/repository/category_repository.go b/modules/category/repository/category_repository.go new file mode 100644 index 0000000..78255c7 --- /dev/null +++ b/modules/category/repository/category_repository.go @@ -0,0 +1,80 @@ +package repository + +import ( + "context" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + "github.com/Caknoooo/go-gin-clean-starter/modules/category/query" + "gorm.io/gorm" +) + +type CategoryRepository interface { + Create(ctx context.Context, tx *gorm.DB, category entities.MCategoryEntity) (entities.MCategoryEntity, error) + GetById(ctx context.Context, tx *gorm.DB, categoryId string) (entities.MCategoryEntity, error) + GetAll(ctx context.Context, filter query.CategoryFilter) ([]entities.MCategoryEntity, int64, error) + Update(ctx context.Context, tx *gorm.DB, category entities.MCategoryEntity) (entities.MCategoryEntity, error) + Delete(ctx context.Context, tx *gorm.DB, categoryId string) error +} + +type categoryRepository struct { + db *gorm.DB +} + +func NewCategoryRepository(db *gorm.DB) CategoryRepository { + return &categoryRepository{db: db} +} + +func (r *categoryRepository) Create(ctx context.Context, tx *gorm.DB, category entities.MCategoryEntity) (entities.MCategoryEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Create(&category).Error; err != nil { + return category, err + } + return category, nil +} + +func (r *categoryRepository) GetById(ctx context.Context, tx *gorm.DB, categoryId string) (entities.MCategoryEntity, error) { + if tx == nil { + tx = r.db + } + var category entities.MCategoryEntity + if err := tx.WithContext(ctx). + Preload("Client"). + First(&category, "id = ?", categoryId).Error; err != nil { + return category, err + } + return category, nil +} + +func (r *categoryRepository) GetAll(ctx context.Context, filter query.CategoryFilter) ([]entities.MCategoryEntity, int64, error) { + var categories []entities.MCategoryEntity + var total int64 + db := r.db.Preload("Client").Model(&entities.MCategoryEntity{}) + db = query.ApplyCategoryFilters(db, filter) + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + err := db.Limit(filter.PerPage).Offset(filter.Page).Find(&categories).Error + return categories, total, err +} + +func (r *categoryRepository) Update(ctx context.Context, tx *gorm.DB, category entities.MCategoryEntity) (entities.MCategoryEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Save(&category).Error; err != nil { + return category, err + } + return category, nil +} + +func (r *categoryRepository) Delete(ctx context.Context, tx *gorm.DB, categoryId string) error { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Delete(&entities.MCategoryEntity{}, "id = ?", categoryId).Error; err != nil { + return err + } + return nil +} diff --git a/modules/category/routes.go b/modules/category/routes.go new file mode 100644 index 0000000..c3c5e38 --- /dev/null +++ b/modules/category/routes.go @@ -0,0 +1,24 @@ +package category + +import ( + "github.com/Caknoooo/go-gin-clean-starter/middlewares" + "github.com/Caknoooo/go-gin-clean-starter/modules/auth/service" + "github.com/Caknoooo/go-gin-clean-starter/modules/category/controller" + "github.com/Caknoooo/go-gin-clean-starter/pkg/constants" + "github.com/gin-gonic/gin" + "github.com/samber/do" +) + +func RegisterRoutes(server *gin.Engine, injector *do.Injector) { + categoryController := do.MustInvoke[controller.CategoryController](injector) + jwtService := do.MustInvokeNamed[service.JWTService](injector, constants.JWTService) + + categoryRoutes := server.Group("/api/v1/categories") + { + categoryRoutes.POST("", middlewares.Authenticate(jwtService), categoryController.Create) + categoryRoutes.GET("/:id", middlewares.Authenticate(jwtService), categoryController.GetById) + categoryRoutes.PUT("/:id", middlewares.Authenticate(jwtService), categoryController.Update) + categoryRoutes.DELETE("/:id", middlewares.Authenticate(jwtService), categoryController.Delete) + categoryRoutes.GET("", middlewares.Authenticate(jwtService), categoryController.GetAll) + } +} diff --git a/modules/category/service/category_service.go b/modules/category/service/category_service.go new file mode 100644 index 0000000..7469392 --- /dev/null +++ b/modules/category/service/category_service.go @@ -0,0 +1,148 @@ +package service + +import ( + "context" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + "github.com/Caknoooo/go-gin-clean-starter/modules/category/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/category/query" + "github.com/Caknoooo/go-gin-clean-starter/modules/category/repository" + pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type CategoryService interface { + Create(ctx context.Context, req dto.CategoryCreateRequest) (dto.CategoryResponse, error) + GetById(ctx context.Context, categoryId string) (dto.CategoryResponse, error) + GetAll(ctx context.Context, filter query.CategoryFilter) ([]dto.CategoryResponse, int64, error) + Update(ctx context.Context, req dto.CategoryUpdateRequest, categoryId string) (dto.CategoryResponse, error) + Delete(ctx context.Context, categoryId string) error +} + +type categoryService struct { + db *gorm.DB + categoryRepo repository.CategoryRepository +} + +func NewCategoryService(categoryRepo repository.CategoryRepository, db *gorm.DB) CategoryService { + return &categoryService{ + categoryRepo: categoryRepo, + db: db, + } +} + +func (s *categoryService) Create(ctx context.Context, req dto.CategoryCreateRequest) (dto.CategoryResponse, error) { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + category := entities.MCategoryEntity{ + Name: req.Name, + SearchKey: req.SearchKey, + Description: req.Description, + IsActive: req.IsActive, + } + clientUUID, err := uuid.Parse(req.ClientID) + if err != nil { + tx.Rollback() + return dto.CategoryResponse{}, err + } + category.ClientID = clientUUID + created, err := s.categoryRepo.Create(ctx, tx, category) + if err != nil { + tx.Rollback() + return dto.CategoryResponse{}, err + } + tx.Commit() + + result, err := s.categoryRepo.GetById(ctx, s.db, created.ID.String()) + if err != nil { + return dto.CategoryResponse{}, err + } + return toCategoryResponse(result), nil +} + +func (s *categoryService) GetById(ctx context.Context, categoryId string) (dto.CategoryResponse, error) { + category, err := s.categoryRepo.GetById(ctx, nil, categoryId) + if err != nil { + return dto.CategoryResponse{}, err + } + return toCategoryResponse(category), nil +} + +func (s *categoryService) GetAll(ctx context.Context, filter query.CategoryFilter) ([]dto.CategoryResponse, int64, error) { + categories, total, err := s.categoryRepo.GetAll(ctx, filter) + if err != nil { + return nil, 0, err + } + var responses []dto.CategoryResponse + for _, c := range categories { + responses = append(responses, toCategoryResponse(c)) + } + if responses == nil { + responses = make([]dto.CategoryResponse, 0) + } + return responses, total, nil +} + +func (s *categoryService) Update(ctx context.Context, req dto.CategoryUpdateRequest, categoryId string) (dto.CategoryResponse, error) { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + category, err := s.categoryRepo.GetById(ctx, tx, categoryId) + if err != nil { + tx.Rollback() + return dto.CategoryResponse{}, err + } + category.Name = req.Name + category.SearchKey = req.SearchKey + category.Description = req.Description + category.IsActive = req.IsActive + updated, err := s.categoryRepo.Update(ctx, tx, category) + if err != nil { + tx.Rollback() + return dto.CategoryResponse{}, err + } + tx.Commit() + + result, err := s.categoryRepo.GetById(ctx, s.db, updated.ID.String()) + if err != nil { + return dto.CategoryResponse{}, err + } + return toCategoryResponse(result), nil +} + +func (s *categoryService) Delete(ctx context.Context, categoryId string) error { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + if err := s.categoryRepo.Delete(ctx, tx, categoryId); err != nil { + tx.Rollback() + return err + } + tx.Commit() + return nil +} + +func toCategoryResponse(e entities.MCategoryEntity) dto.CategoryResponse { + return dto.CategoryResponse{ + ID: e.ID.String(), + Name: e.Name, + SearchKey: e.SearchKey, + Description: e.Description, + IsActive: e.IsActive, + Client: pkgdto.ClientResponse{ + ID: e.Client.ID.String(), + Name: e.Client.Name, + }, + } +} diff --git a/providers/core.go b/providers/core.go index f593f5f..4650489 100644 --- a/providers/core.go +++ b/providers/core.go @@ -4,18 +4,22 @@ import ( "github.com/Caknoooo/go-gin-clean-starter/config" authRepo "github.com/Caknoooo/go-gin-clean-starter/modules/auth/repository" "github.com/Caknoooo/go-gin-clean-starter/modules/auth/service" + clientController "github.com/Caknoooo/go-gin-clean-starter/modules/client/controller" clientRepo "github.com/Caknoooo/go-gin-clean-starter/modules/client/repository" clientService "github.com/Caknoooo/go-gin-clean-starter/modules/client/service" "github.com/sirupsen/logrus" logsController "github.com/Caknoooo/go-gin-clean-starter/modules/logs/controller" + menuController "github.com/Caknoooo/go-gin-clean-starter/modules/menu/controller" menuRepo "github.com/Caknoooo/go-gin-clean-starter/modules/menu/repository" menuService "github.com/Caknoooo/go-gin-clean-starter/modules/menu/service" + productController "github.com/Caknoooo/go-gin-clean-starter/modules/product/controller" productRepo "github.com/Caknoooo/go-gin-clean-starter/modules/product/repository" productService "github.com/Caknoooo/go-gin-clean-starter/modules/product/service" + roleController "github.com/Caknoooo/go-gin-clean-starter/modules/role/controller" roleRepo "github.com/Caknoooo/go-gin-clean-starter/modules/role/repository" roleService "github.com/Caknoooo/go-gin-clean-starter/modules/role/service" @@ -29,6 +33,11 @@ import ( maintGroupRepoRole "github.com/Caknoooo/go-gin-clean-starter/modules/maintenance_group/repository" maintGroupRepoRoleUser "github.com/Caknoooo/go-gin-clean-starter/modules/maintenance_group/repository" maintGroupService "github.com/Caknoooo/go-gin-clean-starter/modules/maintenance_group/service" + + categoryController "github.com/Caknoooo/go-gin-clean-starter/modules/category/controller" + categoryRepo "github.com/Caknoooo/go-gin-clean-starter/modules/category/repository" + categoryService "github.com/Caknoooo/go-gin-clean-starter/modules/category/service" + "github.com/Caknoooo/go-gin-clean-starter/modules/user/controller" "github.com/Caknoooo/go-gin-clean-starter/modules/user/repository" userService "github.com/Caknoooo/go-gin-clean-starter/modules/user/service" @@ -64,7 +73,7 @@ func RegisterDependencies(injector *do.Injector) { // Repository userRepository := repository.NewUserRepository(db) refreshTokenRepository := authRepo.NewRefreshTokenRepository(db) - // productRepository := productRepo.NewProductRepository(db) + productRepository := productRepo.NewProductRepository(db) menuRepository := menuRepo.NewMenuRepository(db) roleRepository := roleRepo.NewRoleRepository(db) clientRepository := clientRepo.NewClientRepository(db) @@ -72,7 +81,7 @@ func RegisterDependencies(injector *do.Injector) { maintenanceGroupRoleRepository := maintGroupRepoRole.NewMaintGroupRoleRepository(db) maintenanceGroupRoleUserRepository := maintGroupRepoRoleUser.NewMaintGroupRoleUserRepository(db) permissionsRepository := permissionsRepo.NewPermissionsRepository(db) - productRepository := productRepo.NewProductRepository(db) + categoryRepository := categoryRepo.NewCategoryRepository(db) // Service userServ := userService.NewUserService(userRepository, refreshTokenRepository, jwtService, db) @@ -82,6 +91,7 @@ func RegisterDependencies(injector *do.Injector) { maintenanceGroupServ := maintGroupService.NewMaintenanceGroupService(maintenanceGroupRepository, maintenanceGroupRoleRepository, maintenanceGroupRoleUserRepository, db) clientServ := clientService.NewClientService(clientRepository, db) permissionsServ := permissionsService.NewPermissionsService(permissionsRepository, db) + categoryServ := categoryService.NewCategoryService(categoryRepository, db) // Controller do.Provide( @@ -99,11 +109,6 @@ func RegisterDependencies(injector *do.Injector) { return userServ, nil }, ) - // do.Provide( - // injector, func(i *do.Injector) (productController.ProductController, error) { - // return productController.NewProductController(i, productService), nil - // }, - // ) do.Provide( injector, func(i *do.Injector) (roleController.RoleController, error) { return roleController.NewRoleController(i, roleService), nil @@ -134,4 +139,9 @@ func RegisterDependencies(injector *do.Injector) { return productController.NewProductController(i, productService), nil }, ) + do.Provide( + injector, func(i *do.Injector) (categoryController.CategoryController, error) { + return categoryController.NewCategoryController(i, categoryServ), nil + }, + ) }