package service import ( "errors" "strings" common "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" 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" stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" ) 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 } func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService { return &adjustmentService{ Log: utils.Log, Validate: validate, StockLogsRepository: stockLogsRepo, WarehouseRepo: warehouseRepo, ProductWarehouseRepo: productWarehouseRepo, ProductRepo: productRepo, } } 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.LogType != 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() 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 { 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 { 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 if transactionType == entity.TransactionTypeIncrease { afterQuantity += req.Quantity } else { if productWarehouse.Quantity < req.Quantity { return fiber.NewError(fiber.StatusBadRequest, "Insufficient stock for adjustment") } afterQuantity -= req.Quantity } newLog := &entity.StockLog{ TransactionType: transactionType, Quantity: req.Quantity, BeforeQuantity: productWarehouse.Quantity, AfterQuantity: afterQuantity, LogType: entity.LogTypeAdjustment, LogId: 0, Note: req.Note, ProductWarehouseId: productWarehouse.Id, CreatedBy: 1, // TODO: should Get from auth middleware } 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) 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("log_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 }