feat(product): Add AssignCrossReference and RemoveCrossReference methods in ProductController, Service, and Repository

This commit is contained in:
Habib Fatkhul Rohman 2025-11-05 14:52:26 +07:00
parent e0a5af65a1
commit e1440e317c
5 changed files with 210 additions and 33 deletions

View File

@ -20,6 +20,8 @@ type (
Delete(ctx *gin.Context) Delete(ctx *gin.Context)
GetById(ctx *gin.Context) GetById(ctx *gin.Context)
GetAll(ctx *gin.Context) GetAll(ctx *gin.Context)
AssignCrossReference(ctx *gin.Context)
RemoveCrossReference(ctx *gin.Context)
} }
productController struct { productController struct {
@ -115,3 +117,39 @@ func (c *productController) GetAll(ctx *gin.Context) {
response := utils.BuildResponseSuccessWithPagination(http.StatusOK, dto.MESSAGE_SUCCESS_GET_PRODUCT, products, paginationResponse) response := utils.BuildResponseSuccessWithPagination(http.StatusOK, dto.MESSAGE_SUCCESS_GET_PRODUCT, products, paginationResponse)
ctx.JSON(http.StatusOK, response) ctx.JSON(http.StatusOK, response)
} }
// AssignCrossReference implements ProductController.
func (c *productController) AssignCrossReference(ctx *gin.Context) {
id := ctx.Param("id")
var req dto.CrossReferenceRequest
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
}
if err := c.productService.AssignCrossReference(ctx, id, req.VendorIDs); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_ASSIGN_CROSS_REF, dto.ErrAssignCrossRef.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_ASSIGN_CROSS_REF, nil)
ctx.JSON(http.StatusOK, res)
}
// RemoveCrossReference implements ProductController.
func (c *productController) RemoveCrossReference(ctx *gin.Context) {
id := ctx.Param("id")
var req dto.CrossReferenceRequest
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
}
if err := c.productService.RemoveCrossReference(ctx, id, req.VendorIDs); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_REMOVE_CROSS_REF, dto.ErrRemoveCrossRef.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_REMOVE_CROSS_REF, nil)
ctx.JSON(http.StatusOK, res)
}

View File

@ -16,6 +16,10 @@ const (
MESSAGE_FAILED_DELETE_PRODUCT = "failed delete product" MESSAGE_FAILED_DELETE_PRODUCT = "failed delete product"
MESSAGE_SUCCESS_DELETE_PRODUCT = "success delete product" MESSAGE_SUCCESS_DELETE_PRODUCT = "success delete product"
MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body" MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body"
MESSAGE_FAILED_ASSIGN_CROSS_REF = "failed assign cross reference"
MESSAGE_SUCCESS_ASSIGN_CROSS_REF = "success assign cross reference"
MESSAGE_FAILED_REMOVE_CROSS_REF = "failed remove cross reference"
MESSAGE_SUCCESS_REMOVE_CROSS_REF = "success remove cross reference"
) )
var ( var (
@ -23,6 +27,8 @@ var (
ErrGetProductById = errors.New("failed to get product by id") ErrGetProductById = errors.New("failed to get product by id")
ErrUpdateProduct = errors.New("failed to update product") ErrUpdateProduct = errors.New("failed to update product")
ErrDeleteProduct = errors.New("failed to delete product") ErrDeleteProduct = errors.New("failed to delete product")
ErrAssignCrossRef = errors.New("failed to assign cross reference")
ErrRemoveCrossRef = errors.New("failed to remove cross reference")
) )
type ( type (
@ -97,38 +103,50 @@ type (
} }
ProductResponse struct { ProductResponse struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
RefNumber string `json:"ref_number"` RefNumber string `json:"ref_number"`
SKU string `json:"sku"` SKU string `json:"sku"`
Description string `json:"description"` Description string `json:"description"`
Status string `json:"status"` Status string `json:"status"`
IsReturnable bool `json:"is_returnable"` IsReturnable bool `json:"is_returnable"`
DimLength float64 `json:"dim_length"` DimLength float64 `json:"dim_length"`
DimWidth float64 `json:"dim_width"` DimWidth float64 `json:"dim_width"`
DimHeight float64 `json:"dim_height"` DimHeight float64 `json:"dim_height"`
Weight float64 `json:"weight"` Weight float64 `json:"weight"`
Volume float64 `json:"volume"` Volume float64 `json:"volume"`
MaxStackHeight int `json:"max_stack_height"` MaxStackHeight int `json:"max_stack_height"`
Temperature string `json:"temperature"` Temperature string `json:"temperature"`
IsHazardous bool `json:"is_hazardous"` IsHazardous bool `json:"is_hazardous"`
MinStock int `json:"min_stock"` MinStock int `json:"min_stock"`
MaxStock int `json:"max_stock"` MaxStock int `json:"max_stock"`
ReplenishType string `json:"replenish_type"` ReplenishType string `json:"replenish_type"`
CycleCount string `json:"cycle_count"` CycleCount string `json:"cycle_count"`
LotRules string `json:"lot_rules"` LotRules string `json:"lot_rules"`
LeadTime int `json:"lead_time"` LeadTime int `json:"lead_time"`
MultiplyRate string `json:"multiply_rate"` MultiplyRate string `json:"multiply_rate"`
DivideRate float64 `json:"divide_rate"` DivideRate float64 `json:"divide_rate"`
Client pkgdto.IdNameResponse `json:"client"` Client pkgdto.IdNameResponse `json:"client"`
Category pkgdto.IdNameResponse `json:"category"` Category pkgdto.IdNameResponse `json:"category"`
Uom pkgdto.IdNameResponse `json:"uom"` Uom pkgdto.IdNameResponse `json:"uom"`
DimUom pkgdto.IdNameResponse `json:"dim_uom"` DimUom pkgdto.IdNameResponse `json:"dim_uom"`
WeightUom pkgdto.IdNameResponse `json:"weight_uom"` WeightUom pkgdto.IdNameResponse `json:"weight_uom"`
VolumeUom pkgdto.IdNameResponse `json:"volume_uom"` VolumeUom pkgdto.IdNameResponse `json:"volume_uom"`
MinStockUom pkgdto.IdNameResponse `json:"min_stock_uom"` MinStockUom pkgdto.IdNameResponse `json:"min_stock_uom"`
MaxStockUom pkgdto.IdNameResponse `json:"max_stock_uom"` MaxStockUom pkgdto.IdNameResponse `json:"max_stock_uom"`
LeadTimeUom pkgdto.IdNameResponse `json:"lead_time_uom"` LeadTimeUom pkgdto.IdNameResponse `json:"lead_time_uom"`
UomToUom pkgdto.IdNameResponse `json:"uom_to_uom"` UomToUom pkgdto.IdNameResponse `json:"uom_to_uom"`
CrossReferences []ProductVendorResponse `json:"cross_references"`
}
CrossReferenceRequest struct {
VendorIDs []string `json:"vendor_ids" binding:"required"`
}
ProductVendorResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
ContactPerson string `json:"contact_person"`
} }
) )

