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" nonstockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/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" recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories" purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" 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) 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) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (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) Resubmit(ctx *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error) } type projectflockService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.ProjectflockRepository FlockRepo flockRepository.FlockRepository KandangRepo kandangRepository.KandangRepository NonstockRepo nonstockRepository.NonstockRepository WarehouseRepo warehouseRepository.WarehouseRepository ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository ProjectBudgetRepo projectBudgetRepository.ProjectBudgetRepository PivotRepo repository.ProjectFlockKandangRepository PopulationRepo repository.ProjectFlockPopulationRepository RecordingRepo recordingRepo.RecordingRepository 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, nonstockRepo nonstockRepository.NonstockRepository, populationRepo repository.ProjectFlockPopulationRepository, recordingRepo recordingRepo.RecordingRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate, ) ProjectflockService { return &projectflockService{ Log: utils.Log, Validate: validate, Repository: repo, FlockRepo: flockRepo, KandangRepo: kandangRepo, NonstockRepo: nonstockRepo, WarehouseRepo: warehouseRepo, ProductWarehouseRepo: productWarehouseRepo, ProjectBudgetRepo: projectBudgetRepo, PivotRepo: pivotRepo, PopulationRepo: populationRepo, RecordingRepo: recordingRepo, 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: "Production Standard", ID: &req.ProductionStandardId, Exists: s.Repository.ProductionStandardExists}, commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists}, ); err != nil { return nil, err } var location entity.Location if err := s.Repository.DB().WithContext(c.Context()). Where("id = ? AND area_id = ?", req.LocationId, req.AreaId). First(&location).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusBadRequest, "Lokasi tidak berada pada area yang diminta") } return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi relasi area-lokasi") } 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, ProductionStandardId: req.ProductionStandardId, 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) 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) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error) { if s.PopulationRepo == nil { return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured") } if projectFlockKandangID == 0 { return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required") } if s.RecordingRepo != nil { latest, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx.Context(), projectFlockKandangID) if err != nil { s.Log.Errorf("Failed to fetch latest recording for project flock kandang %d: %+v", projectFlockKandangID, err) return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population") } if latest != nil && latest.TotalChickQty != nil && *latest.TotalChickQty > 0 { return *latest.TotalChickQty, nil } } total, err := s.PopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), projectFlockKandangID) if err != nil { s.Log.Errorf("Failed to fetch project flock kandang population %d: %+v", projectFlockKandangID, err) return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population") } return total, 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) 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) projectFlockKandangRepoTx := repository.NewProjectFlockKandangRepository(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 } pfks, err := projectFlockKandangRepoTx.GetByProjectFlockID(c.Context(), approvableID) if err != nil { return err } for _, pfk := range pfks { latest, lerr := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, pfk.Id, nil) if lerr != nil { return lerr } if latest != nil && latest.StepNumber == uint16(utils.ProjectFlockKandangStepDisetujui) { continue } if _, aerr := approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowProjectFlockKandang, pfk.Id, utils.ProjectFlockKandangStepDisetujui, &action, actorID, req.Notes, ); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) { return aerr } } 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) 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") } if err := s.ensureProjectFlockKandangProductWarehouses(ctx, dbTransaction, records); err != nil { return err } 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 } // NOTE: Recording constraints are enforced via FK cascade; allow detachment even if recordings exist. pfkIDs, err := s.pivotRepoWithTx(dbTransaction).ListIDsByProjectAndKandang(ctx, projectFlockID, kandangIDs) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandang ids") } if len(pfkIDs) > 0 { uniformityRepo := uniformityRepository.NewUniformityRepository(s.Repository.DB()) if dbTransaction != nil { uniformityRepo = uniformityRepository.NewUniformityRepository(dbTransaction) } if err := uniformityRepo.DeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove uniformity data for project flock kandang") } db := s.Repository.DB() if dbTransaction != nil { db = dbTransaction } purchaseRepo := purchaseRepository.NewPurchaseRepository(db) if err := purchaseRepo.SoftDeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to soft delete purchases for project flock kandang") } pwRepo := s.ProductWarehouseRepo if dbTransaction != nil { pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction) } else if pwRepo == nil { pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB()) } if err := pwRepo.DeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove product warehouses for project flock kandang") } } 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) ensureProjectFlockKandangProductWarehouses(ctx context.Context, dbTransaction *gorm.DB, records []*entity.ProjectFlockKandang) error { if len(records) == 0 { return nil } projectFlockID := records[0].ProjectFlockId if projectFlockID == 0 { return fiber.NewError(fiber.StatusBadRequest, "Project flock id tidak ditemukan") } pwRepo := s.ProductWarehouseRepo if dbTransaction != nil { pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction) } else if pwRepo == nil { pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB()) } warehouseRepo := s.WarehouseRepo if dbTransaction != nil { warehouseRepo = warehouseRepository.NewWarehouseRepository(dbTransaction) } else if warehouseRepo == nil { warehouseRepo = warehouseRepository.NewWarehouseRepository(s.Repository.DB()) } db := s.Repository.DB() if dbTransaction != nil { db = dbTransaction } var category string if err := db.WithContext(ctx). Model(&entity.ProjectFlock{}). Select("category"). Where("id = ?", projectFlockID). Scan(&category).Error; err != nil { return err } if strings.TrimSpace(category) == "" { return fiber.NewError(fiber.StatusBadRequest, "Project flock category tidak ditemukan") } prefixes := []string{"AYAM-"} if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) { prefixes = append(prefixes, "TELUR") } invisibleOnly := false productIDs, err := pwRepo.ListProductIDsByFlagPrefixes(ctx, prefixes, &invisibleOnly) if err != nil { return err } if len(productIDs) == 0 { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product dengan flag %s tidak ditemukan", strings.Join(prefixes, ", "))) } for _, record := range records { if record == nil || record.Id == 0 { continue } warehouse, err := warehouseRepo.GetByKandangID(ctx, record.KandangId) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse untuk kandang %d belum tersedia", record.KandangId)) } return err } for _, productID := range productIDs { if _, err := pwRepo.GetByProductWarehouseAndProjectFlockKandang(ctx, productID, warehouse.Id, record.Id); err == nil { continue } else if !errors.Is(err, gorm.ErrRecordNotFound) { return err } newPW := entity.ProductWarehouse{ ProductId: productID, WarehouseId: warehouse.Id, ProjectFlockKandangId: &record.Id, Quantity: 0, } if err := pwRepo.CreateOne(ctx, &newPW, nil); err != nil { return err } } } return nil } func (s projectflockService) Resubmit(c *fiber.Ctx, req *validation.Resubmit, 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, "Project flock tidak ditemukan") } if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock") } 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, "Beberapa kandang tidak ditemukan") } return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data kandang") } if len(kandangs) != len(kandangIDs) { return nil, fiber.NewError(fiber.StatusNotFound, "Beberapa kandang tidak ditemukan") } for _, pb := range req.ProjectBudgets { if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Nonstock", ID: &pb.NonstockId, Exists: s.NonstockRepo.IdExists}, ); err != nil { return nil, err } } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) var period int = 1 if len(existing.KandangHistory) > 0 { period = existing.KandangHistory[0].Period } periods := make(map[uint]int, len(kandangIDs)) for _, kandangID := range kandangIDs { periods[kandangID] = period } if err := s.attachKandangs(c.Context(), dbTransaction, existing.Id, kandangIDs, periods); err != nil { return err } if err := s.UpsertProjectBudget(c.Context(), dbTransaction, existing.Id, req.ProjectBudgets); err != nil { return err } action := entity.ApprovalActionUpdated _, err = approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowProjectFlock, existing.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.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan") } return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengajukan ulang project flock") } return s.getOneEntityOnly(c, id) } 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) nonstockMap := make(map[uint]bool) relationChecks := make([]commonSvc.RelationCheck, 0, len(budgets)) for _, b := range budgets { if nonstockMap[b.NonstockId] { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate nonstock_id: %d", b.NonstockId)) } nonstockMap[b.NonstockId] = true nonstockID := b.NonstockId relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "Nonstock", ID: &nonstockID, Exists: s.NonstockRepo.IdExists, }) } if err := commonSvc.EnsureRelations(ctx, relationChecks...); err != nil { return err } 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 fiber.NewError(fiber.StatusInternalServerError, "Failed to save project budgets") } return nil }