From 371c2f8e16db20517244ea5b1bdd07ca9ec6ea53 Mon Sep 17 00:00:00 2001 From: Habib Fatkhul Rohman Date: Wed, 5 Nov 2025 14:53:23 +0700 Subject: [PATCH] feat(vendor): Implement Vendor CRUD operations with DTOs, service, repository, and routes --- .../mvendor/controller/vendor_controller.go | 115 +++++++++++++ modules/mvendor/dto/vendor_dto.go | 54 ++++++ modules/mvendor/query/vendor_query.go | 24 +++ .../mvendor/repository/vendor_repository.go | 82 +++++++++ modules/mvendor/routes.go | 24 +++ modules/mvendor/service/vendor_service.go | 155 ++++++++++++++++++ 6 files changed, 454 insertions(+) create mode 100644 modules/mvendor/controller/vendor_controller.go create mode 100644 modules/mvendor/dto/vendor_dto.go create mode 100644 modules/mvendor/query/vendor_query.go create mode 100644 modules/mvendor/repository/vendor_repository.go create mode 100644 modules/mvendor/routes.go create mode 100644 modules/mvendor/service/vendor_service.go diff --git a/modules/mvendor/controller/vendor_controller.go b/modules/mvendor/controller/vendor_controller.go new file mode 100644 index 0000000..d40bfdf --- /dev/null +++ b/modules/mvendor/controller/vendor_controller.go @@ -0,0 +1,115 @@ +package controller + +import ( + "net/http" + + "github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/query" + "github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/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 VendorController interface { + Create(ctx *gin.Context) + Update(ctx *gin.Context) + Delete(ctx *gin.Context) + GetById(ctx *gin.Context) + GetAll(ctx *gin.Context) +} + +type vendorController struct { + vendorService service.VendorService + db *gorm.DB +} + +func NewVendorController(i *do.Injector, vendorService service.VendorService) VendorController { + db := do.MustInvokeNamed[*gorm.DB](i, constants.DB) + return &vendorController{ + vendorService: vendorService, + db: db, + } +} + +func (c *vendorController) Create(ctx *gin.Context) { + var req dto.VendorCreateRequest + 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.vendorService.Create(ctx, req) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_VENDOR, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_CREATE_VENDOR, created) + ctx.JSON(http.StatusOK, res) +} + +func (c *vendorController) Update(ctx *gin.Context) { + id := ctx.Param("id") + var req dto.VendorUpdateRequest + 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.vendorService.Update(ctx, req, id) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_UPDATE_VENDOR, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_UPDATE_VENDOR, updated) + ctx.JSON(http.StatusOK, res) +} + +func (c *vendorController) Delete(ctx *gin.Context) { + id := ctx.Param("id") + if err := c.vendorService.Delete(ctx, id); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_DELETE_VENDOR, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_DELETE_VENDOR, nil) + ctx.JSON(http.StatusOK, res) +} + +func (c *vendorController) GetById(ctx *gin.Context) { + id := ctx.Param("id") + vendor, err := c.vendorService.GetById(ctx, id) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_VENDOR, err.Error(), nil) + ctx.JSON(http.StatusNotFound, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_VENDOR, vendor) + ctx.JSON(http.StatusOK, res) +} + +func (c *vendorController) GetAll(ctx *gin.Context) { + var filter query.VendorFilter + if err := ctx.ShouldBindQuery(&filter); err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_VENDOR, 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 + vendors, total, err := c.vendorService.GetAll(ctx, filter) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_VENDOR, err.Error(), nil) + ctx.JSON(http.StatusBadRequest, res) + return + } + paginationResponse := utils.BuildPaginationResponse(perPage, page, total) + response := utils.BuildResponseSuccessWithPagination(http.StatusOK, dto.MESSAGE_SUCCESS_GET_VENDOR, vendors, paginationResponse) + ctx.JSON(http.StatusOK, response) +} diff --git a/modules/mvendor/dto/vendor_dto.go b/modules/mvendor/dto/vendor_dto.go new file mode 100644 index 0000000..a68e11e --- /dev/null +++ b/modules/mvendor/dto/vendor_dto.go @@ -0,0 +1,54 @@ +package dto + +import ( + "errors" + + pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" +) + +const ( + MESSAGE_FAILED_CREATE_VENDOR = "failed create vendor" + MESSAGE_SUCCESS_CREATE_VENDOR = "success create vendor" + MESSAGE_FAILED_GET_VENDOR = "failed get vendor" + MESSAGE_SUCCESS_GET_VENDOR = "success get vendor" + MESSAGE_FAILED_UPDATE_VENDOR = "failed update vendor" + MESSAGE_SUCCESS_UPDATE_VENDOR = "success update vendor" + MESSAGE_FAILED_DELETE_VENDOR = "failed delete vendor" + MESSAGE_SUCCESS_DELETE_VENDOR = "success delete vendor" + MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body" +) + +var ( + ErrCreateVendor = errors.New("failed to create vendor") + ErrGetVendorById = errors.New("failed to get vendor by id") + ErrUpdateVendor = errors.New("failed to update vendor") + ErrDeleteVendor = errors.New("failed to delete vendor") +) + +type VendorCreateRequest struct { + SearchKey string `json:"search_key"` + Name string `json:"name" binding:"required"` + Address string `json:"address"` + ContactPerson string `json:"contact_person"` + IsActive bool `json:"is_active"` + ClientID string `json:"client_id" binding:"required"` +} + +type VendorUpdateRequest struct { + SearchKey string `json:"search_key"` + Name string `json:"name"` + Address string `json:"address"` + ContactPerson string `json:"contact_person"` + IsActive bool `json:"is_active"` + ClientID string `json:"client_id"` +} + +type VendorResponse struct { + ID string `json:"id"` + SearchKey string `json:"search_key"` + Name string `json:"name"` + Address string `json:"address"` + ContactPerson string `json:"contact_person"` + IsActive bool `json:"is_active"` + Client pkgdto.IdNameResponse `json:"client"` +} diff --git a/modules/mvendor/query/vendor_query.go b/modules/mvendor/query/vendor_query.go new file mode 100644 index 0000000..522fd91 --- /dev/null +++ b/modules/mvendor/query/vendor_query.go @@ -0,0 +1,24 @@ +package query + +import "gorm.io/gorm" + +type VendorFilter struct { + Name string `form:"name"` + IsActive *bool `form:"is_active"` + ClientID string `form:"client_id"` + PerPage int `form:"per_page"` + Page int `form:"page"` +} + +func ApplyVendorFilters(db *gorm.DB, filter VendorFilter) *gorm.DB { + if filter.Name != "" { + db = db.Where("name ILIKE ?", "%"+filter.Name+"%") + } + 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/mvendor/repository/vendor_repository.go b/modules/mvendor/repository/vendor_repository.go new file mode 100644 index 0000000..98140fc --- /dev/null +++ b/modules/mvendor/repository/vendor_repository.go @@ -0,0 +1,82 @@ +package repository + +import ( + "context" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + "github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/query" + "gorm.io/gorm" +) + +type VendorRepository interface { + Create(ctx context.Context, tx *gorm.DB, vendor entities.MVendorEntity) (entities.MVendorEntity, error) + GetById(ctx context.Context, tx *gorm.DB, vendorId string) (entities.MVendorEntity, error) + GetAll(ctx context.Context, filter query.VendorFilter) ([]entities.MVendorEntity, int64, error) + Update(ctx context.Context, tx *gorm.DB, vendor entities.MVendorEntity) (entities.MVendorEntity, error) + Delete(ctx context.Context, tx *gorm.DB, vendorId string) error +} + +type vendorRepository struct { + db *gorm.DB +} + +func NewVendorRepository(db *gorm.DB) VendorRepository { + return &vendorRepository{db: db} +} + +func (r *vendorRepository) Create(ctx context.Context, tx *gorm.DB, vendor entities.MVendorEntity) (entities.MVendorEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Create(&vendor).Error; err != nil { + return vendor, err + } + return vendor, nil +} + +func (r *vendorRepository) GetById(ctx context.Context, tx *gorm.DB, vendorId string) (entities.MVendorEntity, error) { + if tx == nil { + tx = r.db + } + var vendor entities.MVendorEntity + if err := tx.WithContext(ctx).Preload("Client").First(&vendor, "id = ?", vendorId).Error; err != nil { + return vendor, err + } + return vendor, nil +} + +func (r *vendorRepository) GetAll(ctx context.Context, filter query.VendorFilter) ([]entities.MVendorEntity, int64, error) { + var vendors []entities.MVendorEntity + var total int64 + db := query.ApplyVendorFilters(r.db, filter) + db.Model(&entities.MVendorEntity{}).Count(&total) + if filter.PerPage > 0 && filter.Page > 0 { + db = db.Limit(filter.PerPage).Offset((filter.Page - 1) * filter.PerPage) + } + if err := db. + Preload("Client"). + Find(&vendors).Error; err != nil { + return vendors, total, err + } + return vendors, total, nil +} + +func (r *vendorRepository) Update(ctx context.Context, tx *gorm.DB, vendor entities.MVendorEntity) (entities.MVendorEntity, error) { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Save(&vendor).Error; err != nil { + return vendor, err + } + return vendor, nil +} + +func (r *vendorRepository) Delete(ctx context.Context, tx *gorm.DB, vendorId string) error { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Delete(&entities.MVendorEntity{}, "id = ?", vendorId).Error; err != nil { + return err + } + return nil +} diff --git a/modules/mvendor/routes.go b/modules/mvendor/routes.go new file mode 100644 index 0000000..7edc724 --- /dev/null +++ b/modules/mvendor/routes.go @@ -0,0 +1,24 @@ +package mvendor + +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/mvendor/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) { + vendorController := do.MustInvoke[controller.VendorController](injector) + jwtService := do.MustInvokeNamed[service.JWTService](injector, constants.JWTService) + + vendorRoutes := server.Group("/api/v1/vendors") + { + vendorRoutes.POST("", middlewares.Authenticate(jwtService), vendorController.Create) + vendorRoutes.GET("/:id", middlewares.Authenticate(jwtService), vendorController.GetById) + vendorRoutes.PUT("/:id", middlewares.Authenticate(jwtService), vendorController.Update) + vendorRoutes.DELETE("/:id", middlewares.Authenticate(jwtService), vendorController.Delete) + vendorRoutes.GET("", middlewares.Authenticate(jwtService), vendorController.GetAll) + } +} diff --git a/modules/mvendor/service/vendor_service.go b/modules/mvendor/service/vendor_service.go new file mode 100644 index 0000000..323938e --- /dev/null +++ b/modules/mvendor/service/vendor_service.go @@ -0,0 +1,155 @@ +package service + +import ( + "context" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + "github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/dto" + "github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/query" + "github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/repository" + pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" + "github.com/google/uuid" + "gorm.io/gorm" +) + +type VendorService interface { + Create(ctx context.Context, req dto.VendorCreateRequest) (dto.VendorResponse, error) + GetById(ctx context.Context, vendorId string) (dto.VendorResponse, error) + GetAll(ctx context.Context, filter query.VendorFilter) ([]dto.VendorResponse, int64, error) + Update(ctx context.Context, req dto.VendorUpdateRequest, vendorId string) (dto.VendorResponse, error) + Delete(ctx context.Context, vendorId string) error +} + +type vendorService struct { + db *gorm.DB + vendorRepo repository.VendorRepository +} + +func NewVendorService(vendorRepo repository.VendorRepository, db *gorm.DB) VendorService { + return &vendorService{ + vendorRepo: vendorRepo, + db: db, + } +} + +func (s *vendorService) Create(ctx context.Context, req dto.VendorCreateRequest) (dto.VendorResponse, error) { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + vendor := entities.MVendorEntity{ + SearchKey: req.SearchKey, + Name: req.Name, + Address: req.Address, + ContactPerson: req.ContactPerson, + IsActive: req.IsActive, + } + clientUUID, err := uuid.Parse(req.ClientID) + if err != nil { + tx.Rollback() + return dto.VendorResponse{}, err + } + vendor.ClientID = clientUUID + created, err := s.vendorRepo.Create(ctx, tx, vendor) + if err != nil { + tx.Rollback() + return dto.VendorResponse{}, err + } + tx.Commit() + return entityToVendorResponse(created), nil +} + +func (s *vendorService) GetById(ctx context.Context, vendorId string) (dto.VendorResponse, error) { + vendor, err := s.vendorRepo.GetById(ctx, nil, vendorId) + if err != nil { + return dto.VendorResponse{}, err + } + return entityToVendorResponse(vendor), nil +} + +func (s *vendorService) GetAll(ctx context.Context, filter query.VendorFilter) ([]dto.VendorResponse, int64, error) { + vendors, total, err := s.vendorRepo.GetAll(ctx, filter) + if err != nil { + return nil, 0, err + } + responses := make([]dto.VendorResponse, len(vendors)) + for i, v := range vendors { + responses[i] = entityToVendorResponse(v) + } + return responses, total, nil +} + +func (s *vendorService) Update(ctx context.Context, req dto.VendorUpdateRequest, vendorId string) (dto.VendorResponse, error) { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + vendor, err := s.vendorRepo.GetById(ctx, tx, vendorId) + if err != nil { + tx.Rollback() + return dto.VendorResponse{}, err + } + if req.SearchKey != "" { + vendor.SearchKey = req.SearchKey + } + if req.Name != "" { + vendor.Name = req.Name + } + if req.Address != "" { + vendor.Address = req.Address + } + if req.ContactPerson != "" { + vendor.ContactPerson = req.ContactPerson + } + vendor.IsActive = req.IsActive + if req.ClientID != "" { + clientUUID, err := uuid.Parse(req.ClientID) + if err != nil { + tx.Rollback() + return dto.VendorResponse{}, err + } + vendor.ClientID = clientUUID + } + updated, err := s.vendorRepo.Update(ctx, tx, vendor) + if err != nil { + tx.Rollback() + return dto.VendorResponse{}, err + } + tx.Commit() + return entityToVendorResponse(updated), nil +} + +func (s *vendorService) Delete(ctx context.Context, vendorId string) error { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + if err := s.vendorRepo.Delete(ctx, tx, vendorId); err != nil { + tx.Rollback() + return err + } + tx.Commit() + return nil +} + +func entityToVendorResponse(e entities.MVendorEntity) dto.VendorResponse { + + return dto.VendorResponse{ + ID: e.ID.String(), + SearchKey: e.SearchKey, + Name: e.Name, + Address: e.Address, + ContactPerson: e.ContactPerson, + IsActive: e.IsActive, + Client: pkgdto.IdNameResponse{ + ID: e.ClientID.String(), + Name: e.Client.Name, + }, + } +}