feat(BE-48): auto-create product_warehouse on stock adjustment & remove unused APIs

- Change logic: automatically create product_warehouse if it does not exist during stock adjustment
- Remove unnecessary/unused API endpoints
- Ensure adjustment process continues even if product_warehouse was not previously available
This commit is contained in:
aguhh18
2025-10-13 11:38:05 +07:00
parent ce28429efd
commit 5283aed996
8 changed files with 69 additions and 207 deletions
@@ -7,6 +7,7 @@ import (
sAdjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services" sAdjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories" rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories"
@@ -21,8 +22,9 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
warehouseRepo := rWarehouse.NewWarehouseRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
productRepo := rproduct.NewProductRepository(db)
adjustmentService := sAdjustment.NewAdjustmentService(stockLogsRepo, warehouseRepo, productWarehouseRepo, validate) adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
AdjustmentRoutes(router, userService, adjustmentService) AdjustmentRoutes(router, userService, adjustmentService)
@@ -7,6 +7,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations"
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories" stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -29,15 +30,17 @@ type adjustmentService struct {
StockLogsRepository stockLogsRepo.StockLogRepository StockLogsRepository stockLogsRepo.StockLogRepository
WarehouseRepo warehouseRepo.WarehouseRepository WarehouseRepo warehouseRepo.WarehouseRepository
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
ProductRepo productRepo.ProductRepository
} }
func NewAdjustmentService(stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService { func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService {
return &adjustmentService{ return &adjustmentService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
StockLogsRepository: stockLogsRepo, StockLogsRepository: stockLogsRepo,
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
ProductRepo: productRepo,
} }
} }
@@ -74,14 +77,27 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
} }
ctx := c.Context() ctx := c.Context()
productWarehouseExists, err := s.ProductWarehouseRepo.ProductWarehouseExists(ctx, uint(req.ProductID), uint(req.WarehouseID), nil) isProductExist, err := s.ProductRepo.IdExists(c.Context(), uint(req.ProductID))
if err != nil { if err != nil {
return nil, err s.Log.Errorf("Failed to check product existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product")
} }
if !productWarehouseExists { if !isProductExist {
return nil, fiber.NewError(fiber.StatusBadRequest, "Product warehouse not found") return nil, fiber.NewError(fiber.StatusBadRequest, "Product not found")
} }
isWarehouseExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(req.WarehouseID))
if err != nil {
s.Log.Errorf("Failed to check warehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
}
if !isWarehouseExist {
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse not found")
}
if req.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Quantity must be greater than zero")
}
transactionType := strings.ToUpper(req.TransactionType) transactionType := strings.ToUpper(req.TransactionType)
if transactionType != entity.TransactionTypeIncrease && transactionType != entity.TransactionTypeDecrease { if transactionType != entity.TransactionTypeIncrease && transactionType != entity.TransactionTypeDecrease {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type")
@@ -89,16 +105,33 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
var createdLogId uint var createdLogId uint
isProductWarehouseExist, err := s.ProductWarehouseRepo.ProductWarehouseExistByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
if err != nil {
s.Log.Errorf("Failed to check product warehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
}
if !isProductWarehouseExist {
newPW := &entity.ProductWarehouse{
ProductId: uint(req.ProductID),
WarehouseId: uint(req.WarehouseID),
Quantity: 0,
CreatedBy: 1, // TODO: should Get from auth middleware
}
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
s.Log.Errorf("Failed to create product warehouse: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
}
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
}
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// Get product warehouse by product id and warehouse id (read operation, no transaction needed)
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID)) productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
if err != nil { if err != nil {
return err s.Log.Errorf("Failed to get product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
} }
if productWarehouse == nil {
return fiber.NewError(fiber.StatusBadRequest, "Product warehouse not found")
}
s.Log.Infof("Product Warehouse found: %+v", productWarehouse.Id)
afterQuantity := productWarehouse.Quantity afterQuantity := productWarehouse.Quantity
if transactionType == entity.TransactionTypeIncrease { if transactionType == entity.TransactionTypeIncrease {
@@ -135,7 +168,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
} }
s.Log.Infof("Product warehouse quantity updated: %+v", productWarehouse.Id) s.Log.Infof("Product warehouse quantity updated: %+v", productWarehouse.Id)
// Set createdLogId to get the log with relations after transaction
createdLogId = newLog.Id createdLogId = newLog.Id
return nil return nil
}) })
@@ -72,70 +72,4 @@ func (u *ProductWarehouseController) GetOne(c *fiber.Ctx) error {
}) })
} }
func (u *ProductWarehouseController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ProductWarehouseService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create productWarehouse successfully",
Data: dto.ToProductWarehouseListDTO(*result),
})
}
func (u *ProductWarehouseController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ProductWarehouseService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update productWarehouse successfully",
Data: dto.ToProductWarehouseListDTO(*result),
})
}
func (u *ProductWarehouseController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ProductWarehouseService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete productWarehouse successfully",
})
}
@@ -13,6 +13,7 @@ type ProductWarehouseRepository interface {
ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error)
IsProductExist(ctx context.Context, productId uint) (bool, error) IsProductExist(ctx context.Context, productId uint) (bool, error)
IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error)
ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error)
ExistsByID(ctx context.Context, id uint) (bool, error) ExistsByID(ctx context.Context, id uint) (bool, error)
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
} }
@@ -53,6 +54,17 @@ func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint
return repository.Exists[entity.ProductWarehouse](ctx, r.db, id) return repository.Exists[entity.ProductWarehouse](ctx, r.db, id)
} }
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) {
var count int64
if err := r.db.WithContext(ctx).
Model(&entity.ProductWarehouse{}).
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) { func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse var productWarehouse entity.ProductWarehouse
if err := r.DB().WithContext(ctx).Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).First(&productWarehouse).Error; err != nil { if err := r.DB().WithContext(ctx).Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).First(&productWarehouse).Error; err != nil {
@@ -21,8 +21,6 @@ func ProductWarehouseRoutes(v1 fiber.Router, u user.UserService, s productWareho
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll) route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne) route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
} }
@@ -17,9 +17,6 @@ import (
type ProductWarehouseService interface { type ProductWarehouseService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProductWarehouse, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProductWarehouse, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProductWarehouse, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductWarehouse, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
} }
type productWarehouseService struct { type productWarehouseService struct {
@@ -79,125 +76,3 @@ func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductW
} }
return productWarehouse, nil return productWarehouse, nil
} }
func (s *productWarehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProductWarehouse, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
isProductExist, err := s.Repository.IsProductExist(c.Context(), req.ProductId)
if err != nil {
s.Log.Errorf("Failed to check product existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product existence")
}
if !isProductExist {
return nil, fiber.NewError(fiber.StatusBadRequest, "Product not found")
}
isWarehouseExist, err := s.Repository.IsWarehouseExist(c.Context(), req.WarehouseId)
if err != nil {
s.Log.Errorf("Failed to check warehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check warehouse existence")
}
if !isWarehouseExist {
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse not found")
}
// chceking if productWarehouse with same product_id and warehouse_id already
exists, err := s.Repository.ProductWarehouseExists(c.Context(), req.ProductId, req.WarehouseId, nil)
if err != nil {
s.Log.Errorf("Failed to check productWarehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check productWarehouse existence")
}
if exists {
return nil, fiber.NewError(fiber.StatusConflict, "ProductWarehouse already exists")
}
createBody := &entity.ProductWarehouse{
ProductId: req.ProductId,
WarehouseId: req.WarehouseId,
Quantity: req.Quantity,
CreatedBy: 1, // TODO: Ganti dengan user ID dari context setelah middleware auth diimplementasi
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create productWarehouse: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s productWarehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductWarehouse, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
// validation Id exist
if exists, err := s.Repository.ExistsByID(c.Context(), id); err != nil {
s.Log.Errorf("Failed to check productWarehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check productWarehouse existence")
} else if !exists {
return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found")
}
// validation productId and warehouseId exist
if req.ProductId != nil {
isProductExist, err := s.Repository.IsProductExist(c.Context(), *req.ProductId)
if err != nil {
s.Log.Errorf("Failed to check product existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check product existence")
}
if !isProductExist {
return nil, fiber.NewError(fiber.StatusBadRequest, "Product not found")
}
}
if req.WarehouseId != nil {
isWarehouseExist, err := s.Repository.IsWarehouseExist(c.Context(), *req.WarehouseId)
if err != nil {
s.Log.Errorf("Failed to check warehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check warehouse existence")
}
if !isWarehouseExist {
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
}
updateBody := make(map[string]any)
if req.ProductId != nil {
updateBody["product_id"] = *req.ProductId
}
if req.WarehouseId != nil {
updateBody["warehouse_id"] = *req.WarehouseId
}
if req.Quantity != nil {
updateBody["quantity"] = *req.Quantity
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found")
}
s.Log.Errorf("Failed to update productWarehouse: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s productWarehouseService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found")
}
s.Log.Errorf("Failed to delete productWarehouse: %+v", err)
return err
}
return nil
}
@@ -13,6 +13,7 @@ type ProductRepository interface {
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
SkuExists(ctx context.Context, sku string, excludeID *uint) (bool, error) SkuExists(ctx context.Context, sku string, excludeID *uint) (bool, error)
UomExists(ctx context.Context, uomID uint) (bool, error) UomExists(ctx context.Context, uomID uint) (bool, error)
IdExists(ctx context.Context, id uint) (bool, error)
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)
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error
@@ -194,3 +195,7 @@ func (r *ProductRepositoryImpl) GetFlags(ctx context.Context, productID uint) ([
} }
return flags, nil return flags, nil
} }
func (r *ProductRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Product](ctx, r.DB(), id)
}
@@ -14,6 +14,7 @@ type WarehouseRepository interface {
LocationExists(ctx context.Context, locationId uint) (bool, error) LocationExists(ctx context.Context, locationId uint) (bool, error)
KandangExists(ctx context.Context, kandangId uint) (bool, error) KandangExists(ctx context.Context, kandangId uint) (bool, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
IdExists(ctx context.Context, id uint) (bool, error)
} }
type WarehouseRepositoryImpl struct { type WarehouseRepositoryImpl struct {
@@ -43,3 +44,6 @@ func (r *WarehouseRepositoryImpl) KandangExists(ctx context.Context, kandangId u
func (r *WarehouseRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) { func (r *WarehouseRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Warehouse](ctx, r.db, name, excludeID) return repository.ExistsByName[entity.Warehouse](ctx, r.db, name, excludeID)
} }
func (r *WarehouseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Warehouse](ctx, r.db, id)
}