package service import ( "context" "errors" "fmt" "strings" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "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" utils "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" "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) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) } type projectflockService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.ProjectflockRepository FlockRepo flockRepository.FlockRepository KandangRepo kandangRepository.KandangRepository PivotRepo repository.ProjectFlockKandangRepository ApprovalSvc commonSvc.ApprovalService approvalWorkflow approvalutils.ApprovalWorkflowKey } type FlockPeriodSummary struct { Flock entity.Flock NextPeriod int } func NewProjectflockService( repo repository.ProjectflockRepository, flockRepo flockRepository.FlockRepository, kandangRepo kandangRepository.KandangRepository, pivotRepo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate, ) ProjectflockService { return &projectflockService{ Log: utils.Log, Validate: validate, Repository: repo, FlockRepo: flockRepo, KandangRepo: kandangRepo, PivotRepo: pivotRepo, ApprovalSvc: approvalSvc, approvalWorkflow: utils.ApprovalWorkflowProjectFlock, } } func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). Preload("Flock"). Preload("Area"). 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 } if params.Page <= 0 { params.Page = 1 } if params.Limit <= 0 { params.Limit = 10 } 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) if params.AreaId > 0 { db = db.Where("project_flocks.area_id = ?", params.AreaId) } if params.LocationId > 0 { db = db.Where("project_flocks.location_id = ?", params.LocationId) } if params.Period > 0 { db = db.Where("project_flocks.period = ?", params.Period) } if len(params.KandangIds) > 0 { db = db.Where("EXISTS (SELECT 1 FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id AND kandangs.id IN ?)", params.KandangIds) } if params.Search != "" { normalizedSearch := strings.ToLower(strings.TrimSpace(params.Search)) if normalizedSearch == "" { for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) { db = db.Order(expr) } return db } likeQuery := "%" + normalizedSearch + "%" db = db. Joins("LEFT JOIN flocks ON flocks.id = project_flocks.flock_id"). Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id"). Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id"). Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id"). Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by"). Where(` LOWER(flocks.name) LIKE ? OR LOWER(areas.name) LIKE ? OR LOWER(project_flocks.category) LIKE ? OR LOWER(fcrs.name) LIKE ? OR LOWER(locations.name) LIKE ? OR LOWER(locations.address) LIKE ? OR LOWER(created_users.name) LIKE ? OR LOWER(created_users.email) LIKE ? OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ? OR EXISTS ( SELECT 1 FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id AND LOWER(kandangs.name) LIKE ? ) `, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, ) } for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) { db = db.Order(expr) } return db }) if err != nil { s.Log.Errorf("Failed to get projectflocks: %+v", err) return nil, 0, err } if s.ApprovalSvc != nil && len(projectflocks) > 0 { ids := make([]uint, len(projectflocks)) for i, item := range projectflocks { ids[i] = item.Id } latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) if err != nil { s.Log.Warnf("Unable to load latest approvals for projectflocks: %+v", err) } else if len(latestMap) > 0 { for i := range projectflocks { if approval, ok := latestMap[projectflocks[i].Id]; ok { projectflocks[i].LatestApproval = approval } } } } 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 } if s.ApprovalSvc != nil { approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) if err != nil { s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err) } else if len(approvals) > 0 { if projectflock.LatestApproval == nil { latest := approvals[len(approvals)-1] projectflock.LatestApproval = &latest } } else { projectflock.LatestApproval = nil } } 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 } cat := strings.ToUpper(req.Category) if !utils.IsValidProjectFlockCategory(cat) { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category") } if len(req.KandangIds) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required") } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())}, commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())}, commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())}, commonSvc.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)) } } createBody := &entity.ProjectFlock{ FlockId: req.FlockId, AreaId: req.AreaId, Category: cat, FcrId: req.FcrId, LocationId: req.LocationId, CreatedBy: 1, } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { projectRepo := repository.NewProjectflockRepository(dbTransaction) period, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId) if err != nil { return err } createBody.Period = period if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { return err } if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs); err != nil { return err } actorID := uint(1) //TODO: Change From Auth action := entity.ApprovalActionCreated approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) _, err = approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowProjectFlock, createBody.Id, utils.ProjectFlockStepPengajuan, &action, actorID, nil, ) return err }) if err != nil { 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 } 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) hasBodyChanges := false var relationChecks []commonSvc.RelationCheck if req.FlockId != nil { updateBody["flock_id"] = *req.FlockId hasBodyChanges = true relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "Flock", ID: req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB()), }) } if req.AreaId != nil { updateBody["area_id"] = *req.AreaId hasBodyChanges = true relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "Area", ID: req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB()), }) } if req.Category != nil { cat := strings.ToUpper(*req.Category) if !utils.IsValidProjectFlockCategory(cat) { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category") } updateBody["category"] = cat } if req.FcrId != nil { updateBody["fcr_id"] = *req.FcrId hasBodyChanges = true relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "FCR", ID: req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()), }) } if req.LocationId != nil { updateBody["location_id"] = *req.LocationId hasBodyChanges = true relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "Location", ID: req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB()), }) } if len(relationChecks) > 0 { if err := commonSvc.EnsureRelations(c.Context(), relationChecks...); err != nil { return nil, err } } var newKandangIDs []uint hasKandangChanges := false if req.KandangIds != nil { hasKandangChanges = true 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)) } } } hasChanges := hasBodyChanges || hasKandangChanges if !hasChanges { return s.GetOne(c, id) } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { projectRepo := repository.NewProjectflockRepository(dbTransaction) if len(updateBody) > 0 { if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil { return err } } else { if _, err := projectRepo.GetByID(c.Context(), id, nil); err != nil { return 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 _, kid := range newKandangIDs { newSet[kid] = struct{}{} } var toDetach []uint for kid := range existingIDs { if _, ok := newSet[kid]; !ok { toDetach = append(toDetach, kid) } } var toAttach []uint for kid := range newSet { if _, ok := existingIDs[kid]; !ok { toAttach = append(toAttach, kid) } } if len(toDetach) > 0 { if err := s.detachKandangs(c.Context(), dbTransaction, id, toDetach, true); err != nil { return err } } if len(toAttach) > 0 { if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach); err != nil { return err } } } if hasChanges { actorID := uint(1) //TODO: Change From Auth approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) if approvalSvc != nil { latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil) if err != nil { return err } shouldRecordUpdate := latestBeforeReset == nil || latestBeforeReset.StepNumber != uint16(utils.ProjectFlockStepPengajuan) || latestBeforeReset.Action == nil || (latestBeforeReset.Action != nil && *latestBeforeReset.Action != entity.ApprovalActionUpdated) if shouldRecordUpdate { action := entity.ApprovalActionUpdated if _, err := approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowProjectFlock, id, utils.ProjectFlockStepPengajuan, &action, actorID, nil, ); err != nil { return err } } } } return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } s.Log.Errorf("Failed to update projectflock %d: %+v", id, err) return nil, err } return s.GetOne(c, id) } func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } actorID := uint(1) // TODO: change from auth context var action entity.ApprovalAction switch strings.ToUpper(strings.TrimSpace(req.Action)) { case string(entity.ApprovalActionRejected): action = entity.ApprovalActionRejected case string(entity.ApprovalActionApproved): action = entity.ApprovalActionApproved default: return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") } approvableIDs := uniqueUintSlice(req.ApprovableIds) if len(approvableIDs) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") } step := utils.ProjectFlockStepPengajuan if action == entity.ApprovalActionApproved { step = utils.ProjectFlockStepAktif } err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction) projectRepoTx := repository.NewProjectflockRepository(dbTransaction) for _, approvableID := range approvableIDs { if _, err := projectRepoTx.GetByID(c.Context(), approvableID, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Projectflock %d not found", approvableID)) } return err } if _, err := approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowProjectFlock, approvableID, step, &action, actorID, req.Notes, ); err != nil { return err } switch action { case entity.ApprovalActionApproved: if err := kandangRepoTx.UpdateStatusByProjectFlockID( c.Context(), approvableID, utils.KandangStatusActive, ); err != nil { return err } case entity.ApprovalActionRejected: if err := kandangRepoTx.UpdateStatusByProjectFlockID( c.Context(), approvableID, utils.KandangStatusNonActive, ); err != nil { return err } } } return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } s.Log.Errorf("Failed to record approval for projectflocks %+v: %+v", approvableIDs, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } updated := make([]entity.ProjectFlock, 0, len(approvableIDs)) for _, approvableID := range approvableIDs { project, err := s.GetOne(c, approvableID) if err != nil { return nil, err } updated = append(updated, *project) } return updated, nil } 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") } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { if len(existing.Kandangs) > 0 { ids := make([]uint, len(existing.Kandangs)) for i, k := range existing.Kandangs { ids[i] = k.Id } if err := s.detachKandangs(c.Context(), dbTransaction, id, ids, true); err != nil { return err } } if err := repository.NewProjectflockRepository(dbTransaction).DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } return err } return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return fiberErr } s.Log.Errorf("Failed to delete projectflock %d: %+v", id, err) return err } 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) } } func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []string { direction := "ASC" if strings.ToLower(sortOrder) == "desc" { direction = "DESC" } switch sortBy { case "area": return []string{ fmt.Sprintf("(SELECT name FROM areas WHERE areas.id = project_flocks.area_id) %s", direction), fmt.Sprintf("project_flocks.id %s", direction), } case "location": return []string{ fmt.Sprintf("(SELECT name FROM locations WHERE locations.id = project_flocks.location_id) %s", direction), fmt.Sprintf("project_flocks.id %s", direction), } case "kandangs": return []string{ fmt.Sprintf("(SELECT COUNT(*) FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id) %s", direction), fmt.Sprintf("project_flocks.id %s", direction), } case "period": return []string{ fmt.Sprintf("project_flocks.period %s", direction), fmt.Sprintf("project_flocks.id %s", direction), } default: return []string{ "project_flocks.created_at DESC", "project_flocks.updated_at DESC", } } } func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint) error { if len(kandangIDs) == 0 { return nil } if err := dbTransaction.Model(&entity.Kandang{}). Where("id IN ?", kandangIDs). Updates(map[string]any{ "project_flock_id": projectFlockID, "status": string(utils.KandangStatusPengajuan), }).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") } pivotRepo := s.pivotRepoWithTx(dbTransaction) records := make([]*entity.ProjectFlockKandang, len(kandangIDs)) for i, id := range kandangIDs { records[i] = &entity.ProjectFlockKandang{ ProjectFlockId: projectFlockID, KandangId: id, } } if err := pivotRepo.CreateMany(ctx, records); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history") } return nil } func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, resetStatus bool) error { if len(kandangIDs) == 0 { return nil } updates := map[string]any{"project_flock_id": nil} if resetStatus { updates["status"] = string(utils.KandangStatusNonActive) } if err := dbTransaction.Model(&entity.Kandang{}). Where("id IN ?", kandangIDs). Updates(updates).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") } if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history") } return nil } func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository { if s.PivotRepo == nil { return repository.NewProjectFlockKandangRepository(dbTransaction) } return s.PivotRepo.WithTx(dbTransaction) }