package service import ( "errors" "fmt" 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" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" "strings" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" "gorm.io/gorm" ) type WarehouseService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Warehouse, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.Warehouse, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Warehouse, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Warehouse, error) DeleteOne(ctx *fiber.Ctx, id uint) error } type warehouseService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.WarehouseRepository } func NewWarehouseService(repo repository.WarehouseRepository, validate *validator.Validate) WarehouseService { return &warehouseService{ Log: utils.Log, Validate: validate, Repository: repo, } } func (s warehouseService) withRelations(db *gorm.DB) *gorm.DB { return db.Preload("CreatedUser").Preload("Area").Preload("Location").Preload("Kandang") } func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Warehouse, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) if params.Search != "" { db = db.Where("warehouses.name ILIKE ?", "%"+params.Search+"%") } if params.AreaId != 0 { db = db.Where("area_id = ?", params.AreaId) } if params.ActiveProjectFlockOnly { db = db.Where(` EXISTS ( SELECT 1 FROM kandangs k JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id JOIN ( SELECT DISTINCT ON (approvable_id) approvable_id, step_name, action_at FROM approvals WHERE approvable_type = 'PROJECT_FLOCKS' ORDER BY approvable_id, action_at DESC ) latest_approval ON latest_approval.approvable_id = pfk.project_flock_id WHERE k.id = warehouses.kandang_id AND LOWER(latest_approval.step_name) = LOWER(?) ) `, "Aktif") } return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { s.Log.Errorf("Failed to get warehouses: %+v", err) return nil, 0, err } return warehouses, total, nil } func (s warehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.Warehouse, error) { warehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") } if err != nil { s.Log.Errorf("Failed get warehouse by id: %+v", err) return nil, err } return warehouse, nil } func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Warehouse, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil { s.Log.Errorf("Failed to check warehouse name: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check warehouse name") } else if exists { return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Warehouse with name %s already exists", req.Name)) } typ := strings.ToUpper(req.Type) createValidationOpts := WarehouseTypeValidationOptions{ LocationProvided: req.LocationId != nil, KandangProvided: req.KandangId != nil, } if err := validateWarehouseTypeRequirements(typ, &req.AreaId, &req.LocationId, &req.KandangId, createValidationOpts); err != nil { return nil, err } //? Check relation area, location, and kandang if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists}, common.RelationCheck{Name: "Location", ID: req.LocationId, Exists: s.Repository.LocationExists}, common.RelationCheck{Name: "Kandang", ID: req.KandangId, Exists: s.Repository.KandangExists}, ); err != nil { return nil, err } actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } createBody := &entity.Warehouse{ Name: req.Name, Type: typ, AreaId: req.AreaId, CreatedBy: actorID, } if req.LocationId != nil { createBody.LocationId = req.LocationId } if req.KandangId != nil { createBody.KandangId = req.KandangId } if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { s.Log.Errorf("Failed to create warehouse: %+v", err) return nil, err } return s.GetOne(c, createBody.Id) } func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Warehouse, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } existing, err := s.GetOne(c, id) if err != nil { s.Log.Errorf("Failed to get warehouse for update: %+v", err) return nil, err } updateBody := make(map[string]any) if req.Name != nil { if exists, err := s.Repository.NameExists(c.Context(), *req.Name, &id); err != nil { s.Log.Errorf("Failed to check warehouse name: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check warehouse name") } else if exists { return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Warehouse with name %s already exists", *req.Name)) } updateBody["name"] = *req.Name } if req.Type != nil { normalizedType := strings.ToUpper(*req.Type) updateBody["type"] = normalizedType req.Type = &normalizedType } if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Area", ID: req.AreaId, Exists: s.Repository.AreaExists}, common.RelationCheck{Name: "Location", ID: req.LocationId, Exists: s.Repository.LocationExists}, common.RelationCheck{Name: "Kandang", ID: req.KandangId, Exists: s.Repository.KandangExists}, ); err != nil { return nil, err } if req.AreaId != nil { updateBody["area_id"] = *req.AreaId } if req.LocationId != nil { updateBody["location_id"] = req.LocationId } if req.KandangId != nil { updateBody["kandang_id"] = req.KandangId } finalType := strings.ToUpper(existing.Type) if req.Type != nil { finalType = *req.Type } finalAreaId := existing.AreaId if req.AreaId != nil { finalAreaId = *req.AreaId } finalLocationId := existing.LocationId if req.LocationId != nil { finalLocationId = req.LocationId } finalKandangId := existing.KandangId if req.KandangId != nil { finalKandangId = req.KandangId } originalLocationId := finalLocationId originalKandangId := finalKandangId updateValidationOpts := WarehouseTypeValidationOptions{ AutoClear: true, LocationProvided: req.LocationId != nil, KandangProvided: req.KandangId != nil, } if err := validateWarehouseTypeRequirements(finalType, &finalAreaId, &finalLocationId, &finalKandangId, updateValidationOpts); err != nil { return nil, err } if originalLocationId != finalLocationId { updateBody["location_id"] = nil } if originalKandangId != finalKandangId { updateBody["kandang_id"] = nil } 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, "Warehouse not found") } s.Log.Errorf("Failed to update warehouse: %+v", err) return nil, err } return s.GetOne(c, id) } func (s warehouseService) 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, "Warehouse not found") } s.Log.Errorf("Failed to delete warehouse: %+v", err) return err } return nil } type WarehouseTypeValidationOptions struct { AutoClear bool LocationProvided bool KandangProvided bool } func validateWarehouseTypeRequirements(typ string, areaID *uint, locationID **uint, kandangID **uint, opts WarehouseTypeValidationOptions) error { switch utils.WarehouseType(typ) { case utils.WarehouseTypeArea: if areaID == nil || *areaID == 0 { return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is AREA") } if locationID != nil && *locationID != nil { if opts.AutoClear && !opts.LocationProvided { *locationID = nil } else { return fiber.NewError(fiber.StatusBadRequest, "location_id must not be provided when type is AREA") } } if kandangID != nil && *kandangID != nil { if opts.AutoClear && !opts.KandangProvided { *kandangID = nil } else { return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is AREA") } } return nil case utils.WarehouseTypeLokasi: if areaID == nil || *areaID == 0 { return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is LOCATION") } if locationID == nil || *locationID == nil { return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is LOCATION") } if **locationID == 0 { return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is LOCATION") } if kandangID != nil && *kandangID != nil { if opts.AutoClear && !opts.KandangProvided { *kandangID = nil } else { return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is LOCATION") } } return nil case utils.WarehouseTypeKandang: if areaID == nil || *areaID == 0 { return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is KANDANG") } if locationID == nil || *locationID == nil { return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is KANDANG") } if **locationID == 0 { return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is KANDANG") } if kandangID == nil || *kandangID == nil { return fiber.NewError(fiber.StatusBadRequest, "kandang_id is required when type is KANDANG") } if **kandangID == 0 { return fiber.NewError(fiber.StatusBadRequest, "kandang_id must be greater than 0 when type is KANDANG") } return nil default: return fiber.NewError(fiber.StatusBadRequest, "Invalid warehouse type") } }