package service import ( "context" "errors" "fmt" "strconv" "strings" "time" 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" transferLayingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/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) GetWarehouseByKandangID(ctx *fiber.Ctx, kandangID uint) (*entity.Warehouse, 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) GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, projectFlockKandangID uint) (*time.Time, error) GetProjectFlockKandangTransferState(ctx *fiber.Ctx, projectFlockKandangID uint) (bool, bool, error) GetProjectFlockKandangTransferStateAtDate(ctx *fiber.Ctx, projectFlockKandangID uint, referenceDate *time.Time) (bool, bool, 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) EnsureProjectFlockApproved(ctx context.Context, projectFlockID uint) 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 TransferLayingRepo transferLayingRepo.TransferLayingRepository 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, transferLayingRepo transferLayingRepo.TransferLayingRepository, 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, TransferLayingRepo: transferLayingRepo, 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) EnsureProjectFlockApproved(ctx context.Context, projectFlockID uint) error { if projectFlockID == 0 { return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid") } approvalSvc := s.ApprovalSvc if approvalSvc == nil { approvalSvc = commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB())) } latest, err := approvalSvc.LatestByTarget(ctx, s.approvalWorkflow, projectFlockID, nil) if err != nil { s.Log.Errorf("Failed to check project flock %d approval status: %+v", projectFlockID, err) return fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa status project flock") } if latest == nil { return fiber.NewError(fiber.StatusBadRequest, "Project flock masih dalam status pengajuan sehingga belum dapat membuat recording") } if latest.StepNumber != uint16(utils.ProjectFlockStepAktif) || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved { return fiber.NewError(fiber.StatusBadRequest, "Project flock masih dalam status pengajuan sehingga belum dapat membuat recording") } return nil } 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 } scope, err := m.ResolveLocationScope(c, s.Repository.DB()) if err != nil { return nil, 0, nil, err } if params.TransferContext == utils.TransferContextTransferToLaying { if m.HasPermission(c, m.P_TransferToLaying_CreateOne) || m.HasPermission(c, m.P_TransferToLaying_UpdateOne) { scope.Restrict = false scope.IDs = nil } } offset := (params.Page - 1) * params.Limit projectflocks, total, err := s.Repository.GetAllWithFiltersScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict) 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) { scope, err := m.ResolveLocationScope(c, s.Repository.DB()) if err != nil { return nil, nil, err } projectflock, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { db = s.Repository.WithDefaultRelations()(db) db = m.ApplyScopeFilter(db, scope, "project_flocks.location_id") return db }) 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: "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, 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") } 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") } if total > 0 { return total, nil } 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 } } return total, nil } func (s projectflockService) GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, projectFlockKandangID uint) (*time.Time, error) { if s.PopulationRepo == nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured") } if projectFlockKandangID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required") } populations, err := s.PopulationRepo.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID) if err != nil { s.Log.Errorf("Failed to fetch populations for project flock kandang %d: %+v", projectFlockKandangID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang chick in date") } var earliest *time.Time for _, pop := range populations { if pop.ProjectChickin == nil || pop.ProjectChickin.ChickInDate.IsZero() { continue } chickinDate := pop.ProjectChickin.ChickInDate if earliest == nil || chickinDate.Before(*earliest) { copy := chickinDate earliest = © } } return earliest, nil } func (s projectflockService) GetProjectFlockKandangTransferState(ctx *fiber.Ctx, projectFlockKandangID uint) (bool, bool, error) { return s.GetProjectFlockKandangTransferStateAtDate(ctx, projectFlockKandangID, nil) } func (s projectflockService) GetProjectFlockKandangTransferStateAtDate(ctx *fiber.Ctx, projectFlockKandangID uint, referenceDate *time.Time) (bool, bool, error) { if projectFlockKandangID == 0 || s.TransferLayingRepo == nil || s.PivotRepo == nil { return false, false, nil } pfk, err := s.PivotRepo.GetByIDLight(ctx.Context(), projectFlockKandangID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return false, false, nil } s.Log.Errorf("Failed to resolve project flock kandang %d for transfer state: %+v", projectFlockKandangID, err) return false, false, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve transfer state") } category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category)) var transfer *entity.LayingTransfer switch category { case strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)): transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx.Context(), projectFlockKandangID) case strings.ToUpper(string(utils.ProjectFlockCategoryLaying)): transfer, err = s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx.Context(), projectFlockKandangID) default: return false, false, nil } if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return false, false, nil } s.Log.Errorf("Failed to resolve transfer state for project flock kandang %d: %+v", projectFlockKandangID, err) return false, false, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve transfer state") } if transfer == nil { return false, false, nil } physicalMoveDate := normalizeDateOnlyUTC(transfer.TransferDate) if physicalMoveDate.IsZero() { return false, false, nil } economicCutoffDate := physicalMoveDate if transfer.EconomicCutoffDate != nil && !transfer.EconomicCutoffDate.IsZero() { economicCutoffDate = normalizeDateOnlyUTC(*transfer.EconomicCutoffDate) } else if transfer.EffectiveMoveDate != nil && !transfer.EffectiveMoveDate.IsZero() { economicCutoffDate = normalizeDateOnlyUTC(*transfer.EffectiveMoveDate) } if economicCutoffDate.Before(physicalMoveDate) { economicCutoffDate = physicalMoveDate } reference := normalizeDateOnlyUTC(time.Now().UTC()) if referenceDate != nil && !referenceDate.IsZero() { reference = normalizeDateOnlyUTC(referenceDate.UTC()) } isTransition := !reference.Before(physicalMoveDate) && reference.Before(economicCutoffDate) isLaying := !reference.Before(economicCutoffDate) return isTransition, isLaying, 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 normalizeDateOnlyUTC(value time.Time) time.Time { return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC) } func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) { if s.PopulationRepo == nil { return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured") } pfk, err := s.PivotRepo.GetActiveByKandangID(ctx.Context(), kandangID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") } return 0, err } total, err := s.PopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), pfk.Id) if err != nil { return 0, err } return total, nil } func (s projectflockService) GetWarehouseByKandangID(ctx *fiber.Ctx, kandangID uint) (*entity.Warehouse, error) { if kandangID == 0 || s.WarehouseRepo == nil { return nil, nil } var warehouse entity.Warehouse err := s.WarehouseRepo.DB().WithContext(ctx.Context()). Preload("Area"). Preload("Location"). Preload("Kandang"). Where("kandang_id = ?", kandangID). Where("deleted_at IS NULL"). Order("id DESC"). First(&warehouse).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } if err != nil { s.Log.Errorf("Failed to fetch warehouse for kandang %d: %+v", kandangID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouse") } return &warehouse, 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 resolveWarehouseByKandangAndLocation(ctx context.Context, warehouseRepo warehouseRepository.WarehouseRepository, kandangID uint, locationID uint) (*entity.Warehouse, error) { if warehouseRepo == nil { return nil, gorm.ErrRecordNotFound } if kandangID == 0 { return nil, gorm.ErrRecordNotFound } if locationID != 0 { warehouse, err := warehouseRepo.GetByKandangIDAndLocationID(ctx, kandangID, locationID) if err == nil { return warehouse, nil } if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } } return warehouseRepo.GetByKandangID(ctx, kandangID) } 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 } type projectFlockMeta struct { Category string `gorm:"column:category"` LocationId uint `gorm:"column:location_id"` } var flockMeta projectFlockMeta if err := db.WithContext(ctx). Model(&entity.ProjectFlock{}). Select("category, location_id"). Where("id = ?", projectFlockID). Scan(&flockMeta).Error; err != nil { return err } if strings.TrimSpace(flockMeta.Category) == "" { return fiber.NewError(fiber.StatusBadRequest, "Project flock category tidak ditemukan") } prefixes := []string{"AYAM-"} if strings.EqualFold(flockMeta.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 := resolveWarehouseByKandangAndLocation(ctx, warehouseRepo, record.KandangId, flockMeta.LocationId) 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 } } // Hitung newFlockName sebelum membuka transaksi (fast-path conflict check) var newFlockName string if req.Periode != nil { lastSpace := strings.LastIndex(existing.FlockName, " ") if lastSpace < 0 { return nil, fiber.NewError(fiber.StatusInternalServerError, "Format flock name tidak valid") } baseName := strings.TrimSpace(existing.FlockName[:lastSpace]) newFlockName = fmt.Sprintf("%s %03d", baseName, *req.Periode) taken, err := s.Repository.ExistsByFlockName(c.Context(), newFlockName, &id) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa nama flock") } if taken { return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Nama flock '%s' sudah digunakan oleh project flock lain", newFlockName)) } } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) var period int = 1 if req.Periode != nil { period = *req.Periode } else 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 } // Update period pada SEMUA row project_flock_kandangs milik flock ini. // attachKandangs hanya INSERT baris baru dan melewati yang sudah ada, // sehingga period pada baris lama tidak terupdate tanpa langkah ini. if req.Periode != nil { if err := dbTransaction.WithContext(c.Context()). Model(&entity.ProjectFlockKandang{}). Where("project_flock_id = ?", existing.Id). Update("period", *req.Periode).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui periode kandang") } } // Update flock_name sesuai periode baru. if req.Periode != nil { projectRepoTx := repository.NewProjectflockRepository(dbTransaction) // Re-check di dalam transaksi untuk cegah race condition. taken, err := projectRepoTx.ExistsByFlockName(c.Context(), newFlockName, &existing.Id) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa nama flock") } if taken { return fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Nama flock '%s' sudah digunakan oleh project flock lain", newFlockName)) } if err := projectRepoTx.PatchOne(c.Context(), existing.Id, map[string]any{ "flock_name": newFlockName, }, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui nama flock") } } 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 }