From 02dc2c71d8cf5e4ad7a518969ffc6f9cfa95a61f Mon Sep 17 00:00:00 2001 From: Habib Fatkhul Rohman Date: Thu, 30 Oct 2025 14:20:20 +0700 Subject: [PATCH] feat(uom): Add UOM management with CRUD operations and routing --- cmd/main.go | 2 + database/entities/m_uom_entitiy.go | 25 ++++ database/migration.go | 2 + modules/uom/controller/uom_controller.go | 115 ++++++++++++++++ modules/uom/dto/uom_dto.go | 57 ++++++++ modules/uom/query/uom_query.go | 30 ++++ modules/uom/repository/uom_repository.go | 78 +++++++++++ modules/uom/routes.go | 24 ++++ modules/uom/service/uom_service.go | 166 +++++++++++++++++++++++ providers/core.go | 11 ++ 10 files changed, 510 insertions(+) create mode 100644 database/entities/m_uom_entitiy.go create mode 100644 modules/uom/controller/uom_controller.go create mode 100644 modules/uom/dto/uom_dto.go create mode 100644 modules/uom/query/uom_query.go create mode 100644 modules/uom/repository/uom_repository.go create mode 100644 modules/uom/routes.go create mode 100644 modules/uom/service/uom_service.go diff --git a/cmd/main.go b/cmd/main.go index 99f42af..0b85376 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -18,6 +18,7 @@ import ( "github.com/Caknoooo/go-gin-clean-starter/modules/permissions" "github.com/Caknoooo/go-gin-clean-starter/modules/product" "github.com/Caknoooo/go-gin-clean-starter/modules/role" + "github.com/Caknoooo/go-gin-clean-starter/modules/uom" "github.com/sirupsen/logrus" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" @@ -151,6 +152,7 @@ func main() { permissions.RegisterRoutes(server, injector) product.RegisterRoutes(server, injector) category.RegisterRoutes(server, injector) + uom.RegisterRoutes(server, injector) // register swagger route server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) diff --git a/database/entities/m_uom_entitiy.go b/database/entities/m_uom_entitiy.go new file mode 100644 index 0000000..2c9b669 --- /dev/null +++ b/database/entities/m_uom_entitiy.go @@ -0,0 +1,25 @@ +package entities + +import ( + "github.com/google/uuid" +) + +type MUomEntity 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"` + Description string `gorm:"type:text" json:"description"` + Symbol string `gorm:"type:varchar(20);" json:"symbol"` + Code string `gorm:"type:varchar(50);" json:"code"` + StdPrecision int `gorm:"type:int;" json:"std_precision"` + 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" json:"client"` + + FullAuditTrail +} + +func (MUomEntity) TableName() string { + return "m_uoms" +} diff --git a/database/migration.go b/database/migration.go index 6a104be..f6e1e8e 100644 --- a/database/migration.go +++ b/database/migration.go @@ -22,6 +22,7 @@ func Migrate(db *gorm.DB) error { &entities.M_MaintenanceGroupRoleUser{}, &entities.MProductEntity{}, &entities.MCategoryEntity{}, + &entities.MUomEntity{}, ); err != nil { return err } @@ -47,6 +48,7 @@ func MigrateFresh(db *gorm.DB) error { &entities.M_MaintenanceGroupRoleUser{}, &entities.MCategoryEntity{}, &entities.MProductEntity{}, + &entities.MUomEntity{}, ); err != nil { return err } diff --git a/modules/uom/controller/uom_controller.go b/modules/uom/controller/uom_controller.go new file mode 100644 index 0000000..968da35 --- /dev/null +++ b/modules/uom/controller/uom_controller.go @@ -0,0 +1,115 @@ +package controller + +import ( + "net/http" + + dtodomain "github.com/Caknoooo/go-gin-clean-starter/modules/uom/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/uom/query" + "github.com/Caknoooo/go-gin-clean-starter/modules/uom/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 UomController interface { + Create(ctx *gin.Context) + Update(ctx *gin.Context) + Delete(ctx *gin.Context) + GetById(ctx *gin.Context) + GetAll(ctx *gin.Context) +} + +type uomController struct { + uomService service.UomService + db *gorm.DB +} + +func NewUomController(i *do.Injector, uomService service.UomService) UomController { + db := do.MustInvokeNamed[*gorm.DB](i, constants.DB) + return &uomController{ + uomService: uomService, + db: db, + } +} + +func (c *uomController) Create(ctx *gin.Context) { + var req dtodomain.UomCreateRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + res := utils.BuildResponseFailed(dtodomain.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + created, err := c.uomService.Create(ctx, req) + if err != nil { + res := utils.BuildResponseFailed(dtodomain.MESSAGE_FAILED_CREATE_UOM, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dtodomain.MESSAGE_SUCCESS_CREATE_UOM, created) + ctx.JSON(http.StatusOK, res) +} + +func (c *uomController) Update(ctx *gin.Context) { + id := ctx.Param("id") + var req dtodomain.UomUpdateRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + res := utils.BuildResponseFailed(dtodomain.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + updated, err := c.uomService.Update(ctx, req, id) + if err != nil { + res := utils.BuildResponseFailed(dtodomain.MESSAGE_FAILED_UPDATE_UOM, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dtodomain.MESSAGE_SUCCESS_UPDATE_UOM, updated) + ctx.JSON(http.StatusOK, res) +} + +func (c *uomController) Delete(ctx *gin.Context) { + id := ctx.Param("id") + if err := c.uomService.Delete(ctx, id); err != nil { + res := utils.BuildResponseFailed(dtodomain.MESSAGE_FAILED_DELETE_UOM, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dtodomain.MESSAGE_SUCCESS_DELETE_UOM, nil) + ctx.JSON(http.StatusOK, res) +} + +func (c *uomController) GetById(ctx *gin.Context) { + id := ctx.Param("id") + uom, err := c.uomService.GetById(ctx, id) + if err != nil { + res := utils.BuildResponseFailed(dtodomain.MESSAGE_FAILED_GET_UOM, err.Error(), nil) + ctx.JSON(http.StatusNotFound, res) + return + } + res := utils.BuildResponseSuccess(dtodomain.MESSAGE_SUCCESS_GET_UOM, uom) + ctx.JSON(http.StatusOK, res) +} + +func (c *uomController) GetAll(ctx *gin.Context) { + var filter query.UomFilter + if err := ctx.ShouldBindQuery(&filter); err != nil { + res := utils.BuildResponseFailed(dtodomain.MESSAGE_FAILED_GET_UOM, 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 + uoms, total, err := c.uomService.GetAll(ctx, filter) + if err != nil { + res := utils.BuildResponseFailed(dtodomain.MESSAGE_FAILED_GET_UOM, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + paginationResponse := utils.BuildPaginationResponse(perPage, page, total) + response := utils.BuildResponseSuccessWithPagination(http.StatusOK, dtodomain.MESSAGE_SUCCESS_GET_UOM, uoms, paginationResponse) + ctx.JSON(http.StatusOK, response) +} diff --git a/modules/uom/dto/uom_dto.go b/modules/uom/dto/uom_dto.go new file mode 100644 index 0000000..0315727 --- /dev/null +++ b/modules/uom/dto/uom_dto.go @@ -0,0 +1,57 @@ +package dto + +import ( + "errors" + + pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" +) + +const ( + MESSAGE_FAILED_CREATE_UOM = "failed create uom" + MESSAGE_SUCCESS_CREATE_UOM = "success create uom" + MESSAGE_FAILED_GET_UOM = "failed get uom" + MESSAGE_SUCCESS_GET_UOM = "success get uom" + MESSAGE_FAILED_UPDATE_UOM = "failed update uom" + MESSAGE_SUCCESS_UPDATE_UOM = "success update uom" + MESSAGE_FAILED_DELETE_UOM = "failed delete uom" + MESSAGE_SUCCESS_DELETE_UOM = "success delete uom" + MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body" +) + +var ( + ErrCreateUom = errors.New("failed to create uom") + ErrGetUomById = errors.New("failed to get uom by id") + ErrUpdateUom = errors.New("failed to update uom") + ErrDeleteUom = errors.New("failed to delete uom") +) + +type UomCreateRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Symbol string `json:"symbol"` + Code string `json:"code"` + StdPrecision int `json:"std_precision"` + IsActive bool `json:"is_active"` + ClientID string `json:"client_id" binding:"required"` +} + +type UomUpdateRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Symbol string `json:"symbol"` + Code string `json:"code"` + StdPrecision int `json:"std_precision"` + IsActive bool `json:"is_active"` + ClientID string `json:"client_id"` +} + +type UomResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Symbol string `json:"symbol"` + Code string `json:"code"` + StdPrecision int `json:"std_precision"` + IsActive bool `json:"is_active"` + Client pkgdto.ClientResponse `json:"client"` +} diff --git a/modules/uom/query/uom_query.go b/modules/uom/query/uom_query.go new file mode 100644 index 0000000..47b3f33 --- /dev/null +++ b/modules/uom/query/uom_query.go @@ -0,0 +1,30 @@ +package query + +import ( + "gorm.io/gorm" +) + +type UomFilter struct { + Name string `form:"name"` + Code string `form:"code"` + IsActive *bool `form:"is_active"` + ClientID string `form:"client_id"` + PerPage int `form:"per_page"` + Page int `form:"page"` +} + +func ApplyUomFilters(db *gorm.DB, filter UomFilter) *gorm.DB { + if filter.Name != "" { + db = db.Where("name ILIKE ?", "%"+filter.Name+"%") + } + if filter.Code != "" { + db = db.Where("code ILIKE ?", "%"+filter.Code+"%") + } + 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/uom/repository/uom_repository.go b/modules/uom/repository/uom_repository.go new file mode 100644 index 0000000..1ff2027 --- /dev/null +++ b/modules/uom/repository/uom_repository.go @@ -0,0 +1,78 @@ +package repository + +import ( + "context" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + "github.com/Caknoooo/go-gin-clean-starter/modules/uom/query" + "gorm.io/gorm" +) + +type UomRepository interface { + Create(ctx context.Context, tx *gorm.DB, uom entities.MUomEntity) (entities.MUomEntity, error) + GetById(ctx context.Context, tx *gorm.DB, uomId string) (entities.MUomEntity, error) + GetAll(ctx context.Context, filter query.UomFilter) ([]entities.MUomEntity, int64, error) + Update(ctx context.Context, tx *gorm.DB, uom entities.MUomEntity) (entities.MUomEntity, error) + Delete(ctx context.Context, tx *gorm.DB, uomId string) error +} + +type uomRepository struct { + db *gorm.DB +} + +func NewUomRepository(db *gorm.DB) UomRepository { + return &uomRepository{db: db} +} + +func (r *uomRepository) Create(ctx context.Context, tx *gorm.DB, uom entities.MUomEntity) (entities.MUomEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Create(&uom).Error; err != nil { + return uom, err + } + return uom, nil +} + +func (r *uomRepository) GetById(ctx context.Context, tx *gorm.DB, uomId string) (entities.MUomEntity, error) { + if tx == nil { + tx = r.db + } + var uom entities.MUomEntity + if err := tx.WithContext(ctx).Preload("Client").First(&uom, "id = ?", uomId).Error; err != nil { + return uom, err + } + return uom, nil +} + +func (r *uomRepository) GetAll(ctx context.Context, filter query.UomFilter) ([]entities.MUomEntity, int64, error) { + var uoms []entities.MUomEntity + var total int64 + db := r.db.Model(&entities.MUomEntity{}) + db = query.ApplyUomFilters(db, filter) + if err := db.Count(&total).Error; err != nil { + return nil, 0, err + } + err := db.Limit(filter.PerPage).Offset(filter.Page).Preload("Client").Find(&uoms).Error + return uoms, total, err +} + +func (r *uomRepository) Update(ctx context.Context, tx *gorm.DB, uom entities.MUomEntity) (entities.MUomEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Save(&uom).Error; err != nil { + return uom, err + } + return uom, nil +} + +func (r *uomRepository) Delete(ctx context.Context, tx *gorm.DB, uomId string) error { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Delete(&entities.MUomEntity{}, "id = ?", uomId).Error; err != nil { + return err + } + return nil +} diff --git a/modules/uom/routes.go b/modules/uom/routes.go new file mode 100644 index 0000000..8791e3d --- /dev/null +++ b/modules/uom/routes.go @@ -0,0 +1,24 @@ +package uom + +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/uom/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) { + uomController := do.MustInvoke[controller.UomController](injector) + jwtService := do.MustInvokeNamed[service.JWTService](injector, constants.JWTService) + + uomRoutes := server.Group("/api/v1/uoms") + { + uomRoutes.POST("", middlewares.Authenticate(jwtService), uomController.Create) + uomRoutes.GET("/:id", middlewares.Authenticate(jwtService), uomController.GetById) + uomRoutes.PUT("/:id", middlewares.Authenticate(jwtService), uomController.Update) + uomRoutes.DELETE("/:id", middlewares.Authenticate(jwtService), uomController.Delete) + uomRoutes.GET("", middlewares.Authenticate(jwtService), uomController.GetAll) + } +} diff --git a/modules/uom/service/uom_service.go b/modules/uom/service/uom_service.go new file mode 100644 index 0000000..712b600 --- /dev/null +++ b/modules/uom/service/uom_service.go @@ -0,0 +1,166 @@ +package service + +import ( + "context" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + dtodomain "github.com/Caknoooo/go-gin-clean-starter/modules/uom/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/uom/query" + "github.com/Caknoooo/go-gin-clean-starter/modules/uom/repository" + pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type UomService interface { + Create(ctx context.Context, req dtodomain.UomCreateRequest) (dtodomain.UomResponse, error) + GetById(ctx context.Context, uomId string) (dtodomain.UomResponse, error) + GetAll(ctx context.Context, filter query.UomFilter) ([]dtodomain.UomResponse, int64, error) + Update(ctx context.Context, req dtodomain.UomUpdateRequest, uomId string) (dtodomain.UomResponse, error) + Delete(ctx context.Context, uomId string) error +} + +type uomService struct { + db *gorm.DB + uomRepo repository.UomRepository +} + +func NewUomService(uomRepo repository.UomRepository, db *gorm.DB) UomService { + return &uomService{ + uomRepo: uomRepo, + db: db, + } +} + +func (s *uomService) Create(ctx context.Context, req dtodomain.UomCreateRequest) (dtodomain.UomResponse, error) { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + uom := entities.MUomEntity{ + Name: req.Name, + Description: req.Description, + Symbol: req.Symbol, + Code: req.Code, + StdPrecision: req.StdPrecision, + IsActive: req.IsActive, + } + clientUUID, err := uuid.Parse(req.ClientID) + if err != nil { + tx.Rollback() + return dtodomain.UomResponse{}, err + } + uom.ClientID = clientUUID + created, err := s.uomRepo.Create(ctx, tx, uom) + if err != nil { + tx.Rollback() + return dtodomain.UomResponse{}, err + } + tx.Commit() + + result, err := s.uomRepo.GetById(ctx, nil, created.ID.String()) + if err != nil { + return dtodomain.UomResponse{}, err + } + return toUomResponse(result), nil +} + +func (s *uomService) GetById(ctx context.Context, uomId string) (dtodomain.UomResponse, error) { + uom, err := s.uomRepo.GetById(ctx, nil, uomId) + if err != nil { + return dtodomain.UomResponse{}, err + } + return toUomResponse(uom), nil +} + +func (s *uomService) GetAll(ctx context.Context, filter query.UomFilter) ([]dtodomain.UomResponse, int64, error) { + uoms, total, err := s.uomRepo.GetAll(ctx, filter) + if err != nil { + return nil, 0, err + } + var responses []dtodomain.UomResponse + for _, u := range uoms { + responses = append(responses, toUomResponse(u)) + } + if responses == nil { + responses = make([]dtodomain.UomResponse, 0) + } + return responses, total, nil +} + +func (s *uomService) Update(ctx context.Context, req dtodomain.UomUpdateRequest, uomId string) (dtodomain.UomResponse, error) { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + uom, err := s.uomRepo.GetById(ctx, tx, uomId) + if err != nil { + tx.Rollback() + return dtodomain.UomResponse{}, err + } + uom.Name = req.Name + uom.Description = req.Description + uom.Symbol = req.Symbol + uom.Code = req.Code + uom.StdPrecision = req.StdPrecision + uom.IsActive = req.IsActive + if req.ClientID != "" { + clientUUID, err := uuid.Parse(req.ClientID) + if err != nil { + tx.Rollback() + return dtodomain.UomResponse{}, err + } + uom.ClientID = clientUUID + uom.Client = entities.M_Client{} + } else { + uom.Client = entities.M_Client{} + } + updated, err := s.uomRepo.Update(ctx, tx, uom) + if err != nil { + tx.Rollback() + return dtodomain.UomResponse{}, err + } + tx.Commit() + + result, err := s.uomRepo.GetById(ctx, nil, updated.ID.String()) + if err != nil { + return dtodomain.UomResponse{}, err + } + + return toUomResponse(result), nil +} + +func (s *uomService) Delete(ctx context.Context, uomId string) error { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + if err := s.uomRepo.Delete(ctx, tx, uomId); err != nil { + tx.Rollback() + return err + } + tx.Commit() + return nil +} + +func toUomResponse(e entities.MUomEntity) dtodomain.UomResponse { + return dtodomain.UomResponse{ + ID: e.ID.String(), + Name: e.Name, + Description: e.Description, + Symbol: e.Symbol, + Code: e.Code, + StdPrecision: e.StdPrecision, + 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 4650489..360ff58 100644 --- a/providers/core.go +++ b/providers/core.go @@ -38,6 +38,10 @@ import ( categoryRepo "github.com/Caknoooo/go-gin-clean-starter/modules/category/repository" categoryService "github.com/Caknoooo/go-gin-clean-starter/modules/category/service" + uomController "github.com/Caknoooo/go-gin-clean-starter/modules/uom/controller" + uomRepo "github.com/Caknoooo/go-gin-clean-starter/modules/uom/repository" + uomService "github.com/Caknoooo/go-gin-clean-starter/modules/uom/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" @@ -82,6 +86,7 @@ func RegisterDependencies(injector *do.Injector) { maintenanceGroupRoleUserRepository := maintGroupRepoRoleUser.NewMaintGroupRoleUserRepository(db) permissionsRepository := permissionsRepo.NewPermissionsRepository(db) categoryRepository := categoryRepo.NewCategoryRepository(db) + uomRepository := uomRepo.NewUomRepository(db) // Service userServ := userService.NewUserService(userRepository, refreshTokenRepository, jwtService, db) @@ -92,6 +97,7 @@ func RegisterDependencies(injector *do.Injector) { clientServ := clientService.NewClientService(clientRepository, db) permissionsServ := permissionsService.NewPermissionsService(permissionsRepository, db) categoryServ := categoryService.NewCategoryService(categoryRepository, db) + uomServ := uomService.NewUomService(uomRepository, db) // Controller do.Provide( @@ -144,4 +150,9 @@ func RegisterDependencies(injector *do.Injector) { return categoryController.NewCategoryController(i, categoryServ), nil }, ) + do.Provide( + injector, func(i *do.Injector) (uomController.UomController, error) { + return uomController.NewUomController(i, uomServ), nil + }, + ) }