package service import ( "context" "errors" "fmt" "strings" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" 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" warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "gorm.io/gorm" ) type AdjustmentService interface { Adjustment(ctx *fiber.Ctx, req *validation.Create) (*entity.AdjustmentStock, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.AdjustmentStock, error) AdjustmentHistory(ctx *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error) } type adjustmentService struct { 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, 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, AdjustmentStockRepository: adjustmentStockRepo, FifoSvc: fifoSvc, } } func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). Preload("ProductWarehouse.Warehouse"). Preload("StockLog.CreatedUser") } func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.AdjustmentStock, error) { adjustmentStock, err := s.AdjustmentStockRepository.GetByID(c.Context(), id, s.withRelations) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found") } s.Log.Errorf("Failed to get adjustment by id: %+v", err) return nil, err } return adjustmentStock, nil } func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*entity.AdjustmentStock, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } ctx := c.Context() actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists}, common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists}, ); err != nil { return nil, err } if req.Quantity <= 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Quantity must be greater than zero") } transactionType := strings.ToUpper(req.TransactionType) if transactionType != string(utils.StockLogTransactionTypeIncrease) && transactionType != string(utils.StockLogTransactionTypeDecrease) { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type") } var createdAdjustmentStockId uint var projectFlockKandangID *uint pfkID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID)) if err == nil && pfkID > 0 { projectFlockKandangID = &pfkID } pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(ctx, uint(req.ProductID), uint(req.WarehouseID), projectFlockKandangID) if err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse") } newPW := &entity.ProductWarehouse{ ProductId: uint(req.ProductID), WarehouseId: uint(req.WarehouseID), Quantity: 0, ProjectFlockKandangId: projectFlockKandangID, } if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse") } pw = newPW } if err := common.EnsureProjectFlockNotClosedForProductWarehouses( ctx, s.StockLogsRepository.DB(), []uint{pw.Id}, ); err != nil { return nil, err } err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { productWarehouse, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(ctx, uint(req.ProductID), uint(req.WarehouseID), projectFlockKandangID) if err != nil { s.Log.Errorf("Failed to get product warehouse: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse") } afterQuantity := productWarehouse.Quantity newLog := &entity.StockLog{ LoggableType: string(utils.StockLogTypeAdjustment), LoggableId: 0, Notes: req.Note, ProductWarehouseId: productWarehouse.Id, CreatedBy: actorID, } stockLogs, err := s.StockLogsRepository.GetByProductWarehouse(ctx, productWarehouse.Id, 1) if err != nil { s.Log.Errorf("Failed to get stock logs: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") } if len(stockLogs) > 0 { latestStockLog := stockLogs[0] newLog.Stock = latestStockLog.Stock } else { newLog.Stock = 0 } if transactionType == string(utils.StockLogTransactionTypeIncrease) { afterQuantity += req.Quantity newLog.Increase = req.Quantity newLog.Stock += newLog.Increase } else { if productWarehouse.Quantity < req.Quantity { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk pengurangan. Stok saat ini: %.2f, Jumlah yang akan dikurangi: %.2f", productWarehouse.Quantity, req.Quantity)) } afterQuantity -= req.Quantity newLog.Decrease = req.Quantity newLog.Stock -= newLog.Decrease } if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil { return err } adjustmentStock := &entity.AdjustmentStock{ ProductWarehouseId: productWarehouse.Id, } if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record") } newLog.LoggableType = string(utils.StockLogTypeAdjustment) newLog.LoggableId = adjustmentStock.Id if err := s.StockLogsRepository.WithTx(tx).UpdateOne(ctx, newLog.Id, newLog, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to link stock log") } if transactionType == string(utils.StockLogTransactionTypeIncrease) { note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id) _, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{ StockableKey: fifo.StockableKeyAdjustmentIn, StockableID: adjustmentStock.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)) } } else { _, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{ UsableKey: fifo.UsableKeyAdjustmentOut, UsableID: adjustmentStock.Id, ProductWarehouseID: uint(productWarehouse.Id), Quantity: req.Quantity, AllowPending: false, Tx: tx, }) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO: %v", err)) } } 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) return err } createdAdjustmentStockId = adjustmentStock.Id return nil }) if err != nil { s.Log.Errorf("Transaction failed in CreateOne: %+v", err) var fiberErr *fiber.Error if errors.As(err, &fiberErr) { return nil, err } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process adjustment transaction") } return s.GetOne(c, createdAdjustmentStockId) } func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) { warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID)) } s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err) return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang") } if warehouse.KandangId == nil || *warehouse.KandangId == 0 { return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID)) } projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId)) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId)) } s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err) return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang") } return uint(projectFlockKandang.Id), nil } func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error) { if err := s.Validate.Struct(query); err != nil { return nil, 0, err } offset := (query.Page - 1) * query.Limit var isProductsExist bool isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID)) if err != nil { return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse") } if query.WarehouseID > 0 && !isWarehousesExist { return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") } isProductsExist, err = s.ProductRepo.IdExists(c.Context(), uint(query.ProductID)) if err != nil { s.Log.Errorf("Failed to check product existence: %+v", err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product") } if query.ProductID > 0 && !isProductsExist { return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found") } var adjustmentStocks []entity.AdjustmentStock var total int64 q := s.AdjustmentStockRepository.DB().WithContext(c.Context()).Model(&entity.AdjustmentStock{}). Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). Preload("ProductWarehouse.Warehouse"). Preload("StockLog.CreatedUser") if query.ProductID > 0 { q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id"). Where("product_warehouses.product_id = ?", query.ProductID) } if query.WarehouseID > 0 { q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id"). Where("product_warehouses.warehouse_id = ?", query.WarehouseID) } if query.TransactionType != "" { q = q.Joins("JOIN stock_logs ON stock_logs.loggable_type = ? AND stock_logs.loggable_id = adjustment_stocks.id", "ADJUSTMENT"). Where("stock_logs.transaction_type = ?", strings.ToUpper(query.TransactionType)) } if err = q.Count(&total).Error; err != nil { s.Log.Errorf("Failed to get adjustments: %+v", err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get adjustment history") } err = q.Offset(offset).Limit(query.Limit).Order("created_at DESC").Find(&adjustmentStocks).Error if err != nil { s.Log.Errorf("Failed to get adjustments: %+v", err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get adjustment history") } result := make([]*entity.AdjustmentStock, len(adjustmentStocks)) for i := range adjustmentStocks { result[i] = &adjustmentStocks[i] } return result, total, nil }