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)
GetById(ctx *gin.Context)
GetAll(ctx *gin.Context)
AssignCrossReference(ctx *gin.Context)
RemoveCrossReference(ctx *gin.Context)
}
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)
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_SUCCESS_DELETE_PRODUCT = "success delete product"
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 (
@ -23,6 +27,8 @@ var (
ErrGetProductById = errors.New("failed to get product by id")
ErrUpdateProduct = errors.New("failed to update 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 (
@ -97,38 +103,50 @@ type (
}
ProductResponse struct {
ID string `json:"id"`
Name string `json:"name"`
RefNumber string `json:"ref_number"`
SKU string `json:"sku"`
Description string `json:"description"`
Status string `json:"status"`
IsReturnable bool `json:"is_returnable"`
DimLength float64 `json:"dim_length"`
DimWidth float64 `json:"dim_width"`
DimHeight float64 `json:"dim_height"`
Weight float64 `json:"weight"`
Volume float64 `json:"volume"`
MaxStackHeight int `json:"max_stack_height"`
Temperature string `json:"temperature"`
IsHazardous bool `json:"is_hazardous"`
MinStock int `json:"min_stock"`
MaxStock int `json:"max_stock"`
ReplenishType string `json:"replenish_type"`
CycleCount string `json:"cycle_count"`
LotRules string `json:"lot_rules"`
LeadTime int `json:"lead_time"`
MultiplyRate string `json:"multiply_rate"`
DivideRate float64 `json:"divide_rate"`
Client pkgdto.IdNameResponse `json:"client"`
Category pkgdto.IdNameResponse `json:"category"`
Uom pkgdto.IdNameResponse `json:"uom"`
DimUom pkgdto.IdNameResponse `json:"dim_uom"`
WeightUom pkgdto.IdNameResponse `json:"weight_uom"`
VolumeUom pkgdto.IdNameResponse `json:"volume_uom"`
MinStockUom pkgdto.IdNameResponse `json:"min_stock_uom"`
MaxStockUom pkgdto.IdNameResponse `json:"max_stock_uom"`
LeadTimeUom pkgdto.IdNameResponse `json:"lead_time_uom"`
UomToUom pkgdto.IdNameResponse `json:"uom_to_uom"`
ID string `json:"id"`
Name string `json:"name"`
RefNumber string `json:"ref_number"`
SKU string `json:"sku"`
Description string `json:"description"`
Status string `json:"status"`
IsReturnable bool `json:"is_returnable"`
DimLength float64 `json:"dim_length"`
DimWidth float64 `json:"dim_width"`
DimHeight float64 `json:"dim_height"`
Weight float64 `json:"weight"`
Volume float64 `json:"volume"`
MaxStackHeight int `json:"max_stack_height"`
Temperature string `json:"temperature"`
IsHazardous bool `json:"is_hazardous"`
MinStock int `json:"min_stock"`
MaxStock int `json:"max_stock"`
ReplenishType string `json:"replenish_type"`
CycleCount string `json:"cycle_count"`
LotRules string `json:"lot_rules"`
LeadTime int `json:"lead_time"`
MultiplyRate string `json:"multiply_rate"`
DivideRate float64 `json:"divide_rate"`
Client pkgdto.IdNameResponse `json:"client"`
Category pkgdto.IdNameResponse `json:"category"`
Uom pkgdto.IdNameResponse `json:"uom"`
DimUom pkgdto.IdNameResponse `json:"dim_uom"`
WeightUom pkgdto.IdNameResponse `json:"weight_uom"`
VolumeUom pkgdto.IdNameResponse `json:"volume_uom"`
MinStockUom pkgdto.IdNameResponse `json:"min_stock_uom"`
MaxStockUom pkgdto.IdNameResponse `json:"max_stock_uom"`
LeadTimeUom pkgdto.IdNameResponse `json:"lead_time_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/modules/product/query"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type ProductRepository interface {
@ -14,12 +16,74 @@ type ProductRepository interface {
GetAll(ctx context.Context, filter query.ProductFilter) ([]entities.MProductEntity, int64, error)
Update(ctx context.Context, tx *gorm.DB, product entities.MProductEntity) (entities.MProductEntity, 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 {
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 {
return &productRepository{db: db}
}
@ -50,6 +114,8 @@ func (r *productRepository) GetById(ctx context.Context, tx *gorm.DB, productId
Preload("MaxStockUom").
Preload("LeadTimeUom").
Preload("UomToUom").
Preload("CrossReferences").
Preload("CrossReferences.Vendor").
First(&product, "id = ?", productId).Error; err != nil {
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.DELETE("/:id", middlewares.Authenticate(jwtService), productController.Delete)
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)
Update(ctx context.Context, req dto.ProductUpdateRequest, productId string) (dto.ProductResponse, 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 {
@ -25,6 +27,46 @@ type productService struct {
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 {
return &productService{
productRepo: productRepo,
@ -263,6 +305,16 @@ func parseUUID(id string) uuid.UUID {
}
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{
ID: product.ID.String(),
Name: product.Name,
@ -327,5 +379,6 @@ func mapProductToResponse(product entities.MProductEntity) dto.ProductResponse {
ID: product.UomToUom.ID.String(),
Name: product.UomToUom.Name,
},
CrossReferences: crossRefs,
}
}