diff --git a/cmd/main.go b/cmd/main.go index d8ae15b..a74e892 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -20,6 +20,7 @@ import ( "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/Caknoooo/go-gin-clean-starter/modules/warehouse" "github.com/sirupsen/logrus" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" @@ -155,6 +156,7 @@ func main() { category.RegisterRoutes(server, injector) uom.RegisterRoutes(server, injector) mvendor.RegisterRoutes(server, injector) + warehouse.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 a11c186..e2eedb2 100644 --- a/database/migration.go +++ b/database/migration.go @@ -55,7 +55,7 @@ func MigrateFresh(db *gorm.DB) error { &entities.MProductEntity{}, // &entities.MUomEntity{}, // &entities.MVendorEntity{}, - // &entities.MCrossReferenceEntity{}, + &entities.MCrossReferenceEntity{}, &entities.MWarehouseEntity{}, &entities.MZonaEntity{}, &entities.MAisleEntity{}, diff --git a/modules/warehouse/controller/warehouse_controller.go b/modules/warehouse/controller/warehouse_controller.go new file mode 100644 index 0000000..da4920f --- /dev/null +++ b/modules/warehouse/controller/warehouse_controller.go @@ -0,0 +1,114 @@ +package controller + +import ( + "net/http" + + "github.com/Caknoooo/go-gin-clean-starter/modules/warehouse/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/warehouse/query" + "github.com/Caknoooo/go-gin-clean-starter/modules/warehouse/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 WarehouseController interface { + Create(ctx *gin.Context) + Update(ctx *gin.Context) + Delete(ctx *gin.Context) + GetById(ctx *gin.Context) + GetAll(ctx *gin.Context) +} + +type warehouseController struct { + warehouseService service.WarehouseService + db *gorm.DB +} + +func (w *warehouseController) Create(ctx *gin.Context) { + clientId := ctx.MustGet("client_id").(string) + var req dto.WarehouseCreateRequest + req.ClientID = clientId + 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 := w.warehouseService.Create(ctx, req) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_WAREHOUSE, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_CREATE_WAREHOUSE, created) + ctx.JSON(http.StatusOK, res) +} + +func (w *warehouseController) Update(ctx *gin.Context) { + id := ctx.Param("id") + var req dto.WarehouseUpdateRequest + 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 := w.warehouseService.Update(ctx, req, id) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_UPDATE_WAREHOUSE, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_UPDATE_WAREHOUSE, updated) + ctx.JSON(http.StatusOK, res) +} + +func (w *warehouseController) Delete(ctx *gin.Context) { + id := ctx.Param("id") + if err := w.warehouseService.Delete(ctx, id); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_DELETE_WAREHOUSE, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_DELETE_WAREHOUSE, nil) + ctx.JSON(http.StatusOK, res) +} + +func (w *warehouseController) GetById(ctx *gin.Context) { + id := ctx.Param("id") + warehouse, err := w.warehouseService.GetById(ctx, id) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_WAREHOUSE, err.Error(), nil) + ctx.JSON(http.StatusNotFound, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_WAREHOUSE, warehouse) + ctx.JSON(http.StatusOK, res) +} + +func (w *warehouseController) GetAll(ctx *gin.Context) { + clientId := ctx.MustGet("client_id").(string) + var filter query.WarehouseFilter + filter.ClientID = clientId + if err := ctx.ShouldBindQuery(&filter); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_WAREHOUSE, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + warehouses, total, err := w.warehouseService.GetAll(ctx, filter) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_WAREHOUSE, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_WAREHOUSE, gin.H{"data": warehouses, "total": total}) + ctx.JSON(http.StatusOK, res) +} + +func NewWarehouseController(i *do.Injector, warehouseService service.WarehouseService) WarehouseController { + db := do.MustInvokeNamed[*gorm.DB](i, constants.DB) + return &warehouseController{ + warehouseService: warehouseService, + db: db, + } +} diff --git a/modules/warehouse/dto/warehouse_dto.go b/modules/warehouse/dto/warehouse_dto.go new file mode 100644 index 0000000..2805b25 --- /dev/null +++ b/modules/warehouse/dto/warehouse_dto.go @@ -0,0 +1,52 @@ +package dto + +import ( + "errors" +) + +const ( + MESSAGE_FAILED_CREATE_WAREHOUSE = "failed create warehouse" + MESSAGE_SUCCESS_CREATE_WAREHOUSE = "success create warehouse" + MESSAGE_FAILED_GET_WAREHOUSE = "failed get warehouse" + MESSAGE_SUCCESS_GET_WAREHOUSE = "success get warehouse" + MESSAGE_FAILED_UPDATE_WAREHOUSE = "failed update warehouse" + MESSAGE_SUCCESS_UPDATE_WAREHOUSE = "success update warehouse" + MESSAGE_FAILED_DELETE_WAREHOUSE = "failed delete warehouse" + MESSAGE_SUCCESS_DELETE_WAREHOUSE = "success delete warehouse" + MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body" +) + +var ( + ErrCreateWarehouse = errors.New("failed to create warehouse") + ErrGetWarehouseById = errors.New("failed to get warehouse by id") + ErrUpdateWarehouse = errors.New("failed to update warehouse") + ErrDeleteWarehouse = errors.New("failed to delete warehouse") +) + +type WarehouseCreateRequest struct { + Code string `json:"code" binding:"required"` + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Status string `json:"status" binding:"required"` + DissallowNegativeInventory bool `json:"dissallow_negative_inventory"` + ClientID string `json:"client_id" binding:"required"` +} + +type WarehouseUpdateRequest struct { + Code string `json:"code"` + Name string `json:"name"` + Description string `json:"description"` + Status string `json:"status"` + DissallowNegativeInventory bool `json:"dissallow_negative_inventory"` + ClientID string `json:"client_id"` +} + +type WarehouseResponse struct { + ID string `json:"id"` + Code string `json:"code"` + Name string `json:"name"` + Description string `json:"description"` + Status string `json:"status"` + DissallowNegativeInventory bool `json:"dissallow_negative_inventory"` + ClientID string `json:"client_id"` +} diff --git a/modules/warehouse/query/warehouse_query.go b/modules/warehouse/query/warehouse_query.go new file mode 100644 index 0000000..acb6e5d --- /dev/null +++ b/modules/warehouse/query/warehouse_query.go @@ -0,0 +1,30 @@ +package query + +import ( + "gorm.io/gorm" +) + +type WarehouseFilter struct { + Name string `form:"name"` + Code string `form:"code"` + Status string `form:"status"` + ClientID string `form:"client_id"` + PerPage int `form:"per_page"` + Page int `form:"page"` +} + +func ApplyWarehouseFilters(db *gorm.DB, filter WarehouseFilter) *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.Status != "" { + db = db.Where("status = ?", filter.Status) + } + return db +} diff --git a/modules/warehouse/repository/warehouse_repository.go b/modules/warehouse/repository/warehouse_repository.go new file mode 100644 index 0000000..0a54d06 --- /dev/null +++ b/modules/warehouse/repository/warehouse_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/warehouse/query" + "gorm.io/gorm" +) + +type WarehouseRepository interface { + Create(ctx context.Context, tx *gorm.DB, warehouse entities.MWarehouseEntity) (entities.MWarehouseEntity, error) + GetById(ctx context.Context, tx *gorm.DB, warehouseId string) (entities.MWarehouseEntity, error) + GetAll(ctx context.Context, filter query.WarehouseFilter) ([]entities.MWarehouseEntity, int64, error) + Update(ctx context.Context, tx *gorm.DB, warehouse entities.MWarehouseEntity) (entities.MWarehouseEntity, error) + Delete(ctx context.Context, tx *gorm.DB, warehouseId string) error +} + +type warehouseRepository struct { + db *gorm.DB +} + +func NewWarehouseRepository(db *gorm.DB) WarehouseRepository { + return &warehouseRepository{db: db} +} + +func (r *warehouseRepository) Create(ctx context.Context, tx *gorm.DB, warehouse entities.MWarehouseEntity) (entities.MWarehouseEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Create(&warehouse).Error; err != nil { + return warehouse, err + } + return warehouse, nil +} + +func (r *warehouseRepository) GetById(ctx context.Context, tx *gorm.DB, warehouseId string) (entities.MWarehouseEntity, error) { + if tx == nil { + tx = r.db + } + var warehouse entities.MWarehouseEntity + if err := tx.WithContext(ctx).Preload("Client").First(&warehouse, "id = ?", warehouseId).Error; err != nil { + return warehouse, err + } + return warehouse, nil +} + +func (r *warehouseRepository) GetAll(ctx context.Context, filter query.WarehouseFilter) ([]entities.MWarehouseEntity, int64, error) { + var warehouses []entities.MWarehouseEntity + var total int64 + db := query.ApplyWarehouseFilters(r.db, filter) + db.Model(&entities.MWarehouseEntity{}).Count(&total) + if filter.PerPage > 0 && filter.Page > 0 { + db = db.Offset((filter.Page - 1) * filter.PerPage).Limit(filter.PerPage) + } + if err := db.Find(&warehouses).Error; err != nil { + return warehouses, total, err + } + return warehouses, total, nil +} + +func (r *warehouseRepository) Update(ctx context.Context, tx *gorm.DB, warehouse entities.MWarehouseEntity) (entities.MWarehouseEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Model(&entities.MWarehouseEntity{}).Where("id = ?", warehouse.ID).Updates(&warehouse).Error; err != nil { + return warehouse, err + } + return warehouse, nil +} + +func (r *warehouseRepository) Delete(ctx context.Context, tx *gorm.DB, warehouseId string) error { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Delete(&entities.MWarehouseEntity{}, "id = ?", warehouseId).Error; err != nil { + return err + } + return nil +} diff --git a/modules/warehouse/routes.go b/modules/warehouse/routes.go new file mode 100644 index 0000000..1bd7b14 --- /dev/null +++ b/modules/warehouse/routes.go @@ -0,0 +1,24 @@ +package warehouse + +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/warehouse/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) { + warehouseController := do.MustInvoke[controller.WarehouseController](injector) + jwtService := do.MustInvokeNamed[service.JWTService](injector, constants.JWTService) + + warehouseRoutes := server.Group("/api/v1/warehouses") + { + warehouseRoutes.POST("", middlewares.Authenticate(jwtService), warehouseController.Create) + warehouseRoutes.GET("/:id", middlewares.Authenticate(jwtService), warehouseController.GetById) + warehouseRoutes.PUT("/:id", middlewares.Authenticate(jwtService), warehouseController.Update) + warehouseRoutes.DELETE("/:id", middlewares.Authenticate(jwtService), warehouseController.Delete) + warehouseRoutes.GET("", middlewares.Authenticate(jwtService), warehouseController.GetAll) + } +} diff --git a/modules/warehouse/service/warehouse_service.go b/modules/warehouse/service/warehouse_service.go new file mode 100644 index 0000000..2dc5fa1 --- /dev/null +++ b/modules/warehouse/service/warehouse_service.go @@ -0,0 +1,151 @@ +package service + +import ( + "context" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + dtodomain "github.com/Caknoooo/go-gin-clean-starter/modules/warehouse/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/warehouse/query" + "github.com/Caknoooo/go-gin-clean-starter/modules/warehouse/repository" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type WarehouseService interface { + Create(ctx context.Context, req dtodomain.WarehouseCreateRequest) (dtodomain.WarehouseResponse, error) + GetById(ctx context.Context, warehouseId string) (dtodomain.WarehouseResponse, error) + GetAll(ctx context.Context, filter query.WarehouseFilter) ([]dtodomain.WarehouseResponse, int64, error) + Update(ctx context.Context, req dtodomain.WarehouseUpdateRequest, warehouseId string) (dtodomain.WarehouseResponse, error) + Delete(ctx context.Context, warehouseId string) error +} + +type warehouseService struct { + db *gorm.DB + warehouseRepo repository.WarehouseRepository +} + +func toWarehouseResponse(e entities.MWarehouseEntity) dtodomain.WarehouseResponse { + return dtodomain.WarehouseResponse{ + ID: e.ID.String(), + Code: e.Code, + Name: e.Name, + Description: e.Description, + Status: e.Status, + DissallowNegativeInventory: e.DissallowNegativeInventory, + ClientID: e.ClientID.String(), + } +} + +func (w *warehouseService) Create(ctx context.Context, req dtodomain.WarehouseCreateRequest) (dtodomain.WarehouseResponse, error) { + tx := w.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + clientUUID, err := uuid.Parse(req.ClientID) + if err != nil { + tx.Rollback() + return dtodomain.WarehouseResponse{}, err + } + warehouse := entities.MWarehouseEntity{ + Code: req.Code, + Name: req.Name, + Description: req.Description, + Status: req.Status, + DissallowNegativeInventory: req.DissallowNegativeInventory, + ClientID: clientUUID, + } + created, err := w.warehouseRepo.Create(ctx, tx, warehouse) + if err != nil { + tx.Rollback() + return dtodomain.WarehouseResponse{}, err + } + tx.Commit() + result, err := w.warehouseRepo.GetById(ctx, nil, created.ID.String()) + if err != nil { + return dtodomain.WarehouseResponse{}, err + } + return toWarehouseResponse(result), nil +} + +func (w *warehouseService) GetById(ctx context.Context, warehouseId string) (dtodomain.WarehouseResponse, error) { + warehouse, err := w.warehouseRepo.GetById(ctx, nil, warehouseId) + if err != nil { + return dtodomain.WarehouseResponse{}, err + } + return toWarehouseResponse(warehouse), nil +} + +func (w *warehouseService) GetAll(ctx context.Context, filter query.WarehouseFilter) ([]dtodomain.WarehouseResponse, int64, error) { + warehouses, total, err := w.warehouseRepo.GetAll(ctx, filter) + if err != nil { + return nil, 0, err + } + var responses []dtodomain.WarehouseResponse + for _, e := range warehouses { + responses = append(responses, toWarehouseResponse(e)) + } + if responses == nil { + responses = make([]dtodomain.WarehouseResponse, 0) + } + return responses, total, nil +} + +func (w *warehouseService) Update(ctx context.Context, req dtodomain.WarehouseUpdateRequest, warehouseId string) (dtodomain.WarehouseResponse, error) { + tx := w.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + warehouse, err := w.warehouseRepo.GetById(ctx, tx, warehouseId) + if err != nil { + tx.Rollback() + return dtodomain.WarehouseResponse{}, err + } + if req.Code != "" { + warehouse.Code = req.Code + } + if req.Name != "" { + warehouse.Name = req.Name + } + warehouse.Description = req.Description + warehouse.Status = req.Status + warehouse.DissallowNegativeInventory = req.DissallowNegativeInventory + if req.ClientID != "" { + clientUUID, err := uuid.Parse(req.ClientID) + if err == nil { + warehouse.ClientID = clientUUID + } + } + updated, err := w.warehouseRepo.Update(ctx, tx, warehouse) + if err != nil { + tx.Rollback() + return dtodomain.WarehouseResponse{}, err + } + tx.Commit() + return toWarehouseResponse(updated), nil +} + +func (w *warehouseService) Delete(ctx context.Context, warehouseId string) error { + tx := w.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + if err := w.warehouseRepo.Delete(ctx, tx, warehouseId); err != nil { + tx.Rollback() + return err + } + tx.Commit() + return nil +} + +func NewWarehouseService(warehouseRepo repository.WarehouseRepository, db *gorm.DB) WarehouseService { + return &warehouseService{ + warehouseRepo: warehouseRepo, + db: db, + } +} diff --git a/providers/core.go b/providers/core.go index e70f4c3..1579b27 100644 --- a/providers/core.go +++ b/providers/core.go @@ -46,6 +46,10 @@ import ( mvendorRepo "github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/repository" mvendorService "github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/service" + warehouseController "github.com/Caknoooo/go-gin-clean-starter/modules/warehouse/controller" + warehouseRepo "github.com/Caknoooo/go-gin-clean-starter/modules/warehouse/repository" + warehouseService "github.com/Caknoooo/go-gin-clean-starter/modules/warehouse/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" @@ -92,6 +96,7 @@ func RegisterDependencies(injector *do.Injector) { categoryRepository := categoryRepo.NewCategoryRepository(db) uomRepository := uomRepo.NewUomRepository(db) mvendorRepository := mvendorRepo.NewVendorRepository(db) + warehouseRepository := warehouseRepo.NewWarehouseRepository(db) // Service userServ := userService.NewUserService(userRepository, refreshTokenRepository, jwtService, db) @@ -104,6 +109,7 @@ func RegisterDependencies(injector *do.Injector) { categoryServ := categoryService.NewCategoryService(categoryRepository, db) uomServ := uomService.NewUomService(uomRepository, db) mvendorServ := mvendorService.NewVendorService(mvendorRepository, db) + warehouseServ := warehouseService.NewWarehouseService(warehouseRepository, db) // Controller do.Provide( @@ -166,4 +172,9 @@ func RegisterDependencies(injector *do.Injector) { return mvendorController.NewVendorController(i, mvendorServ), nil }, ) + do.Provide( + injector, func(i *do.Injector) (warehouseController.WarehouseController, error) { + return warehouseController.NewWarehouseController(i, warehouseServ), nil + }, + ) }