feat(uom): Add UOM management with CRUD operations and routing
Deploy Application / deploy (push) Successful in 22s Details

This commit is contained in:
Habib Fatkhul Rohman 2025-10-30 14:20:20 +07:00
parent fbbbeb5304
commit 02dc2c71d8
10 changed files with 510 additions and 0 deletions

View File

@ -18,6 +18,7 @@ import (
"github.com/Caknoooo/go-gin-clean-starter/modules/permissions" "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/product"
"github.com/Caknoooo/go-gin-clean-starter/modules/role" "github.com/Caknoooo/go-gin-clean-starter/modules/role"
"github.com/Caknoooo/go-gin-clean-starter/modules/uom"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
swaggerFiles "github.com/swaggo/files" swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger" ginSwagger "github.com/swaggo/gin-swagger"
@ -151,6 +152,7 @@ func main() {
permissions.RegisterRoutes(server, injector) permissions.RegisterRoutes(server, injector)
product.RegisterRoutes(server, injector) product.RegisterRoutes(server, injector)
category.RegisterRoutes(server, injector) category.RegisterRoutes(server, injector)
uom.RegisterRoutes(server, injector)
// register swagger route // register swagger route
server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

View File

@ -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"
}

View File

@ -22,6 +22,7 @@ func Migrate(db *gorm.DB) error {
&entities.M_MaintenanceGroupRoleUser{}, &entities.M_MaintenanceGroupRoleUser{},
&entities.MProductEntity{}, &entities.MProductEntity{},
&entities.MCategoryEntity{}, &entities.MCategoryEntity{},
&entities.MUomEntity{},
); err != nil { ); err != nil {
return err return err
} }
@ -47,6 +48,7 @@ func MigrateFresh(db *gorm.DB) error {
&entities.M_MaintenanceGroupRoleUser{}, &entities.M_MaintenanceGroupRoleUser{},
&entities.MCategoryEntity{}, &entities.MCategoryEntity{},
&entities.MProductEntity{}, &entities.MProductEntity{},
&entities.MUomEntity{},
); err != nil { ); err != nil {
return err return err
} }

View File

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

View File

@ -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"`
}

View File

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

View File

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

24
modules/uom/routes.go Normal file
View File

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

View File

@ -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,
},
}
}

View File

@ -38,6 +38,10 @@ import (
categoryRepo "github.com/Caknoooo/go-gin-clean-starter/modules/category/repository" categoryRepo "github.com/Caknoooo/go-gin-clean-starter/modules/category/repository"
categoryService "github.com/Caknoooo/go-gin-clean-starter/modules/category/service" 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/controller"
"github.com/Caknoooo/go-gin-clean-starter/modules/user/repository" "github.com/Caknoooo/go-gin-clean-starter/modules/user/repository"
userService "github.com/Caknoooo/go-gin-clean-starter/modules/user/service" userService "github.com/Caknoooo/go-gin-clean-starter/modules/user/service"
@ -82,6 +86,7 @@ func RegisterDependencies(injector *do.Injector) {
maintenanceGroupRoleUserRepository := maintGroupRepoRoleUser.NewMaintGroupRoleUserRepository(db) maintenanceGroupRoleUserRepository := maintGroupRepoRoleUser.NewMaintGroupRoleUserRepository(db)
permissionsRepository := permissionsRepo.NewPermissionsRepository(db) permissionsRepository := permissionsRepo.NewPermissionsRepository(db)
categoryRepository := categoryRepo.NewCategoryRepository(db) categoryRepository := categoryRepo.NewCategoryRepository(db)
uomRepository := uomRepo.NewUomRepository(db)
// Service // Service
userServ := userService.NewUserService(userRepository, refreshTokenRepository, jwtService, db) userServ := userService.NewUserService(userRepository, refreshTokenRepository, jwtService, db)
@ -92,6 +97,7 @@ func RegisterDependencies(injector *do.Injector) {
clientServ := clientService.NewClientService(clientRepository, db) clientServ := clientService.NewClientService(clientRepository, db)
permissionsServ := permissionsService.NewPermissionsService(permissionsRepository, db) permissionsServ := permissionsService.NewPermissionsService(permissionsRepository, db)
categoryServ := categoryService.NewCategoryService(categoryRepository, db) categoryServ := categoryService.NewCategoryService(categoryRepository, db)
uomServ := uomService.NewUomService(uomRepository, db)
// Controller // Controller
do.Provide( do.Provide(
@ -144,4 +150,9 @@ func RegisterDependencies(injector *do.Injector) {
return categoryController.NewCategoryController(i, categoryServ), nil return categoryController.NewCategoryController(i, categoryServ), nil
}, },
) )
do.Provide(
injector, func(i *do.Injector) (uomController.UomController, error) {
return uomController.NewUomController(i, uomServ), nil
},
)
} }