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" 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" "gorm.io/gorm" ) type AdjustmentService interface { Adjustment(ctx *fiber.Ctx, req *validation.Create) (*entity.StockLog, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.StockLog, error) AdjustmentHistory(ctx *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, 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 } func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) AdjustmentService { return &adjustmentService{ Log: utils.Log, Validate: validate, StockLogsRepository: stockLogsRepo, WarehouseRepo: warehouseRepo, ProductWarehouseRepo: productWarehouseRepo, ProductRepo: productRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, } } func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). Preload("ProductWarehouse.Warehouse"). Preload("CreatedUser") } func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, error) { stockLog, err := s.StockLogsRepository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { return s.withRelations(db).Preload("ProductWarehouse.Product.ProductCategory") }) 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 } if stockLog.LoggableType != entity.LogTypeAdjustment { return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found") } return stockLog, nil } func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*entity.StockLog, 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 != entity.TransactionTypeIncrease && transactionType != entity.TransactionTypeDecrease { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type") } 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 { projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID)) if err != nil { return nil, err } newPW := &entity.ProductWarehouse{ ProductId: uint(req.ProductID), WarehouseId: uint(req.WarehouseID), Quantity: 0, ProjectFlockKandangId: &projectFlockKandangID, // 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) } pw, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID( ctx, uint(req.ProductID), uint(req.WarehouseID), ) if err != nil { s.Log.Errorf("Failed to get product warehouse for project flock check: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse") } 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.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID)) 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{ // TransactionType: transactionType, LoggableType: entity.LogTypeAdjustment, LoggableId: 0, Notes: req.Note, ProductWarehouseId: productWarehouse.Id, CreatedBy: actorID, // TODO: should Get from auth middleware } if transactionType == entity.TransactionTypeIncrease { afterQuantity += req.Quantity newLog.Increase = afterQuantity } else { if productWarehouse.Quantity < req.Quantity { return fiber.NewError(fiber.StatusBadRequest, "Insufficient stock for adjustment") } afterQuantity -= req.Quantity newLog.Decrease = afterQuantity } if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil { s.Log.Errorf("Failed to create stock log: %+v", err) return 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 } createdLogId = newLog.Id return nil }) if err != nil { s.Log.Errorf("Transaction failed in CreateOne: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process adjustment transaction") } return s.GetOne(c, createdLogId) } 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.StockLog, int64, error) { if err := s.Validate.Struct(query); err != nil { return nil, 0, err } offset := (query.Page - 1) * query.Limit isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID)) if err != nil { s.Log.Errorf("Failed to check warehouse existence: %+v", err) 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") } stockLogs, total, err := s.StockLogsRepository.GetAll(c.Context(), offset, query.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) db = db.Where("loggable_type = ?", entity.LogTypeAdjustment) if query.TransactionType != "" { db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType)) } db = s.StockLogsRepository.ApplyProductWarehouseFilters(db, uint(query.ProductID), uint(query.WarehouseID)) return db.Order("created_at DESC") }) 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.StockLog, len(stockLogs)) for i, v := range stockLogs { result[i] = &v } return result, total, nil }