View File

@ -5,7 +5,9 @@ import (
"github.com/Caknoooo/go-gin-clean-starter/database/entities" "github.com/Caknoooo/go-gin-clean-starter/database/entities"
"github.com/Caknoooo/go-gin-clean-starter/modules/product/query" "github.com/Caknoooo/go-gin-clean-starter/modules/product/query"
"github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
type ProductRepository interface { type ProductRepository interface {
@ -14,12 +16,74 @@ type ProductRepository interface {
GetAll(ctx context.Context, filter query.ProductFilter) ([]entities.MProductEntity, int64, error) GetAll(ctx context.Context, filter query.ProductFilter) ([]entities.MProductEntity, int64, error)
Update(ctx context.Context, tx *gorm.DB, product entities.MProductEntity) (entities.MProductEntity, error) Update(ctx context.Context, tx *gorm.DB, product entities.MProductEntity) (entities.MProductEntity, error)
Delete(ctx context.Context, tx *gorm.DB, productId string) error Delete(ctx context.Context, tx *gorm.DB, productId string) error
AssignCrossReference(ctx context.Context, tx *gorm.DB, productId string, vendorIds []string) error
RemoveCrossReference(ctx context.Context, tx *gorm.DB, productId string, vendorIds []string) error
} }
type productRepository struct { type productRepository struct {
db *gorm.DB db *gorm.DB
} }
func (r *productRepository) AssignCrossReference(ctx context.Context, tx *gorm.DB, productId string, vendorIds []string) error {
if tx == nil {
tx = r.db
}
productUUID, err := uuid.Parse(productId)
if err != nil {
return err
}
var crossRefs []entities.MCrossReferenceEntity
for _, vendorId := range vendorIds {
vendorUUID, err := uuid.Parse(vendorId)
if err != nil {
return err
}
crossRefs = append(crossRefs, entities.MCrossReferenceEntity{
ProductID: productUUID,
VendorID: vendorUUID,
})
}
if err := tx.WithContext(ctx).
Model(&entities.MCrossReferenceEntity{}).
Clauses(clause.OnConflict{DoNothing: true}).
Create(&crossRefs).Error; err != nil {
return err
}
return nil
}
func (r *productRepository) RemoveCrossReference(ctx context.Context, tx *gorm.DB, productId string, vendorIds []string) error {
if tx == nil {
tx = r.db
}
productUUID, err := uuid.Parse(productId)
if err != nil {
return err
}
var vendorUUIDs []uuid.UUID
for _, vendorId := range vendorIds {
vendorUUID, err := uuid.Parse(vendorId)
if err != nil {
return err
}
vendorUUIDs = append(vendorUUIDs, vendorUUID)
}
if err := tx.WithContext(ctx).
Where("product_id = ? AND vendor_id IN ?", productUUID, vendorUUIDs).
Delete(&entities.MCrossReferenceEntity{}).Error; err != nil {
return err
}
return nil
}
func NewProductRepository(db *gorm.DB) ProductRepository { func NewProductRepository(db *gorm.DB) ProductRepository {
return &productRepository{db: db} return &productRepository{db: db}
} }
@ -50,6 +114,8 @@ func (r *productRepository) GetById(ctx context.Context, tx *gorm.DB, productId
Preload("MaxStockUom"). Preload("MaxStockUom").
Preload("LeadTimeUom"). Preload("LeadTimeUom").
Preload("UomToUom"). Preload("UomToUom").
Preload("CrossReferences").
Preload("CrossReferences.Vendor").
First(&product, "id = ?", productId).Error; err != nil { First(&product, "id = ?", productId).Error; err != nil {
return product, err return product, err
} }

View File

@ -20,5 +20,7 @@ func RegisterRoutes(server *gin.Engine, injector *do.Injector) {
productRoutes.PUT("/:id", middlewares.Authenticate(jwtService), productController.Update) productRoutes.PUT("/:id", middlewares.Authenticate(jwtService), productController.Update)
productRoutes.DELETE("/:id", middlewares.Authenticate(jwtService), productController.Delete) productRoutes.DELETE("/:id", middlewares.Authenticate(jwtService), productController.Delete)
productRoutes.GET("", middlewares.Authenticate(jwtService), productController.GetAll) productRoutes.GET("", middlewares.Authenticate(jwtService), productController.GetAll)
productRoutes.POST("/:id/assign-cross-reference", middlewares.Authenticate(jwtService), productController.AssignCrossReference)
productRoutes.POST("/:id/remove-cross-reference", middlewares.Authenticate(jwtService), productController.RemoveCrossReference)
} }
} }

