package service import ( "context" "errors" "fmt" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" common "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" "gorm.io/gorm" ) type ProjectflockService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) DeleteOne(ctx *fiber.Ctx, id uint) error GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) } type projectflockService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.ProjectflockRepository FlockRepo flockRepository.FlockRepository KandangRepo kandangRepository.KandangRepository } type FlockPeriodSummary struct { Flock entity.Flock NextPeriod int } func NewProjectflockService( repo repository.ProjectflockRepository, flockRepo flockRepository.FlockRepository, kandangRepo kandangRepository.KandangRepository, validate *validator.Validate, ) ProjectflockService { return &projectflockService{ Log: utils.Log, Validate: validate, Repository: repo, FlockRepo: flockRepo, KandangRepo: kandangRepo, } } func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). Preload("Flock"). Preload("Area"). Preload("ProductCategory"). Preload("Fcr"). Preload("Location"). Preload("Kandangs") } func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit projectflocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { s.Log.Errorf("Failed to get projectflocks: %+v", err) return nil, 0, err } return projectflocks, total, nil } func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { projectflock, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } if err != nil { s.Log.Errorf("Failed get projectflock by id: %+v", err) return nil, err } return projectflock, nil } func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } if len(req.KandangIds) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required") } if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())}, common.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())}, common.RelationCheck{Name: "Product category", ID: &req.ProductCategoryId, Exists: relationExistsChecker[entity.ProductCategory](s.Repository.DB())}, common.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())}, common.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())}, ); err != nil { return nil, err } kandangIDs := uniqueUintSlice(req.KandangIds) kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found") } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs") } if len(kandangs) != len(kandangIDs) { return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found") } for _, kandang := range kandangs { if kandang.ProjectFlockId != nil { return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah memiliki project flock", kandang.Name)) } } tx := s.Repository.DB().Begin() if tx.Error != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") } projectRepo := repository.NewProjectflockRepository(tx) nextPeriod, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId) if err != nil { tx.Rollback() s.Log.Errorf("Failed to determine next period for flock %d: %+v", req.FlockId, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine next period") } createBody := &entity.ProjectFlock{ FlockId: req.FlockId, AreaId: req.AreaId, ProductCategoryId: req.ProductCategoryId, FcrId: req.FcrId, LocationId: req.LocationId, Period: nextPeriod, CreatedBy: 1, } if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { tx.Rollback() if errors.Is(err, gorm.ErrDuplicatedKey) { return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists") } s.Log.Errorf("Failed to create projectflock: %+v", err) return nil, err } if err := tx.Model(&entity.Kandang{}). Where("id IN ?", kandangIDs). Updates(map[string]any{"project_flock_id": createBody.Id}).Error; err != nil { tx.Rollback() s.Log.Errorf("Failed to assign kandangs to projectflock: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to assign kandangs") } if err := tx.Commit().Error; err != nil { tx.Rollback() return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction") } return s.GetOne(c, createBody.Id) } func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } if err != nil { s.Log.Errorf("Failed to fetch projectflock %d before update: %+v", id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } updateBody := make(map[string]any) var relationChecks []common.RelationCheck if req.FlockId != nil { updateBody["flock_id"] = *req.FlockId relationChecks = append(relationChecks, common.RelationCheck{ Name: "Flock", ID: req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB()), }) } if req.AreaId != nil { updateBody["area_id"] = *req.AreaId relationChecks = append(relationChecks, common.RelationCheck{ Name: "Area", ID: req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB()), }) } if req.ProductCategoryId != nil { updateBody["product_category_id"] = *req.ProductCategoryId relationChecks = append(relationChecks, common.RelationCheck{ Name: "Product category", ID: req.ProductCategoryId, Exists: relationExistsChecker[entity.ProductCategory](s.Repository.DB()), }) } if req.FcrId != nil { updateBody["fcr_id"] = *req.FcrId relationChecks = append(relationChecks, common.RelationCheck{ Name: "FCR", ID: req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()), }) } if req.LocationId != nil { updateBody["location_id"] = *req.LocationId relationChecks = append(relationChecks, common.RelationCheck{ Name: "Location", ID: req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB()), }) } if req.Period != nil { updateBody["period"] = *req.Period } if len(relationChecks) > 0 { if err := common.EnsureRelations(c.Context(), relationChecks...); err != nil { return nil, err } } var newKandangIDs []uint if req.KandangIds != nil { if len(req.KandangIds) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids cannot be empty") } newKandangIDs = uniqueUintSlice(req.KandangIds) kandangs, err := s.KandangRepo.GetByIDs(c.Context(), newKandangIDs, nil) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found") } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs") } if len(kandangs) != len(newKandangIDs) { return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found") } for _, k := range kandangs { if k.ProjectFlockId != nil && *k.ProjectFlockId != id { return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah terikat dengan project flock lain", k.Name)) } } } tx := s.Repository.DB().Begin() if tx.Error != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") } projectRepo := repository.NewProjectflockRepository(tx) if len(updateBody) > 0 { if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil { tx.Rollback() if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } s.Log.Errorf("Failed to update projectflock: %+v", err) return nil, err } } if req.KandangIds != nil { existingIDs := make(map[uint]struct{}, len(existing.Kandangs)) for _, k := range existing.Kandangs { existingIDs[k.Id] = struct{}{} } newSet := make(map[uint]struct{}, len(newKandangIDs)) for _, id := range newKandangIDs { newSet[id] = struct{}{} } var toDetach []uint for id := range existingIDs { if _, ok := newSet[id]; !ok { toDetach = append(toDetach, id) } } var toAttach []uint for id := range newSet { if _, ok := existingIDs[id]; !ok { toAttach = append(toAttach, id) } } if len(toDetach) > 0 { if err := tx.Model(&entity.Kandang{}). Where("id IN ?", toDetach). Updates(map[string]any{"project_flock_id": nil}).Error; err != nil { tx.Rollback() s.Log.Errorf("Failed to detach kandangs: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") } } if len(toAttach) > 0 { if err := tx.Model(&entity.Kandang{}). Where("id IN ?", toAttach). Updates(map[string]any{"project_flock_id": id}).Error; err != nil { tx.Rollback() s.Log.Errorf("Failed to attach kandangs: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") } } } if err := tx.Commit().Error; err != nil { tx.Rollback() return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction") } return s.GetOne(c, id) } func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error { existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } if err != nil { s.Log.Errorf("Failed to fetch projectflock %d before delete: %+v", id, err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } tx := s.Repository.DB().Begin() if tx.Error != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") } if len(existing.Kandangs) > 0 { ids := make([]uint, len(existing.Kandangs)) for i, k := range existing.Kandangs { ids[i] = k.Id } if err := tx.Model(&entity.Kandang{}). Where("id IN ?", ids). Updates(map[string]any{"project_flock_id": nil}).Error; err != nil { tx.Rollback() s.Log.Errorf("Failed to detach kandangs before delete: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") } } if err := repository.NewProjectflockRepository(tx).DeleteOne(c.Context(), id); err != nil { tx.Rollback() if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } s.Log.Errorf("Failed to delete projectflock: %+v", err) return err } if err := tx.Commit().Error; err != nil { tx.Rollback() return fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction") } return nil } func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) { flock, err := s.FlockRepo.GetByID(c.Context(), flockID, func(db *gorm.DB) *gorm.DB { return db.Preload("CreatedUser") }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Flock not found") } if err != nil { s.Log.Errorf("Failed get flock %d for period summary: %+v", flockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock") } maxPeriod, err := s.Repository.GetMaxPeriodByFlock(c.Context(), flockID) if err != nil { s.Log.Errorf("Failed to compute next period for flock %d: %+v", flockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute next period") } return &FlockPeriodSummary{ Flock: *flock, NextPeriod: maxPeriod + 1, }, nil } func uniqueUintSlice(values []uint) []uint { seen := make(map[uint]struct{}, len(values)) result := make([]uint, 0, len(values)) for _, v := range values { if _, ok := seen[v]; ok { continue } seen[v] = struct{}{} result = append(result, v) } return result } func relationExistsChecker[T any](db *gorm.DB) func(context.Context, uint) (bool, error) { return func(ctx context.Context, id uint) (bool, error) { return commonRepo.Exists[T](ctx, db, id) } }