diff --git a/modules/product/controller/product_controller.go b/modules/product/controller/product_controller.go index 0ef6d68..5168031 100644 --- a/modules/product/controller/product_controller.go +++ b/modules/product/controller/product_controller.go @@ -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) +} diff --git a/modules/product/dto/product_dto.go b/modules/product/dto/product_dto.go index a495680..757c1a8 100644 --- a/modules/product/dto/product_dto.go +++ b/modules/product/dto/product_dto.go @@ -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"` } ) diff --git a/modules/product/repository/product_repository.go b/modules/product/repository/product_repository.go index 74392ac..ba8410b 100644 --- a/modules/product/repository/product_repository.go +++ b/modules/product/repository/product_repository.go @@ -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 } diff --git a/modules/product/routes.go b/modules/product/routes.go index 32b93df..ae6c0bd 100644 --- a/modules/product/routes.go +++ b/modules/product/routes.go @@ -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) } } diff --git a/modules/product/service/product_service.go b/modules/product/service/product_service.go index b247660..e49ed60 100644 --- a/modules/product/service/product_service.go +++ b/modules/product/service/product_service.go @@ -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, } }