View File

@ -18,6 +18,8 @@ type ProductService interface {
GetAll(ctx context.Context, filter query.ProductFilter) ([]dto.ProductResponse, int64, error) GetAll(ctx context.Context, filter query.ProductFilter) ([]dto.ProductResponse, int64, error)
Update(ctx context.Context, req dto.ProductUpdateRequest, productId string) (dto.ProductResponse, error) Update(ctx context.Context, req dto.ProductUpdateRequest, productId string) (dto.ProductResponse, error)
Delete(ctx context.Context, productId string) error Delete(ctx context.Context, productId string) error
AssignCrossReference(ctx context.Context, productId string, vendorIds []string) error
RemoveCrossReference(ctx context.Context, productId string, vendorIds []string) error
} }
type productService struct { type productService struct {
@ -25,6 +27,46 @@ type productService struct {
productRepo repository.ProductRepository productRepo repository.ProductRepository
} }
// AssignCrossReference implements ProductService.
func (s *productService) AssignCrossReference(ctx context.Context, productId string, vendorIds []string) error {
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
err := s.productRepo.AssignCrossReference(ctx, tx, productId, vendorIds)
if err != nil {
tx.Rollback()
return err
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
return err
}
return nil
}
// RemoveCrossReference implements ProductService.
func (s *productService) RemoveCrossReference(ctx context.Context, productId string, vendorIds []string) error {
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
err := s.productRepo.RemoveCrossReference(ctx, tx, productId, vendorIds)
if err != nil {
tx.Rollback()
return err
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
return err
}
return nil
}
func NewProductService(productRepo repository.ProductRepository, db *gorm.DB) ProductService { func NewProductService(productRepo repository.ProductRepository, db *gorm.DB) ProductService {
return &productService{ return &productService{
productRepo: productRepo, productRepo: productRepo,
@ -263,6 +305,16 @@ func parseUUID(id string) uuid.UUID {
} }
func mapProductToResponse(product entities.MProductEntity) dto.ProductResponse { func mapProductToResponse(product entities.MProductEntity) dto.ProductResponse {
crossRefs := make([]dto.ProductVendorResponse, 0, len(product.CrossReferences))
for _, v := range product.CrossReferences {
crossRefs = append(crossRefs, dto.ProductVendorResponse{
ID: v.Vendor.ID.String(),
Name: v.Vendor.Name,
Address: v.Vendor.Address,
ContactPerson: v.Vendor.ContactPerson,
})
}
return dto.ProductResponse{ return dto.ProductResponse{
ID: product.ID.String(), ID: product.ID.String(),
Name: product.Name, Name: product.Name,
@ -327,5 +379,6 @@ func mapProductToResponse(product entities.MProductEntity) dto.ProductResponse {
ID: product.UomToUom.ID.String(), ID: product.UomToUom.ID.String(),
Name: product.UomToUom.Name, Name: product.UomToUom.Name,
}, },
CrossReferences: crossRefs,
} }
} }