feat(vendor): Implement Vendor CRUD operations with DTOs, service, repository, and routes

This commit is contained in:
Habib Fatkhul Rohman 2025-11-05 14:53:23 +07:00
parent e1440e317c
commit 371c2f8e16
6 changed files with 454 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

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

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

View File

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