package service import ( "context" "errors" "fmt" "strconv" "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" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" projectBudgetRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils" 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, map[uint]*flockDTO.FlockRelationDTO, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) DeleteOne(ctx *fiber.Ctx, id uint) error GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, 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 WarehouseRepo warehouseRepository.WarehouseRepository ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository ProjectBudgetRepo projectBudgetRepository.ProjectBudgetRepository PivotRepo repository.ProjectFlockKandangRepository ApprovalSvc commonSvc.ApprovalService approvalWorkflow approvalutils.ApprovalWorkflowKey } type KandangPeriodSummary struct { Id uint Name string Period int } func NewProjectflockService( repo repository.ProjectflockRepository, flockRepo flockRepository.FlockRepository, kandangRepo kandangRepository.KandangRepository, pivotRepo repository.ProjectFlockKandangRepository, warehouseRepo warehouseRepository.WarehouseRepository, productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository, projectBudgetRepo projectBudgetRepository.ProjectBudgetRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate, ) ProjectflockService { return &projectflockService{ Log: utils.Log, Validate: validate, Repository: repo, FlockRepo: flockRepo, KandangRepo: kandangRepo, WarehouseRepo: warehouseRepo, ProductWarehouseRepo: productWarehouseRepo, PivotRepo: pivotRepo, ApprovalSvc: approvalSvc, approvalWorkflow: utils.ApprovalWorkflowProjectFlock, } } func (s projectflockService) approvalQueryModifier() func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") } } func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, nil, err } offset := (params.Page - 1) * params.Limit projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) if err != nil { s.Log.Errorf("Failed to get projectflocks: %+v", err) return nil, 0, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flocks") } 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, s.approvalQueryModifier()) 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 } } } } flockMap := make(map[uint]*flockDTO.FlockRelationDTO) for i := range projectflocks { if projectflocks[i].FlockName != "" { baseName := pfutils.DeriveBaseName(projectflocks[i].FlockName) if baseName != "" { flock, err := s.FlockRepo.GetByName(c.Context(), baseName) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) } else if flock != nil { flockMap[projectflocks[i].Id] = &flockDTO.FlockRelationDTO{ Id: flock.Id, Name: flock.Name, } } } } } return projectflocks, total, flockMap, nil } func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) 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, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } if s.ApprovalSvc != nil { approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier()) 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) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) { projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } if err != nil { s.Log.Errorf("Failed get projectflock by id: %+v", err) return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } if s.ApprovalSvc != nil { approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier()) 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 } } // Fetch Flock master data for this ProjectFlock var flockResult *flockDTO.FlockRelationDTO if projectflock.FlockName != "" { baseName := pfutils.DeriveBaseName(projectflock.FlockName) if baseName != "" { flock, err := s.FlockRepo.GetByName(c.Context(), baseName) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) } else if flock != nil { flockResult = &flockDTO.FlockRelationDTO{ Id: flock.Id, Name: flock.Name, } } } } return projectflock, flockResult, 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 } actorID, err := m.ActorIDFromContext(c) if 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") } baseName := strings.TrimSpace(req.FlockName) if baseName == "" { return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty") } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists}, commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: s.Repository.FcrExists}, commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists}, ); err != nil { return nil, err } canonicalBase := baseName if s.FlockRepo != nil { baseFlock, err := s.ensureFlockByName(c.Context(), actorID, baseName) if err != nil { return nil, err } canonicalBase = baseFlock.Name } 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.LocationId != req.LocationId { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id)) } } // larang kalau ada yg sudah terikat ke project lain if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), kandangIDs, nil); err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage") } else if linked { return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain") } createBody := &entity.ProjectFlock{ AreaId: req.AreaId, Category: cat, FcrId: req.FcrId, LocationId: req.LocationId, CreatedBy: actorID, } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { projectRepo := repository.NewProjectflockRepository(dbTransaction) generatedName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, 1, nil) if err != nil { return err } createBody.FlockName = generatedName if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { return err } periods, err := projectRepo.GetNextPeriodsForKandangs(c.Context(), kandangIDs) if err != nil { return err } if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs, periods); err != nil { return err } if err := s.UpsertProjectBudget(c.Context(), dbTransaction, createBody.Id, req.ProjectBudgets); err != nil { return err } 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 fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } 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, fiber.NewError(fiber.StatusInternalServerError, "Failed to create project flock") } return s.getOneEntityOnly(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 } actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) 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 existingBase := pfutils.DeriveBaseName(existing.FlockName) targetBaseName := existingBase needFlockNameRegenerate := false if req.FlockName != nil { trimmed := strings.TrimSpace(*req.FlockName) if trimmed == "" { return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty") } canonicalBase := trimmed if s.FlockRepo != nil { flockEntity, err := s.ensureFlockByName(c.Context(), actorID, trimmed) if err != nil { return nil, err } canonicalBase = flockEntity.Name } if !strings.EqualFold(canonicalBase, existingBase) { needFlockNameRegenerate = true targetBaseName = canonicalBase hasBodyChanges = true } } if req.AreaId != nil { updateBody["area_id"] = *req.AreaId hasBodyChanges = true relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "Area", ID: req.AreaId, Exists: s.Repository.AreaExists, }) } 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: s.Repository.FcrExists, }) } if req.LocationId != nil { updateBody["location_id"] = *req.LocationId hasBodyChanges = true relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "Location", ID: req.LocationId, Exists: s.Repository.LocationExists, }) } 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") } targetLocationID := existing.LocationId if req.LocationId != nil && *req.LocationId > 0 { targetLocationID = *req.LocationId } for _, kandang := range kandangs { if kandang.LocationId != targetLocationID { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id)) } } if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage") } else if linked { return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain") } } hasChanges := hasBodyChanges || hasKandangChanges if !hasChanges { return s.getOneEntityOnly(c, id) } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { projectRepo := repository.NewProjectflockRepository(dbTransaction) baseForGeneration := targetBaseName if strings.TrimSpace(baseForGeneration) == "" { baseForGeneration = existingBase } if strings.TrimSpace(baseForGeneration) == "" { baseForGeneration = strings.TrimSpace(existing.FlockName) } if needFlockNameRegenerate { newName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, 1, &id) if err != nil { return err } updateBody["flock_name"] = newName } 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 { currentPeriod, err := projectRepo.GetCurrentProjectPeriod(c.Context(), id) if err != nil { return err } periods := make(map[uint]int, len(toAttach)) if currentPeriod > 0 { for _, kid := range toAttach { periods[kid] = currentPeriod } } else { periods, err = projectRepo.GetNextPeriodsForKandangs(c.Context(), toAttach) if err != nil { return err } } if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach, periods); err != nil { return err } } } if hasChanges { 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) if errors.Is(err, gorm.ErrDuplicatedKey) { return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists") } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock") } return s.getOneEntityOnly(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, err := m.ActorIDFromContext(c) if err != nil { return nil, err } 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.getOneEntityOnly(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.Repository.WithDefaultRelations()) 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 fiber.NewError(fiber.StatusInternalServerError, "Failed to delete project flock") } return nil } func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) { pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") } s.Log.Errorf("Failed to fetch project_flock_kandang by project %d and kandang %d: %+v", projectFlockID, kandangID, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang") } availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId) if err != nil { return nil, 0, err } return pfk, availableQuantity, nil } func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) { idStr = strings.TrimSpace(idStr) projectFlockIdStr = strings.TrimSpace(projectFlockIdStr) kandangIdStr = strings.TrimSpace(kandangIdStr) if idStr != "" { id, err := strconv.Atoi(idStr) if err != nil || id <= 0 { return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } pfk, err := s.PivotRepo.GetByID(ctx.Context(), uint(id)) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") } s.Log.Errorf("Failed to fetch project_flock_kandang %d: %+v", id, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang") } availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId) if err != nil { return nil, 0, err } return pfk, availableQuantity, nil } if projectFlockIdStr == "" || kandangIdStr == "" { return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters") } pfid, err := strconv.Atoi(projectFlockIdStr) if err != nil || pfid <= 0 { return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") } kid, err := strconv.Atoi(kandangIdStr) if err != nil || kid <= 0 { return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id") } return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid)) } func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) { wh, err := s.WarehouseRepo.GetByKandangID(ctx.Context(), kandangID) if err != nil { return 0, err } productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), "DOC", wh.Id) if err != nil { return 0, err } total := 0.0 for _, pw := range productWarehouses { total += pw.Quantity } return total, nil } func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) { if len(projectIDs) == 0 { return map[uint]int{}, nil } return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs) } func (s projectflockService) GetPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) { if locationID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required") } exists, err := s.Repository.LocationExists(c.Context(), locationID) if err != nil { s.Log.Errorf("Failed to validate location %d: %+v", locationID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate location") } if !exists { return nil, fiber.NewError(fiber.StatusNotFound, "Location not found") } rows, err := s.Repository.GetKandangPeriodSummaryRows(c.Context(), locationID) if err != nil { s.Log.Errorf("Failed to fetch kandang period summary for location %d: %+v", locationID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandang period summary") } summaries := make([]KandangPeriodSummary, 0, len(rows)) for _, row := range rows { nextPeriod := 1 if row.LatestPeriod > 0 { nextPeriod = row.LatestPeriod + 1 } summaries = append(summaries, KandangPeriodSummary{ Id: row.Id, Name: row.Name, Period: nextPeriod, }) } return summaries, 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 (s projectflockService) generateSequentialFlockName(ctx context.Context, repo repository.ProjectflockRepository, baseName string, startNumber int, excludeID *uint) (string, int, error) { name := strings.TrimSpace(baseName) if name == "" { return "", 0, fiber.NewError(fiber.StatusBadRequest, "Base flock name cannot be empty") } number := startNumber if number <= 0 { number = 1 } attempts := 0 for { candidate := fmt.Sprintf("%s %03d", name, number) exists, err := repo.ExistsByFlockName(ctx, candidate, excludeID) if err != nil { s.Log.Errorf("Failed checking project flock name uniqueness for %q: %+v", candidate, err) return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate flock name") } if !exists { return candidate, number, nil } number++ attempts++ if attempts > 9999 { return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Unable to generate unique flock name") } } } func (s projectflockService) ensureFlockByName(ctx context.Context, actorID uint, name string) (*entity.Flock, error) { trimmed := strings.TrimSpace(name) if trimmed == "" { return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty") } flock, err := s.FlockRepo.GetByName(ctx, trimmed) if err == nil { return flock, nil } if !errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Errorf("Failed to fetch flock by name %q: %+v", trimmed, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data") } newFlock := &entity.Flock{ Name: trimmed, CreatedBy: actorID, } if err := s.FlockRepo.CreateOne(ctx, newFlock, nil); err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return s.FlockRepo.GetByName(ctx, trimmed) } s.Log.Errorf("Failed to create flock %q: %+v", trimmed, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data") } return newFlock, nil } func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, periods map[uint]int) error { if len(kandangIDs) == 0 { return nil } if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusPengajuan); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status") } already, err := s.pivotRepoWithTx(dbTransaction).ListExistingKandangIDs(ctx, projectFlockID, kandangIDs) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing pivot") } exists := make(map[uint]struct{}, len(already)) for _, id := range already { exists[id] = struct{}{} } var toAttach []uint seen := make(map[uint]struct{}, len(kandangIDs)) for _, id := range kandangIDs { if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} if _, ok := exists[id]; !ok { toAttach = append(toAttach, id) } } if len(toAttach) == 0 { return nil } records := make([]*entity.ProjectFlockKandang, 0, len(toAttach)) for _, id := range toAttach { period := periods[id] if period <= 0 { period = 1 } records = append(records, &entity.ProjectFlockKandang{ ProjectFlockId: projectFlockID, KandangId: id, Period: period, }) } if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terhubung dengan project flock ini") } 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 } blocked, err := s.pivotRepoWithTx(dbTransaction).FindKandangsWithRecordings(ctx, projectFlockID, kandangIDs) if err != nil { s.Log.Errorf("Failed to check recordings before detaching kandangs: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandang detachment") } if len(blocked) > 0 { names := make([]string, 0, len(blocked)) for _, item := range blocked { label := fmt.Sprintf("ID %d", item.Id) if strings.TrimSpace(item.Name) != "" { label = fmt.Sprintf("%s (%s)", label, item.Name) } names = append(names, label) } return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", "))) } if resetStatus { if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status") } } 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 dbTransaction == nil { return s.pivotRepo() } return s.pivotRepo().WithTx(dbTransaction) } func (s projectflockService) pivotRepo() repository.ProjectFlockKandangRepository { if s.PivotRepo != nil { return s.PivotRepo } return repository.NewProjectFlockKandangRepository(s.Repository.DB()) } func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.KandangRepository { if tx != nil { return kandangRepository.NewKandangRepository(tx) } if s.KandangRepo != nil { return s.KandangRepo } return kandangRepository.NewKandangRepository(s.Repository.DB()) } func (s projectflockService) UpsertProjectBudget(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, budgets []validation.ProjectBudget) error { if len(budgets) == 0 { return nil } budgetRepo := projectBudgetRepository.NewProjectBudgetRepository(dbTransaction) if err := budgetRepo.DeleteMany(ctx, func(q *gorm.DB) *gorm.DB { return q.Where("project_flock_id = ?", projectFlockID) }); err != nil && err != gorm.ErrRecordNotFound { return err } records := make([]*entity.ProjectBudget, 0, len(budgets)) for _, b := range budgets { records = append(records, &entity.ProjectBudget{ ProjectFlockId: projectFlockID, NonstockId: b.NonstockId, Price: b.Price, Qty: b.Qty, }) } if err := budgetRepo.CreateMany(ctx, records, nil); err != nil { return err } return nil }