From a432c007bf0e9673cb0ce000079c70e6295b3a95 Mon Sep 17 00:00:00 2001 From: Habib Fatkhul Rohman Date: Mon, 10 Nov 2025 14:41:11 +0700 Subject: [PATCH] feat(zona): Implement Zona module with CRUD operations and routing --- cmd/main.go | 2 + database/migration.go | 4 +- modules/zona/controller/zona_controller.go | 131 ++++++++++++++++ modules/zona/dto/zona_dto.go | 59 +++++++ modules/zona/query/zona_query.go | 32 ++++ modules/zona/repository/zona_repository.go | 80 ++++++++++ modules/zona/routes.go | 24 +++ modules/zona/service/zona_service.go | 172 +++++++++++++++++++++ providers/core.go | 11 ++ 9 files changed, 513 insertions(+), 2 deletions(-) create mode 100644 modules/zona/controller/zona_controller.go create mode 100644 modules/zona/dto/zona_dto.go create mode 100644 modules/zona/query/zona_query.go create mode 100644 modules/zona/repository/zona_repository.go create mode 100644 modules/zona/routes.go create mode 100644 modules/zona/service/zona_service.go diff --git a/cmd/main.go b/cmd/main.go index a74e892..4e10bdf 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -21,6 +21,7 @@ import ( "github.com/Caknoooo/go-gin-clean-starter/modules/role" "github.com/Caknoooo/go-gin-clean-starter/modules/uom" "github.com/Caknoooo/go-gin-clean-starter/modules/warehouse" + "github.com/Caknoooo/go-gin-clean-starter/modules/zona" "github.com/sirupsen/logrus" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" @@ -157,6 +158,7 @@ func main() { uom.RegisterRoutes(server, injector) mvendor.RegisterRoutes(server, injector) warehouse.RegisterRoutes(server, injector) + zona.RegisterRoutes(server, injector) // register swagger route server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) diff --git a/database/migration.go b/database/migration.go index e2eedb2..6fb3457 100644 --- a/database/migration.go +++ b/database/migration.go @@ -52,10 +52,10 @@ func MigrateFresh(db *gorm.DB) error { // &entities.M_MaintenanceGroupRole{}, // &entities.M_MaintenanceGroupRoleUser{}, // &entities.MCategoryEntity{}, - &entities.MProductEntity{}, + // &entities.MProductEntity{}, // &entities.MUomEntity{}, // &entities.MVendorEntity{}, - &entities.MCrossReferenceEntity{}, + // &entities.MCrossReferenceEntity{}, &entities.MWarehouseEntity{}, &entities.MZonaEntity{}, &entities.MAisleEntity{}, diff --git a/modules/zona/controller/zona_controller.go b/modules/zona/controller/zona_controller.go new file mode 100644 index 0000000..49b0bc8 --- /dev/null +++ b/modules/zona/controller/zona_controller.go @@ -0,0 +1,131 @@ +package controller + +import ( + "net/http" + + "github.com/Caknoooo/go-gin-clean-starter/modules/zona/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/zona/query" + "github.com/Caknoooo/go-gin-clean-starter/modules/zona/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 ZonaController interface { + Create(ctx *gin.Context) + Update(ctx *gin.Context) + Delete(ctx *gin.Context) + GetById(ctx *gin.Context) + GetAll(ctx *gin.Context) +} + +type zonaController struct { + zonaService service.ZonaService + db *gorm.DB +} + +func (z *zonaController) Create(ctx *gin.Context) { + var req dto.ZonaCreateRequest + 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 := z.zonaService.Create(ctx, req) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_ZONA, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_CREATE_ZONA, created) + ctx.JSON(http.StatusOK, res) +} + +func (z *zonaController) Update(ctx *gin.Context) { + id := ctx.Param("id") + var req dto.ZonaUpdateRequest + 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 := z.zonaService.Update(ctx, req, id) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_UPDATE_ZONA, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_UPDATE_ZONA, updated) + ctx.JSON(http.StatusOK, res) +} + +func (z *zonaController) Delete(ctx *gin.Context) { + id := ctx.Param("id") + if err := z.zonaService.Delete(ctx, id); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_DELETE_ZONA, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_DELETE_ZONA, nil) + ctx.JSON(http.StatusOK, res) +} + +func (z *zonaController) GetById(ctx *gin.Context) { + id := ctx.Param("id") + zona, err := z.zonaService.GetById(ctx, id) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_ZONA, err.Error(), nil) + ctx.JSON(http.StatusNotFound, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_ZONA, zona) + ctx.JSON(http.StatusOK, res) +} + +func (z *zonaController) GetAll(ctx *gin.Context) { + clientId := ctx.MustGet("client_id").(string) + var filter query.ZonaFilter + filter.ClientID = clientId + if err := ctx.ShouldBindQuery(&filter); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_ZONA, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + + getAll := ctx.Query("get_all") + if getAll != "" { + zonas, _, err := z.zonaService.GetAll(ctx, filter) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_ZONA, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + response := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_ZONA, zonas) + ctx.JSON(http.StatusOK, response) + return + } + + perPage := utils.ParseInt(ctx.DefaultQuery("per_page", "10")) + page := utils.ParseInt(ctx.DefaultQuery("page", "1")) + filter.PerPage = perPage + filter.Page = (page - 1) * perPage + zonas, total, err := z.zonaService.GetAll(ctx, filter) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_ZONA, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + paginationResponse := utils.BuildPaginationResponse(perPage, page, total) + res := utils.BuildResponseSuccessWithPagination(http.StatusOK, dto.MESSAGE_SUCCESS_GET_ZONA, zonas, paginationResponse) + ctx.JSON(http.StatusOK, res) +} + +func NewZonaController(i *do.Injector, zonaService service.ZonaService) ZonaController { + db := do.MustInvokeNamed[*gorm.DB](i, constants.DB) + return &zonaController{ + zonaService: zonaService, + db: db, + } +} diff --git a/modules/zona/dto/zona_dto.go b/modules/zona/dto/zona_dto.go new file mode 100644 index 0000000..ef76cc9 --- /dev/null +++ b/modules/zona/dto/zona_dto.go @@ -0,0 +1,59 @@ +package dto + +import "errors" + +const ( + MESSAGE_FAILED_CREATE_ZONA = "failed create zona" + MESSAGE_SUCCESS_CREATE_ZONA = "success create zona" + MESSAGE_FAILED_GET_ZONA = "failed get zona" + MESSAGE_SUCCESS_GET_ZONA = "success get zona" + MESSAGE_FAILED_UPDATE_ZONA = "failed update zona" + MESSAGE_SUCCESS_UPDATE_ZONA = "success update zona" + MESSAGE_FAILED_DELETE_ZONA = "failed delete zona" + MESSAGE_SUCCESS_DELETE_ZONA = "success delete zona" + MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body" +) + +var ( + ErrCreateZona = errors.New("failed to create zona") + ErrGetZonaById = errors.New("failed to get zona by id") + ErrUpdateZona = errors.New("failed to update zona") + ErrDeleteZona = errors.New("failed to delete zona") +) + +type ZonaCreateRequest struct { + Code string `json:"code" binding:"required"` + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required"` + Temperature string `json:"temperature"` + Hazardous bool `json:"hazardous"` + // QRCodeZone string `json:"qr_code_zone"` + // IsActive bool `json:"is_active"` + WarehouseID string `json:"warehouse_id" binding:"required"` + ClientID string `json:"client_id" binding:"required"` +} + +type ZonaUpdateRequest struct { + Code string `json:"code"` + Name string `json:"name"` + Type string `json:"type"` + Temperature string `json:"temperature"` + Hazardous bool `json:"hazardous"` + // QRCodeZone string `json:"qr_code_zone"` + IsActive bool `json:"is_active"` + // WarehouseID string `json:"warehouse_id"` + // ClientID string `json:"client_id"` +} + +type ZonaResponse struct { + ID string `json:"id"` + Code string `json:"code"` + Name string `json:"name"` + Type string `json:"type"` + Temperature string `json:"temperature"` + Hazardous bool `json:"hazardous"` + QRCodeZone string `json:"qr_code_zone"` + IsActive bool `json:"is_active"` + WarehouseID string `json:"warehouse_id"` + ClientID string `json:"client_id"` +} diff --git a/modules/zona/query/zona_query.go b/modules/zona/query/zona_query.go new file mode 100644 index 0000000..d72330b --- /dev/null +++ b/modules/zona/query/zona_query.go @@ -0,0 +1,32 @@ +package query + +import "gorm.io/gorm" + +type ZonaFilter struct { + Name string `form:"name"` + Code string `form:"code"` + Type string `form:"type"` + WarehouseID string `form:"warehouse_id"` + ClientID string `form:"client_id"` + PerPage int `form:"per_page"` + Page int `form:"page"` +} + +func ApplyZonaFilters(db *gorm.DB, filter ZonaFilter) *gorm.DB { + if filter.Name != "" { + db = db.Where("name ILIKE ?", "%"+filter.Name+"%") + } + if filter.Code != "" { + db = db.Where("code ILIKE ?", "%"+filter.Code+"%") + } + if filter.Type != "" { + db = db.Where("type = ?", filter.Type) + } + if filter.WarehouseID != "" { + db = db.Where("warehouse_id = ?", filter.WarehouseID) + } + if filter.ClientID != "" { + db = db.Where("client_id = ?", filter.ClientID) + } + return db +} diff --git a/modules/zona/repository/zona_repository.go b/modules/zona/repository/zona_repository.go new file mode 100644 index 0000000..9d82c0b --- /dev/null +++ b/modules/zona/repository/zona_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/zona/query" + "gorm.io/gorm" +) + +type ZonaRepository interface { + Create(ctx context.Context, tx *gorm.DB, zona entities.MZonaEntity) (entities.MZonaEntity, error) + GetById(ctx context.Context, tx *gorm.DB, zonaId string) (entities.MZonaEntity, error) + GetAll(ctx context.Context, filter query.ZonaFilter) ([]entities.MZonaEntity, int64, error) + Update(ctx context.Context, tx *gorm.DB, zona entities.MZonaEntity) (entities.MZonaEntity, error) + Delete(ctx context.Context, tx *gorm.DB, zonaId string) error +} + +type zonaRepository struct { + db *gorm.DB +} + +func NewZonaRepository(db *gorm.DB) ZonaRepository { + return &zonaRepository{db: db} +} + +func (r *zonaRepository) Create(ctx context.Context, tx *gorm.DB, zona entities.MZonaEntity) (entities.MZonaEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Create(&zona).Error; err != nil { + return zona, err + } + return zona, nil +} + +func (r *zonaRepository) GetById(ctx context.Context, tx *gorm.DB, zonaId string) (entities.MZonaEntity, error) { + if tx == nil { + tx = r.db + } + var zona entities.MZonaEntity + if err := tx.WithContext(ctx).Preload("Client").Preload("Warehouse").First(&zona, "id = ?", zonaId).Error; err != nil { + return zona, err + } + return zona, nil +} + +func (r *zonaRepository) GetAll(ctx context.Context, filter query.ZonaFilter) ([]entities.MZonaEntity, int64, error) { + var zonas []entities.MZonaEntity + var total int64 + db := query.ApplyZonaFilters(r.db, filter) + db.Model(&entities.MZonaEntity{}).Count(&total) + if filter.PerPage > 0 && filter.Page > 0 { + db = db.Offset((filter.Page - 1) * filter.PerPage).Limit(filter.PerPage) + } + if err := db.Find(&zonas).Error; err != nil { + return zonas, total, err + } + return zonas, total, nil +} + +func (r *zonaRepository) Update(ctx context.Context, tx *gorm.DB, zona entities.MZonaEntity) (entities.MZonaEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Model(&entities.MZonaEntity{}).Where("id = ?", zona.ID).Updates(&zona).Error; err != nil { + return zona, err + } + return zona, nil +} + +func (r *zonaRepository) Delete(ctx context.Context, tx *gorm.DB, zonaId string) error { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Delete(&entities.MZonaEntity{}, "id = ?", zonaId).Error; err != nil { + return err + } + return nil +} diff --git a/modules/zona/routes.go b/modules/zona/routes.go new file mode 100644 index 0000000..e9ba5fe --- /dev/null +++ b/modules/zona/routes.go @@ -0,0 +1,24 @@ +package zona + +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/zona/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) { + zonaController := do.MustInvoke[controller.ZonaController](injector) + jwtService := do.MustInvokeNamed[service.JWTService](injector, constants.JWTService) + + zonaRoutes := server.Group("/api/v1/zonas") + { + zonaRoutes.POST("", middlewares.Authenticate(jwtService), zonaController.Create) + zonaRoutes.GET("/:id", middlewares.Authenticate(jwtService), zonaController.GetById) + zonaRoutes.PUT("/:id", middlewares.Authenticate(jwtService), zonaController.Update) + zonaRoutes.DELETE("/:id", middlewares.Authenticate(jwtService), zonaController.Delete) + zonaRoutes.GET("", middlewares.Authenticate(jwtService), zonaController.GetAll) + } +} diff --git a/modules/zona/service/zona_service.go b/modules/zona/service/zona_service.go new file mode 100644 index 0000000..2e5ca30 --- /dev/null +++ b/modules/zona/service/zona_service.go @@ -0,0 +1,172 @@ +package service + +import ( + "context" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + "github.com/Caknoooo/go-gin-clean-starter/modules/zona/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/zona/query" + "github.com/Caknoooo/go-gin-clean-starter/modules/zona/repository" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type ZonaService interface { + Create(ctx context.Context, req dto.ZonaCreateRequest) (dto.ZonaResponse, error) + GetById(ctx context.Context, zonaId string) (dto.ZonaResponse, error) + GetAll(ctx context.Context, filter query.ZonaFilter) ([]dto.ZonaResponse, int64, error) + Update(ctx context.Context, req dto.ZonaUpdateRequest, zonaId string) (dto.ZonaResponse, error) + Delete(ctx context.Context, zonaId string) error +} + +type zonaService struct { + db *gorm.DB + zonaRepo repository.ZonaRepository +} + +func toZonaResponse(e entities.MZonaEntity) dto.ZonaResponse { + return dto.ZonaResponse{ + ID: e.ID.String(), + Code: e.Code, + Name: e.Name, + Type: e.Type, + Temperature: e.Temperature, + Hazardous: e.Hazardous, + QRCodeZone: e.QRCodeZone, + IsActive: e.IsActive, + WarehouseID: e.WarehouseID.String(), + ClientID: e.ClientID.String(), + } +} + +func (s *zonaService) Create(ctx context.Context, req dto.ZonaCreateRequest) (dto.ZonaResponse, error) { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + warehouseUUID, err := uuid.Parse(req.WarehouseID) + if err != nil { + tx.Rollback() + return dto.ZonaResponse{}, err + } + clientUUID, err := uuid.Parse(req.ClientID) + if err != nil { + tx.Rollback() + return dto.ZonaResponse{}, err + } + zona := entities.MZonaEntity{ + Code: req.Code, + Name: req.Name, + Type: req.Type, + Temperature: req.Temperature, + Hazardous: req.Hazardous, + // QRCodeZone: req.QRCodeZone, + // IsActive: req.IsActive, + WarehouseID: warehouseUUID, + ClientID: clientUUID, + } + created, err := s.zonaRepo.Create(ctx, tx, zona) + if err != nil { + tx.Rollback() + return dto.ZonaResponse{}, err + } + tx.Commit() + result, err := s.zonaRepo.GetById(ctx, nil, created.ID.String()) + if err != nil { + return dto.ZonaResponse{}, err + } + return toZonaResponse(result), nil +} + +func (s *zonaService) GetById(ctx context.Context, zonaId string) (dto.ZonaResponse, error) { + zona, err := s.zonaRepo.GetById(ctx, nil, zonaId) + if err != nil { + return dto.ZonaResponse{}, err + } + return toZonaResponse(zona), nil +} + +func (s *zonaService) GetAll(ctx context.Context, filter query.ZonaFilter) ([]dto.ZonaResponse, int64, error) { + zonas, total, err := s.zonaRepo.GetAll(ctx, filter) + if err != nil { + return nil, 0, err + } + var responses []dto.ZonaResponse + for _, e := range zonas { + responses = append(responses, toZonaResponse(e)) + } + if responses == nil { + responses = make([]dto.ZonaResponse, 0) + } + return responses, total, nil +} + +func (s *zonaService) Update(ctx context.Context, req dto.ZonaUpdateRequest, zonaId string) (dto.ZonaResponse, error) { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + zona, err := s.zonaRepo.GetById(ctx, tx, zonaId) + if err != nil { + tx.Rollback() + return dto.ZonaResponse{}, err + } + if req.Code != "" { + zona.Code = req.Code + } + if req.Name != "" { + zona.Name = req.Name + } + if req.Type != "" { + zona.Type = req.Type + } + zona.Temperature = req.Temperature + zona.Hazardous = req.Hazardous + // zona.QRCodeZone = req.QRCodeZone + zona.IsActive = req.IsActive + // if req.WarehouseID != "" { + // warehouseUUID, err := uuid.Parse(req.WarehouseID) + // if err == nil { + // zona.WarehouseID = warehouseUUID + // } + // } + // if req.ClientID != "" { + // clientUUID, err := uuid.Parse(req.ClientID) + // if err == nil { + // zona.ClientID = clientUUID + // } + // } + updated, err := s.zonaRepo.Update(ctx, tx, zona) + if err != nil { + tx.Rollback() + return dto.ZonaResponse{}, err + } + tx.Commit() + return toZonaResponse(updated), nil +} + +func (s *zonaService) Delete(ctx context.Context, zonaId string) error { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + if err := s.zonaRepo.Delete(ctx, tx, zonaId); err != nil { + tx.Rollback() + return err + } + tx.Commit() + return nil +} + +func NewZonaService(zonaRepo repository.ZonaRepository, db *gorm.DB) ZonaService { + return &zonaService{ + zonaRepo: zonaRepo, + db: db, + } +} diff --git a/providers/core.go b/providers/core.go index 1579b27..8dd54ce 100644 --- a/providers/core.go +++ b/providers/core.go @@ -50,6 +50,10 @@ import ( warehouseRepo "github.com/Caknoooo/go-gin-clean-starter/modules/warehouse/repository" warehouseService "github.com/Caknoooo/go-gin-clean-starter/modules/warehouse/service" + zonaController "github.com/Caknoooo/go-gin-clean-starter/modules/zona/controller" + zonaRepo "github.com/Caknoooo/go-gin-clean-starter/modules/zona/repository" + zonaService "github.com/Caknoooo/go-gin-clean-starter/modules/zona/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" @@ -97,6 +101,7 @@ func RegisterDependencies(injector *do.Injector) { uomRepository := uomRepo.NewUomRepository(db) mvendorRepository := mvendorRepo.NewVendorRepository(db) warehouseRepository := warehouseRepo.NewWarehouseRepository(db) + zonaRepository := zonaRepo.NewZonaRepository(db) // Service userServ := userService.NewUserService(userRepository, refreshTokenRepository, jwtService, db) @@ -110,6 +115,7 @@ func RegisterDependencies(injector *do.Injector) { uomServ := uomService.NewUomService(uomRepository, db) mvendorServ := mvendorService.NewVendorService(mvendorRepository, db) warehouseServ := warehouseService.NewWarehouseService(warehouseRepository, db) + zonaServ := zonaService.NewZonaService(zonaRepository, db) // Controller do.Provide( @@ -177,4 +183,9 @@ func RegisterDependencies(injector *do.Injector) { return warehouseController.NewWarehouseController(i, warehouseServ), nil }, ) + do.Provide( + injector, func(i *do.Injector) (zonaController.ZonaController, error) { + return zonaController.NewZonaController(i, zonaServ), nil + }, + ) }