feat(BE): price-product-supplier

This commit is contained in:
Hafizh A. Y
2026-01-12 14:54:14 +07:00
parent 5ddfb2c745
commit 8fab5d7d91
14 changed files with 212 additions and 83 deletions
@@ -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;
@@ -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;
+1
View File
@@ -5,6 +5,7 @@ import "time"
type NonstockSupplier struct { type NonstockSupplier struct {
NonstockId uint `gorm:"not null"` NonstockId uint `gorm:"not null"`
SupplierId 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"` CreatedAt time.Time `gorm:"autoCreateTime"`
Nonstock Nonstock `gorm:"foreignKey:NonstockId;references:Id"` Nonstock Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
+1
View File
@@ -5,6 +5,7 @@ import "time"
type ProductSupplier struct { type ProductSupplier struct {
ProductId uint `gorm:"not null"` ProductId uint `gorm:"not null"`
SupplierId 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"` CreatedAt time.Time `gorm:"autoCreateTime"`
Product Product `gorm:"foreignKey:ProductId;references:Id"` Product Product `gorm:"foreignKey:ProductId;references:Id"`
@@ -4,7 +4,6 @@ import (
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" 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" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
@@ -23,7 +22,7 @@ type NonstockListDTO struct {
Name string `json:"name"` Name string `json:"name"`
Flags []string `json:"flags"` Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom"` Uom *uomDTO.UomRelationDTO `json:"uom"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"` Suppliers []NonstockSupplierDTO `json:"suppliers"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"` CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -33,6 +32,14 @@ type NonstockDetailDTO struct {
NonstockListDTO 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 === // === Mapper Functions ===
func ToNonstockRelationDTO(e entity.Nonstock) NonstockRelationDTO { 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 { 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 { for _, relation := range relations {
if relation.Supplier.Id == 0 { if relation.Supplier.Id == 0 {
continue 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 { if len(result) == 0 {
return make([]supplierDTO.SupplierRelationDTO, 0) return make([]NonstockSupplierDTO, 0)
} }
return result return result
@@ -12,7 +12,7 @@ import (
type NonstockRepository interface { type NonstockRepository interface {
repository.BaseRepository[entity.Nonstock] repository.BaseRepository[entity.Nonstock]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) 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) UomExists(ctx context.Context, uomID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error) GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
SyncFlags(ctx context.Context, tx *gorm.DB, nonstockID uint, flags []string) 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) 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 db := tx
if db == nil { if db == nil {
db = r.DB() db = r.DB()
} }
if supplierIDs == nil { if suppliers == nil {
return db.WithContext(ctx). return db.WithContext(ctx).
Where("nonstock_id = ?", nonstockID). Where("nonstock_id = ?", nonstockID).
Delete(&entity.NonstockSupplier{}). Delete(&entity.NonstockSupplier{}).
@@ -61,18 +61,31 @@ func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm
return err return err
} }
existingMap := make(map[uint]struct{}, len(existing)) existingMap := make(map[uint]entity.NonstockSupplier, len(existing))
for _, rel := range existing { for _, rel := range existing {
existingMap[rel.SupplierId] = struct{}{} existingMap[rel.SupplierId] = rel
} }
incomingMap := make(map[uint]struct{}, len(supplierIDs)) incomingMap := make(map[uint]struct{}, len(suppliers))
for _, id := range supplierIDs { for _, rel := range suppliers {
incomingMap[id] = struct{}{} incomingMap[rel.SupplierId] = struct{}{}
if _, exists := existingMap[id]; exists { 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 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 { if err := db.WithContext(ctx).Create(&record).Error; err != nil {
return err return err
} }
@@ -111,8 +111,25 @@ func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
return nil, err return nil, err
} }
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs) var (
if len(supplierIDs) > 0 { 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) supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if supplierErr != nil { if supplierErr != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr) 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 err
} }
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs) return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierLinks)
}) })
if err != nil { if err != nil {
@@ -193,15 +210,27 @@ func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
updateBody["uom_id"] = *req.UomID updateBody["uom_id"] = *req.UomID
} }
var supplierIDs []uint var supplierLinks []entity.NonstockSupplier
var supplierUpdate bool var supplierUpdate bool
if req.SupplierIDs != nil { if req.Suppliers != nil {
supplierUpdate = true supplierUpdate = true
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs) if len(*req.Suppliers) > 0 {
if len(supplierIDs) > 0 { seen := make(map[uint]struct{}, len(*req.Suppliers))
var supplierList []entity.Supplier supplierLinks = make([]entity.NonstockSupplier, 0, len(*req.Suppliers))
var supplierErr error supplierIDs := make([]uint, 0, len(*req.Suppliers))
supplierList, supplierErr = s.Repository.GetSuppliersByIDs(ctx, supplierIDs) 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 { if supplierErr != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr) s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers") 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 { if supplierUpdate {
var ids []uint if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, supplierLinks); err != nil {
if len(supplierIDs) > 0 {
ids = supplierIDs
}
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
return err return err
} }
} }
@@ -1,16 +1,21 @@
package validation 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 { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3,max=50"`
UomID uint `json:"uom_id" validate:"required,gt=0"` 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"` Flags []string `json:"flags" validate:"dive,max=50"`
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"` Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"`
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"` 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"` Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
} }
@@ -5,7 +5,6 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto" 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" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/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"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags *[]string `json:"flags,omitempty"` Flags *[]string `json:"flags,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"` Suppliers []ProductSupplierDTO `json:"suppliers"`
} }
type ProductListDTO struct { type ProductListDTO struct {
@@ -35,7 +34,7 @@ type ProductListDTO struct {
Flags []string `json:"flags"` Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"` Suppliers []ProductSupplierDTO `json:"suppliers"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"` CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -45,6 +44,14 @@ type ProductDetailDTO struct {
ProductListDTO 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 === // === Mapper Functions ===
func ToProductRelationDTO(e entity.Product) ProductRelationDTO { 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 { 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 { for _, relation := range relations {
if relation.Supplier.Id == 0 { if relation.Supplier.Id == 0 {
continue 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 { if len(result) == 0 {
return make([]supplierDTO.SupplierRelationDTO, 0) return make([]ProductSupplierDTO, 0)
} }
return result return result
@@ -17,7 +17,7 @@ type ProductRepository interface {
CategoryExists(ctx context.Context, categoryID uint) (bool, error) CategoryExists(ctx context.Context, categoryID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error) GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, 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 SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error
DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error
GetFlags(ctx context.Context, productID uint) ([]entity.Flag, 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 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 db := tx
if db == nil { if db == nil {
db = r.DB() db = r.DB()
} }
if supplierIds == nil { if suppliers == nil {
return db.WithContext(ctx). return db.WithContext(ctx).
Where("product_id = ?", productID). Where("product_id = ?", productID).
Delete(&entity.ProductSupplier{}). Delete(&entity.ProductSupplier{}).
@@ -123,18 +123,31 @@ func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.
return err return err
} }
existingMap := make(map[uint]struct{}, len(existing)) existingMap := make(map[uint]entity.ProductSupplier, len(existing))
for _, rel := range existing { for _, rel := range existing {
existingMap[rel.SupplierId] = struct{}{} existingMap[rel.SupplierId] = rel
} }
incomingMap := make(map[uint]struct{}, len(supplierIds)) incomingMap := make(map[uint]struct{}, len(suppliers))
for _, id := range supplierIds { for _, rel := range suppliers {
incomingMap[id] = struct{}{} incomingMap[rel.SupplierId] = struct{}{}
if _, exists := existingMap[id]; exists { 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 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 { if err := db.WithContext(ctx).Create(&record).Error; err != nil {
return err return err
} }
@@ -138,9 +138,25 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err return nil, err
} }
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs) var (
var err error supplierLinks []entity.ProductSupplier
if len(supplierIDs) > 0 { 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) suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if err != nil { if err != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", err) 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, 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) repoTx := s.Repository.WithTx(tx)
if err := repoTx.CreateOne(ctx, createBody, nil); err != nil { 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 err
} }
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs) return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierLinks)
}) })
if err != nil { if err != nil {
@@ -276,15 +292,27 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
ctx := c.Context() ctx := c.Context()
var suppliers []entity.Supplier var supplierLinks []entity.ProductSupplier
var supplierIDs []uint
var supplierUpdate bool var supplierUpdate bool
if req.SupplierIDs != nil { if req.Suppliers != nil {
supplierUpdate = true supplierUpdate = true
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs) if len(*req.Suppliers) > 0 {
if len(supplierIDs) > 0 { seen := make(map[uint]struct{}, len(*req.Suppliers))
var err error supplierLinks = make([]entity.ProductSupplier, 0, len(*req.Suppliers))
suppliers, err = s.Repository.GetSuppliersByIDs(ctx, supplierIDs) 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 { if err != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", err) s.Log.Errorf("Failed to validate suppliers: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers") 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 { if supplierUpdate {
var ids []uint if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, supplierLinks); err != nil {
if len(supplierIDs) > 0 {
ids = supplierIDs
}
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
return err return err
} }
} }
@@ -1,5 +1,10 @@
package validation 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 { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3,max=50"`
Brand string `json:"brand" validate:"required_strict,min=2,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"` SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"` Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"` 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"` Flags []string `json:"flags,omitempty" validate:"omitempty,dive"`
} }
@@ -24,7 +29,7 @@ type Update struct {
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"` SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"` Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"` 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"` Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"`
} }
@@ -10,6 +10,7 @@ import (
type SupplierNonstockDTO struct { type SupplierNonstockDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Price float64 `json:"price"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags []string `json:"flags"` Flags []string `json:"flags"`
} }
@@ -42,6 +43,7 @@ func toSupplierNonstockDTOs(relations []entity.NonstockSupplier) []SupplierNonst
result = append(result, SupplierNonstockDTO{ result = append(result, SupplierNonstockDTO{
Id: Nonstock.Id, Id: Nonstock.Id,
Name: Nonstock.Name, Name: Nonstock.Name,
Price: relation.Price,
Uom: uomRef, Uom: uomRef,
Flags: flags, Flags: flags,
}) })
@@ -8,12 +8,13 @@ import (
// === DTO Structs === // === DTO Structs ===
type SupplierProductDTO struct { type SupplierProductDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
ProductPrice float64 `gorm:"type:numeric(15,3);not null"` ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"` SellingPrice *float64 `gorm:"type:numeric(15,3)"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` SupplierPrice float64 `json:"supplier_price"`
Flags []string `json:"flags"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags []string `json:"flags"`
} }
// === Mapper Functions === // === Mapper Functions ===
@@ -42,12 +43,13 @@ func toSupplierProductDTOs(relations []entity.ProductSupplier) []SupplierProduct
} }
result = append(result, SupplierProductDTO{ result = append(result, SupplierProductDTO{
Id: product.Id, Id: product.Id,
Name: product.Name, Name: product.Name,
ProductPrice: product.ProductPrice, ProductPrice: product.ProductPrice,
SellingPrice: product.SellingPrice, SellingPrice: product.SellingPrice,
Uom: uomRef, SupplierPrice: relation.Price,
Flags: flags, Uom: uomRef,
Flags: flags,
}) })
} }
return result return result