mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
feat(BE): integrate FIFO service for stock adjustments and transfers
- Added FIFO service integration in the adjustments module to manage stockable and usable items for adjustments. - Created a new repository for adjustment stocks to handle database operations. - Enhanced the adjustment service to track stock adjustments using FIFO logic for both increase and decrease operations. - Updated product warehouse DTOs and repositories to include project flock information. - Implemented FIFO logic in the transfer module to manage stock transfers between warehouses. - Added integration tests for FIFO operations in stock transfers, ensuring correct stock consumption and replenishment.
This commit is contained in:
@@ -5,6 +5,9 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rAdjustmentStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/repositories"
|
||||
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"
|
||||
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||
@@ -13,19 +16,67 @@ import (
|
||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
)
|
||||
|
||||
type AdjustmentModule struct{}
|
||||
|
||||
func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
// Repositories
|
||||
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
productRepo := rproduct.NewProductRepository(db)
|
||||
adjustmentStockRepo := rAdjustmentStock.NewAdjustmentStockRepository(db)
|
||||
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
|
||||
|
||||
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate, projectFlockKandangRepo)
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||
|
||||
err := fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKey("ADJUSTMENT_IN"),
|
||||
Table: "adjustment_stocks",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
TotalQuantity: "total_qty",
|
||||
TotalUsedQuantity: "total_used",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
OrderBy: []string{"created_at ASC", "id ASC"},
|
||||
})
|
||||
if err != nil {
|
||||
panic("Failed to register ADJUSTMENT_IN as Stockable: " + err.Error())
|
||||
}
|
||||
|
||||
err = fifoService.RegisterUsable(fifo.UsableConfig{
|
||||
Key: fifo.UsableKey("ADJUSTMENT_OUT"),
|
||||
Table: "adjustment_stocks",
|
||||
Columns: fifo.UsableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
UsageQuantity: "usage_qty",
|
||||
PendingQuantity: "pending_qty",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
OrderBy: []string{"created_at ASC", "id ASC"},
|
||||
})
|
||||
if err != nil {
|
||||
panic("Failed to register ADJUSTMENT_OUT as Usable: " + err.Error())
|
||||
}
|
||||
|
||||
adjustmentService := sAdjustment.NewAdjustmentService(
|
||||
productRepo,
|
||||
stockLogsRepo,
|
||||
warehouseRepo,
|
||||
productWarehouseRepo,
|
||||
adjustmentStockRepo,
|
||||
fifoService,
|
||||
validate,
|
||||
projectFlockKandangRepo,
|
||||
)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
AdjustmentRoutes(router, userService, adjustmentService)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AdjustmentStockRepository interface {
|
||||
CreateOne(ctx context.Context, data *entity.AdjustmentStock, modifier func(*gorm.DB) *gorm.DB) error
|
||||
GetByStockLogID(ctx context.Context, stockLogID uint) (*entity.AdjustmentStock, error)
|
||||
WithTx(tx *gorm.DB) AdjustmentStockRepository
|
||||
DB() *gorm.DB
|
||||
}
|
||||
|
||||
type adjustmentStockRepositoryImpl struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAdjustmentStockRepository(db *gorm.DB) AdjustmentStockRepository {
|
||||
return &adjustmentStockRepositoryImpl{db: db}
|
||||
}
|
||||
|
||||
func (r *adjustmentStockRepositoryImpl) CreateOne(ctx context.Context, data *entity.AdjustmentStock, modifier func(*gorm.DB) *gorm.DB) error {
|
||||
q := r.db.WithContext(ctx)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
return q.Create(data).Error
|
||||
}
|
||||
|
||||
func (r *adjustmentStockRepositoryImpl) GetByStockLogID(ctx context.Context, stockLogID uint) (*entity.AdjustmentStock, error) {
|
||||
var record entity.AdjustmentStock
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("stock_log_id = ?", stockLogID).
|
||||
First(&record).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
func (r *adjustmentStockRepositoryImpl) WithTx(tx *gorm.DB) AdjustmentStockRepository {
|
||||
return &adjustmentStockRepositoryImpl{db: tx}
|
||||
}
|
||||
|
||||
func (r *adjustmentStockRepositoryImpl) DB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
adjustmentStockRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/repositories"
|
||||
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"
|
||||
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||
@@ -29,24 +30,37 @@ type AdjustmentService interface {
|
||||
}
|
||||
|
||||
type adjustmentService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
StockLogsRepository stockLogsRepo.StockLogRepository
|
||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
|
||||
ProductRepo productRepo.ProductRepository
|
||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
StockLogsRepository stockLogsRepo.StockLogRepository
|
||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
|
||||
ProductRepo productRepo.ProductRepository
|
||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||
AdjustmentStockRepository adjustmentStockRepo.AdjustmentStockRepository
|
||||
FifoSvc common.FifoService
|
||||
}
|
||||
|
||||
func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) AdjustmentService {
|
||||
func NewAdjustmentService(
|
||||
productRepo productRepo.ProductRepository,
|
||||
stockLogsRepo stockLogsRepo.StockLogRepository,
|
||||
warehouseRepo warehouseRepo.WarehouseRepository,
|
||||
productWarehouseRepo ProductWarehouse.ProductWarehouseRepository,
|
||||
adjustmentStockRepo adjustmentStockRepo.AdjustmentStockRepository,
|
||||
fifoSvc common.FifoService,
|
||||
validate *validator.Validate,
|
||||
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
||||
) AdjustmentService {
|
||||
return &adjustmentService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
StockLogsRepository: stockLogsRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
ProductRepo: productRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
StockLogsRepository: stockLogsRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
ProductRepo: productRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
AdjustmentStockRepository: adjustmentStockRepo,
|
||||
FifoSvc: fifoSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,15 +166,16 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||
}
|
||||
|
||||
// Create StockLog for history tracking
|
||||
afterQuantity := productWarehouse.Quantity
|
||||
newLog := &entity.StockLog{
|
||||
|
||||
LoggableType: string(utils.StockLogTypeAdjustment),
|
||||
LoggableId: 0,
|
||||
Notes: req.Note,
|
||||
ProductWarehouseId: productWarehouse.Id,
|
||||
CreatedBy: actorID, // TODO: should Get from auth middleware
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
|
||||
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||
afterQuantity += req.Quantity
|
||||
newLog.Increase = afterQuantity
|
||||
@@ -177,6 +192,57 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
return err
|
||||
}
|
||||
|
||||
// Create AdjustmentStock record for FIFO tracking
|
||||
adjustmentStock := &entity.AdjustmentStock{
|
||||
StockLogId: newLog.Id,
|
||||
ProductWarehouseId: productWarehouse.Id,
|
||||
}
|
||||
|
||||
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||
// Adjustment INCREASE → Replenish stock (Stockable)
|
||||
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
|
||||
replenishResult, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
|
||||
StockableKey: "ADJUSTMENT_IN",
|
||||
StockableID: newLog.Id,
|
||||
ProductWarehouseID: uint(productWarehouse.Id),
|
||||
Quantity: req.Quantity,
|
||||
Note: ¬e,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock via FIFO: %v", err))
|
||||
}
|
||||
|
||||
// Update stockable tracking fields
|
||||
adjustmentStock.TotalQty = replenishResult.AddedQuantity
|
||||
adjustmentStock.TotalUsed = 0
|
||||
|
||||
} else {
|
||||
// Adjustment DECREASE → Consume stock (Usable)
|
||||
consumeResult, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
||||
UsableKey: "ADJUSTMENT_OUT",
|
||||
UsableID: newLog.Id,
|
||||
ProductWarehouseID: uint(productWarehouse.Id),
|
||||
Quantity: req.Quantity,
|
||||
AllowPending: false, // Don't allow pending for adjustment
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO: %v", err))
|
||||
}
|
||||
|
||||
// Update usable tracking fields
|
||||
adjustmentStock.UsageQty = consumeResult.UsageQuantity
|
||||
adjustmentStock.PendingQty = consumeResult.PendingQuantity
|
||||
}
|
||||
|
||||
// Save AdjustmentStock record
|
||||
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create adjustment stock: %+v", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
|
||||
}
|
||||
|
||||
// Update ProductWarehouse quantity (for backward compatibility/reporting)
|
||||
productWarehouse.Quantity = afterQuantity
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
|
||||
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
||||
|
||||
Reference in New Issue
Block a user