From 8fab5d7d913b5a3751172f1e8eae53c3e63c318b Mon Sep 17 00:00:00 2001 From: "Hafizh A. Y" Date: Mon, 12 Jan 2026 14:54:14 +0700 Subject: [PATCH] feat(BE): price-product-supplier --- ...016_add_price_to_product_supplier.down.sql | 6 ++ ...73016_add_price_to_product_supplier.up.sql | 6 ++ internal/entities/nonstock_supplier.go | 1 + internal/entities/product_supplier.go | 1 + .../master/nonstocks/dto/nonstock.dto.go | 27 ++++++--- .../repositories/nonstock.repository.go | 33 +++++++---- .../nonstocks/services/nonstock.service.go | 55 +++++++++++++----- .../validations/nonstock.validation.go | 9 ++- .../master/products/dto/product.dto.go | 29 +++++++--- .../repositories/product.repository.go | 33 +++++++---- .../products/services/product.service.go | 58 +++++++++++++------ .../validations/product.validation.go | 9 ++- .../suppliers/dto/supplier_nonstock.dto.go | 2 + .../suppliers/dto/supplier_product.dto.go | 26 +++++---- 14 files changed, 212 insertions(+), 83 deletions(-) create mode 100644 internal/database/migrations/20260112073016_add_price_to_product_supplier.down.sql create mode 100644 internal/database/migrations/20260112073016_add_price_to_product_supplier.up.sql diff --git a/internal/database/migrations/20260112073016_add_price_to_product_supplier.down.sql b/internal/database/migrations/20260112073016_add_price_to_product_supplier.down.sql new file mode 100644 index 00000000..0acbcbe2 --- /dev/null +++ b/internal/database/migrations/20260112073016_add_price_to_product_supplier.down.sql @@ -0,0 +1,6 @@ +-- Rollback: remove price from supplier relations +ALTER TABLE product_suppliers + DROP COLUMN IF EXISTS price; + +ALTER TABLE nonstock_suppliers + DROP COLUMN IF EXISTS price; diff --git a/internal/database/migrations/20260112073016_add_price_to_product_supplier.up.sql b/internal/database/migrations/20260112073016_add_price_to_product_supplier.up.sql new file mode 100644 index 00000000..b96abf64 --- /dev/null +++ b/internal/database/migrations/20260112073016_add_price_to_product_supplier.up.sql @@ -0,0 +1,6 @@ +-- Migration: add price to supplier relations +ALTER TABLE product_suppliers + ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0; + +ALTER TABLE nonstock_suppliers + ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0; diff --git a/internal/entities/nonstock_supplier.go b/internal/entities/nonstock_supplier.go index 2206390c..d666e3c8 100644 --- a/internal/entities/nonstock_supplier.go +++ b/internal/entities/nonstock_supplier.go @@ -5,6 +5,7 @@ import "time" type NonstockSupplier struct { NonstockId uint `gorm:"not null"` SupplierId uint `gorm:"not null"` + Price float64 `gorm:"type:numeric(15,3);not null;default:0"` CreatedAt time.Time `gorm:"autoCreateTime"` Nonstock Nonstock `gorm:"foreignKey:NonstockId;references:Id"` diff --git a/internal/entities/product_supplier.go b/internal/entities/product_supplier.go index d64b1e85..9b9aa67c 100644 --- a/internal/entities/product_supplier.go +++ b/internal/entities/product_supplier.go @@ -5,6 +5,7 @@ import "time" type ProductSupplier struct { ProductId uint `gorm:"not null"` SupplierId uint `gorm:"not null"` + Price float64 `gorm:"type:numeric(15,3);not null;default:0"` CreatedAt time.Time `gorm:"autoCreateTime"` Product Product `gorm:"foreignKey:ProductId;references:Id"` diff --git a/internal/modules/master/nonstocks/dto/nonstock.dto.go b/internal/modules/master/nonstocks/dto/nonstock.dto.go index 9954ee76..fa102b9c 100644 --- a/internal/modules/master/nonstocks/dto/nonstock.dto.go +++ b/internal/modules/master/nonstocks/dto/nonstock.dto.go @@ -4,7 +4,6 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) @@ -23,7 +22,7 @@ type NonstockListDTO struct { Name string `json:"name"` Flags []string `json:"flags"` Uom *uomDTO.UomRelationDTO `json:"uom"` - Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"` + Suppliers []NonstockSupplierDTO `json:"suppliers"` CreatedUser *userDTO.UserRelationDTO `json:"created_user"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -33,6 +32,14 @@ type NonstockDetailDTO struct { NonstockListDTO } +type NonstockSupplierDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Alias string `json:"alias"` + Category string `json:"category"` + Price float64 `json:"price"` +} + // === Mapper Functions === func ToNonstockRelationDTO(e entity.Nonstock) NonstockRelationDTO { @@ -99,21 +106,27 @@ func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO { } } -func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO { +func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []NonstockSupplierDTO { if len(relations) == 0 { - return make([]supplierDTO.SupplierRelationDTO, 0) + return make([]NonstockSupplierDTO, 0) } - result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations)) + result := make([]NonstockSupplierDTO, 0, len(relations)) for _, relation := range relations { if relation.Supplier.Id == 0 { continue } - result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier)) + result = append(result, NonstockSupplierDTO{ + Id: relation.Supplier.Id, + Name: relation.Supplier.Name, + Alias: relation.Supplier.Alias, + Category: relation.Supplier.Category, + Price: relation.Price, + }) } if len(result) == 0 { - return make([]supplierDTO.SupplierRelationDTO, 0) + return make([]NonstockSupplierDTO, 0) } return result diff --git a/internal/modules/master/nonstocks/repositories/nonstock.repository.go b/internal/modules/master/nonstocks/repositories/nonstock.repository.go index aeff162f..16260272 100644 --- a/internal/modules/master/nonstocks/repositories/nonstock.repository.go +++ b/internal/modules/master/nonstocks/repositories/nonstock.repository.go @@ -12,7 +12,7 @@ import ( type NonstockRepository interface { repository.BaseRepository[entity.Nonstock] NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) - SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error + SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, suppliers []entity.NonstockSupplier) error UomExists(ctx context.Context, uomID uint) (bool, error) GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error) SyncFlags(ctx context.Context, tx *gorm.DB, nonstockID uint, flags []string) error @@ -40,13 +40,13 @@ func (r *NonstockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, e return repository.Exists[entity.Nonstock](ctx, r.DB(), id) } -func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error { +func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, suppliers []entity.NonstockSupplier) error { db := tx if db == nil { db = r.DB() } - if supplierIDs == nil { + if suppliers == nil { return db.WithContext(ctx). Where("nonstock_id = ?", nonstockID). Delete(&entity.NonstockSupplier{}). @@ -61,18 +61,31 @@ func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm return err } - existingMap := make(map[uint]struct{}, len(existing)) + existingMap := make(map[uint]entity.NonstockSupplier, len(existing)) for _, rel := range existing { - existingMap[rel.SupplierId] = struct{}{} + existingMap[rel.SupplierId] = rel } - incomingMap := make(map[uint]struct{}, len(supplierIDs)) - for _, id := range supplierIDs { - incomingMap[id] = struct{}{} - if _, exists := existingMap[id]; exists { + incomingMap := make(map[uint]struct{}, len(suppliers)) + for _, rel := range suppliers { + incomingMap[rel.SupplierId] = struct{}{} + if existingRel, exists := existingMap[rel.SupplierId]; exists { + if existingRel.Price != rel.Price { + if err := db.WithContext(ctx). + Model(&entity.NonstockSupplier{}). + Where("nonstock_id = ? AND supplier_id = ?", nonstockID, rel.SupplierId). + Update("price", rel.Price). + Error; err != nil { + return err + } + } continue } - record := entity.NonstockSupplier{NonstockId: nonstockID, SupplierId: id} + record := entity.NonstockSupplier{ + NonstockId: nonstockID, + SupplierId: rel.SupplierId, + Price: rel.Price, + } if err := db.WithContext(ctx).Create(&record).Error; err != nil { return err } diff --git a/internal/modules/master/nonstocks/services/nonstock.service.go b/internal/modules/master/nonstocks/services/nonstock.service.go index ad044b08..e1cc5495 100644 --- a/internal/modules/master/nonstocks/services/nonstock.service.go +++ b/internal/modules/master/nonstocks/services/nonstock.service.go @@ -111,8 +111,25 @@ func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti return nil, err } - supplierIDs := utils.UniqueUintSlice(req.SupplierIDs) - if len(supplierIDs) > 0 { + var ( + supplierLinks []entity.NonstockSupplier + supplierIDs []uint + ) + if len(req.Suppliers) > 0 { + seen := make(map[uint]struct{}, len(req.Suppliers)) + supplierLinks = make([]entity.NonstockSupplier, 0, len(req.Suppliers)) + supplierIDs = make([]uint, 0, len(req.Suppliers)) + for _, supplier := range req.Suppliers { + if _, exists := seen[supplier.SupplierID]; exists { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID)) + } + seen[supplier.SupplierID] = struct{}{} + supplierIDs = append(supplierIDs, supplier.SupplierID) + supplierLinks = append(supplierLinks, entity.NonstockSupplier{ + SupplierId: supplier.SupplierID, + Price: supplier.Price, + }) + } supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs) if supplierErr != nil { s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr) @@ -155,7 +172,7 @@ func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti return err } - return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs) + return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierLinks) }) if err != nil { @@ -193,15 +210,27 @@ func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint updateBody["uom_id"] = *req.UomID } - var supplierIDs []uint + var supplierLinks []entity.NonstockSupplier var supplierUpdate bool - if req.SupplierIDs != nil { + if req.Suppliers != nil { supplierUpdate = true - supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs) - if len(supplierIDs) > 0 { - var supplierList []entity.Supplier - var supplierErr error - supplierList, supplierErr = s.Repository.GetSuppliersByIDs(ctx, supplierIDs) + if len(*req.Suppliers) > 0 { + seen := make(map[uint]struct{}, len(*req.Suppliers)) + supplierLinks = make([]entity.NonstockSupplier, 0, len(*req.Suppliers)) + supplierIDs := make([]uint, 0, len(*req.Suppliers)) + for _, supplier := range *req.Suppliers { + if _, exists := seen[supplier.SupplierID]; exists { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID)) + } + seen[supplier.SupplierID] = struct{}{} + supplierIDs = append(supplierIDs, supplier.SupplierID) + supplierLinks = append(supplierLinks, entity.NonstockSupplier{ + SupplierId: supplier.SupplierID, + Price: supplier.Price, + }) + } + + supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs) if supplierErr != nil { s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers") @@ -253,11 +282,7 @@ func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint } if supplierUpdate { - var ids []uint - if len(supplierIDs) > 0 { - ids = supplierIDs - } - if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil { + if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, supplierLinks); err != nil { return err } } diff --git a/internal/modules/master/nonstocks/validations/nonstock.validation.go b/internal/modules/master/nonstocks/validations/nonstock.validation.go index 62a41197..c5491991 100644 --- a/internal/modules/master/nonstocks/validations/nonstock.validation.go +++ b/internal/modules/master/nonstocks/validations/nonstock.validation.go @@ -1,16 +1,21 @@ package validation +type SupplierPrice struct { + SupplierID uint `json:"supplier_id" validate:"required,gt=0"` + Price float64 `json:"price" validate:"required,gte=0"` +} + type Create struct { Name string `json:"name" validate:"required_strict,min=3,max=50"` UomID uint `json:"uom_id" validate:"required,gt=0"` - SupplierIDs []uint `json:"supplier_ids" validate:"dive,gt=0"` + Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"` Flags []string `json:"flags" validate:"dive,max=50"` } type Update struct { Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"` UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"` - SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"` + Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"` Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"` } diff --git a/internal/modules/master/products/dto/product.dto.go b/internal/modules/master/products/dto/product.dto.go index 59f57034..d115ad23 100644 --- a/internal/modules/master/products/dto/product.dto.go +++ b/internal/modules/master/products/dto/product.dto.go @@ -5,7 +5,6 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto" - supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) @@ -20,7 +19,7 @@ type ProductRelationDTO struct { Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Flags *[]string `json:"flags,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` - Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"` + Suppliers []ProductSupplierDTO `json:"suppliers"` } type ProductListDTO struct { @@ -35,7 +34,7 @@ type ProductListDTO struct { Flags []string `json:"flags"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` - Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"` + Suppliers []ProductSupplierDTO `json:"suppliers"` CreatedUser *userDTO.UserRelationDTO `json:"created_user"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -45,6 +44,14 @@ type ProductDetailDTO struct { ProductListDTO } +type ProductSupplierDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Alias string `json:"alias"` + Category string `json:"category"` + Price float64 `json:"price"` +} + // === Mapper Functions === func ToProductRelationDTO(e entity.Product) ProductRelationDTO { @@ -134,21 +141,27 @@ func ToProductDetailDTO(e entity.Product) ProductDetailDTO { } } -func toProductSupplierDTOs(relations []entity.ProductSupplier) []supplierDTO.SupplierRelationDTO { +func toProductSupplierDTOs(relations []entity.ProductSupplier) []ProductSupplierDTO { if len(relations) == 0 { - return make([]supplierDTO.SupplierRelationDTO, 0) + return make([]ProductSupplierDTO, 0) } - result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations)) + result := make([]ProductSupplierDTO, 0, len(relations)) for _, relation := range relations { if relation.Supplier.Id == 0 { continue } - result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier)) + result = append(result, ProductSupplierDTO{ + Id: relation.Supplier.Id, + Name: relation.Supplier.Name, + Alias: relation.Supplier.Alias, + Category: relation.Supplier.Category, + Price: relation.Price, + }) } if len(result) == 0 { - return make([]supplierDTO.SupplierRelationDTO, 0) + return make([]ProductSupplierDTO, 0) } return result diff --git a/internal/modules/master/products/repositories/product.repository.go b/internal/modules/master/products/repositories/product.repository.go index 244259d5..ecef0204 100644 --- a/internal/modules/master/products/repositories/product.repository.go +++ b/internal/modules/master/products/repositories/product.repository.go @@ -17,7 +17,7 @@ type ProductRepository interface { CategoryExists(ctx context.Context, categoryID uint) (bool, error) GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error) IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, error) - SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error + SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, suppliers []entity.ProductSupplier) error SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error GetFlags(ctx context.Context, productID uint) ([]entity.Flag, error) @@ -102,13 +102,13 @@ func (r *ProductRepositoryImpl) IsLinkedToSupplier(ctx context.Context, productI return count > 0, nil } -func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIds []uint) error { +func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, suppliers []entity.ProductSupplier) error { db := tx if db == nil { db = r.DB() } - if supplierIds == nil { + if suppliers == nil { return db.WithContext(ctx). Where("product_id = ?", productID). Delete(&entity.ProductSupplier{}). @@ -123,18 +123,31 @@ func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm. return err } - existingMap := make(map[uint]struct{}, len(existing)) + existingMap := make(map[uint]entity.ProductSupplier, len(existing)) for _, rel := range existing { - existingMap[rel.SupplierId] = struct{}{} + existingMap[rel.SupplierId] = rel } - incomingMap := make(map[uint]struct{}, len(supplierIds)) - for _, id := range supplierIds { - incomingMap[id] = struct{}{} - if _, exists := existingMap[id]; exists { + incomingMap := make(map[uint]struct{}, len(suppliers)) + for _, rel := range suppliers { + incomingMap[rel.SupplierId] = struct{}{} + if existingRel, exists := existingMap[rel.SupplierId]; exists { + if existingRel.Price != rel.Price { + if err := db.WithContext(ctx). + Model(&entity.ProductSupplier{}). + Where("product_id = ? AND supplier_id = ?", productID, rel.SupplierId). + Update("price", rel.Price). + Error; err != nil { + return err + } + } continue } - record := entity.ProductSupplier{ProductId: productID, SupplierId: id} + record := entity.ProductSupplier{ + ProductId: productID, + SupplierId: rel.SupplierId, + Price: rel.Price, + } if err := db.WithContext(ctx).Create(&record).Error; err != nil { return err } diff --git a/internal/modules/master/products/services/product.service.go b/internal/modules/master/products/services/product.service.go index e63b462b..0aaa0952 100644 --- a/internal/modules/master/products/services/product.service.go +++ b/internal/modules/master/products/services/product.service.go @@ -138,9 +138,25 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, err } - supplierIDs := utils.UniqueUintSlice(req.SupplierIDs) - var err error - if len(supplierIDs) > 0 { + var ( + supplierLinks []entity.ProductSupplier + supplierIDs []uint + ) + if len(req.Suppliers) > 0 { + seen := make(map[uint]struct{}, len(req.Suppliers)) + supplierLinks = make([]entity.ProductSupplier, 0, len(req.Suppliers)) + supplierIDs = make([]uint, 0, len(req.Suppliers)) + for _, supplier := range req.Suppliers { + if _, exists := seen[supplier.SupplierID]; exists { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID)) + } + seen[supplier.SupplierID] = struct{}{} + supplierIDs = append(supplierIDs, supplier.SupplierID) + supplierLinks = append(supplierLinks, entity.ProductSupplier{ + SupplierId: supplier.SupplierID, + Price: supplier.Price, + }) + } suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs) if err != nil { s.Log.Errorf("Failed to validate suppliers: %+v", err) @@ -180,7 +196,7 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit CreatedBy: 1, } - err = s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + err := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { repoTx := s.Repository.WithTx(tx) if err := repoTx.CreateOne(ctx, createBody, nil); err != nil { @@ -191,7 +207,7 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return err } - return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs) + return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierLinks) }) if err != nil { @@ -276,15 +292,27 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) ctx := c.Context() - var suppliers []entity.Supplier - var supplierIDs []uint + var supplierLinks []entity.ProductSupplier var supplierUpdate bool - if req.SupplierIDs != nil { + if req.Suppliers != nil { supplierUpdate = true - supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs) - if len(supplierIDs) > 0 { - var err error - suppliers, err = s.Repository.GetSuppliersByIDs(ctx, supplierIDs) + if len(*req.Suppliers) > 0 { + seen := make(map[uint]struct{}, len(*req.Suppliers)) + supplierLinks = make([]entity.ProductSupplier, 0, len(*req.Suppliers)) + supplierIDs := make([]uint, 0, len(*req.Suppliers)) + for _, supplier := range *req.Suppliers { + if _, exists := seen[supplier.SupplierID]; exists { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID)) + } + seen[supplier.SupplierID] = struct{}{} + supplierIDs = append(supplierIDs, supplier.SupplierID) + supplierLinks = append(supplierLinks, entity.ProductSupplier{ + SupplierId: supplier.SupplierID, + Price: supplier.Price, + }) + } + + suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs) if err != nil { s.Log.Errorf("Failed to validate suppliers: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers") @@ -336,11 +364,7 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } if supplierUpdate { - var ids []uint - if len(supplierIDs) > 0 { - ids = supplierIDs - } - if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil { + if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, supplierLinks); err != nil { return err } } diff --git a/internal/modules/master/products/validations/product.validation.go b/internal/modules/master/products/validations/product.validation.go index e732d054..77e8e1bf 100644 --- a/internal/modules/master/products/validations/product.validation.go +++ b/internal/modules/master/products/validations/product.validation.go @@ -1,5 +1,10 @@ package validation +type SupplierPrice struct { + SupplierID uint `json:"supplier_id" validate:"required,gt=0"` + Price float64 `json:"price" validate:"required,gte=0"` +} + type Create struct { Name string `json:"name" validate:"required_strict,min=3,max=50"` Brand string `json:"brand" validate:"required_strict,min=2,max=50"` @@ -10,7 +15,7 @@ type Create struct { SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"` Tax *float64 `json:"tax,omitempty" validate:"omitempty"` ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"` - SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"` + Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"` Flags []string `json:"flags,omitempty" validate:"omitempty,dive"` } @@ -24,7 +29,7 @@ type Update struct { SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"` Tax *float64 `json:"tax,omitempty" validate:"omitempty"` ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"` - SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"` + Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"` Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"` } diff --git a/internal/modules/master/suppliers/dto/supplier_nonstock.dto.go b/internal/modules/master/suppliers/dto/supplier_nonstock.dto.go index 828063eb..8c5e0082 100644 --- a/internal/modules/master/suppliers/dto/supplier_nonstock.dto.go +++ b/internal/modules/master/suppliers/dto/supplier_nonstock.dto.go @@ -10,6 +10,7 @@ import ( type SupplierNonstockDTO struct { Id uint `json:"id"` Name string `json:"name"` + Price float64 `json:"price"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Flags []string `json:"flags"` } @@ -42,6 +43,7 @@ func toSupplierNonstockDTOs(relations []entity.NonstockSupplier) []SupplierNonst result = append(result, SupplierNonstockDTO{ Id: Nonstock.Id, Name: Nonstock.Name, + Price: relation.Price, Uom: uomRef, Flags: flags, }) diff --git a/internal/modules/master/suppliers/dto/supplier_product.dto.go b/internal/modules/master/suppliers/dto/supplier_product.dto.go index 47a6ae0e..a6178aaf 100644 --- a/internal/modules/master/suppliers/dto/supplier_product.dto.go +++ b/internal/modules/master/suppliers/dto/supplier_product.dto.go @@ -8,12 +8,13 @@ import ( // === DTO Structs === type SupplierProductDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - ProductPrice float64 `gorm:"type:numeric(15,3);not null"` - SellingPrice *float64 `gorm:"type:numeric(15,3)"` - Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` - Flags []string `json:"flags"` + Id uint `json:"id"` + Name string `json:"name"` + ProductPrice float64 `gorm:"type:numeric(15,3);not null"` + SellingPrice *float64 `gorm:"type:numeric(15,3)"` + SupplierPrice float64 `json:"supplier_price"` + Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` + Flags []string `json:"flags"` } // === Mapper Functions === @@ -42,12 +43,13 @@ func toSupplierProductDTOs(relations []entity.ProductSupplier) []SupplierProduct } result = append(result, SupplierProductDTO{ - Id: product.Id, - Name: product.Name, - ProductPrice: product.ProductPrice, - SellingPrice: product.SellingPrice, - Uom: uomRef, - Flags: flags, + Id: product.Id, + Name: product.Name, + ProductPrice: product.ProductPrice, + SellingPrice: product.SellingPrice, + SupplierPrice: relation.Price, + Uom: uomRef, + Flags: flags, }) } return result