package service import ( "context" "encoding/json" "errors" "fmt" "math" "strconv" "strings" "time" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" productionStandardRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories" chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" "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 ClosingService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error) GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error) GetPenjualan(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error) GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) } type closingService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.ClosingRepository ProjectFlockRepo projectflockRepository.ProjectflockRepository ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository MarketingRepo marketingRepository.MarketingRepository MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository ApprovalSvc commonSvc.ApprovalService ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ChickinRepo chickinRepository.ProjectChickinRepository PurchaseRepo purchaseRepository.PurchaseRepository RecordingRepo recordingRepository.RecordingRepository StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository } func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService { return &closingService{ Log: utils.Log, Validate: validate, Repository: repo, ProjectFlockRepo: projectFlockRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, MarketingRepo: marketingRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, ApprovalSvc: approvalSvc, ExpenseRealizationRepo: expenseRealizationRepo, ProjectBudgetRepo: projectBudgetRepo, ChickinRepo: chickinRepo, PurchaseRepo: purchaseRepo, RecordingRepo: recordingRepo, StandardGrowthDetailRepo: standardGrowthDetailRepo, ProductionStandardDetailRepo: productionStandardDetailRepo, } } func (s closingService) withRelations(db *gorm.DB) *gorm.DB { return db.Preload("CreatedUser") } func (s closingService) withClosingRelations(db *gorm.DB) *gorm.DB { return s.withRelations(db). Preload("Location"). Preload("KandangHistory"). Preload("KandangHistory.Chickins") } func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withClosingRelations(db) if params.Search != "" { return db.Where("flock_name ILIKE ?", "%"+params.Search+"%") } return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { s.Log.Errorf("Failed to get closings: %+v", err) return nil, 0, err } result := make([]dto.ClosingListItemDTO, 0, len(closings)) for _, closing := range closings { statusProject, _, err := s.getApprovalStatuses(c.Context(), closing.Id) if err != nil { s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", closing.Id, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status") } result = append(result, dto.ToClosingListItemDTO(closing, statusProject)) } return result, total, nil } func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found") } if err != nil { return nil, err } return projectFlock, nil } func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) { realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID) if err != nil { return nil, err } if len(realisasi) == 0 { return []entity.MarketingDeliveryProduct{}, nil } return realisasi, nil } func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error) { if projectFlockID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } if kandangID != nil { return s.getClosingSummaryByKandang(c.Context(), projectFlockID, *kandangID) } project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") } if err != nil { s.Log.Errorf("Failed get project flock %d for closing summary: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } statusProject, statusClosing, err := s.getApprovalStatuses(c.Context(), projectFlockID) if err != nil { s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status") } summary := dto.ToClosingSummaryDTO(*project, statusProject, statusClosing) return &summary, nil } func (s closingService) getClosingSummaryByKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*dto.ClosingSummaryKandangDTO, error) { if projectFlockID == 0 || kandangID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id or kandang id") } db := s.Repository.DB().WithContext(ctx) var kandang entity.ProjectFlockKandang if err := db. Preload("Kandang"). Preload("Kandang.Location"). Preload("Kandang.Pic"). Where("project_flock_id = ?", projectFlockID). Where("kandang_id = ?", kandangID). First(&kandang).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") } s.Log.Errorf("Failed get project flock kandang %d/%d: %+v", projectFlockID, kandangID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang") } var project entity.ProjectFlock if err := db. Select("id", "category"). First(&project, projectFlockID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") } s.Log.Errorf("Failed get project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } var population float64 if err := db. Table("project_flock_populations pfp"). Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id"). Where("pc.project_flock_kandang_id = ?", kandang.Id). Select("COALESCE(SUM(pfp.total_qty), 0)"). Scan(&population).Error; err != nil { s.Log.Errorf("Failed to sum population for project flock kandang %d: %+v", kandang.Id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data") } var chickInDate time.Time if err := db. Table("project_chickins"). Where("project_flock_kandang_id = ?", kandang.Id). Select("MIN(chick_in_date)"). Scan(&chickInDate).Error; err != nil { s.Log.Errorf("Failed to fetch chick in date for project flock kandang %d: %+v", kandang.Id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chick in date") } statusProject := "Belum Selesai" var approvalDate string if s.ApprovalSvc != nil { records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlockKandang.String(), &kandang.Id, 1, 1000, "") if err != nil { s.Log.Errorf("Failed to fetch approvals for project flock kandang %d: %+v", kandang.Id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval data") } var ( minStep uint16 latestActionAt time.Time ) for _, rec := range records { if minStep == 0 || rec.StepNumber < minStep { minStep = rec.StepNumber } if latestActionAt.IsZero() || rec.ActionAt.After(latestActionAt) { latestActionAt = rec.ActionAt statusProject = rec.StepName } } if statusProject == "" && minStep > 0 { if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlockKandang, approvalutils.ApprovalStep(minStep)); ok { statusProject = label } } if !latestActionAt.IsZero() { approvalDate = latestActionAt.Format("2006-01-02") } } closingDate := "" if kandang.ClosedAt != nil { closingDate = kandang.ClosedAt.Format("2006-01-02") } chickInDateStr := "" if !chickInDate.IsZero() { chickInDateStr = chickInDate.Format("2006-01-02") } populationInt := int(population) return &dto.ClosingSummaryKandangDTO{ FlockID: projectFlockID, Period: kandang.Period, LocationName: kandang.Kandang.Location.Name, Population: populationInt, PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt), ProjectType: project.Category, ClosingDate: closingDate, KandangName: kandang.Kandang.Name, ChickInDate: chickInDateStr, PicName: kandang.Kandang.Pic.Name, ApprovalDate: approvalDate, ProjectStatus: statusProject, }, nil } func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) { if projectFlockID == 0 { return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } if params == nil { params = &validation.ClosingSapronakQuery{} } if params.Page == 0 { params.Page = 1 } if params.Limit == 0 { params.Limit = 10 } if err := s.Validate.Struct(params); err != nil { return nil, 0, fiber.NewError(fiber.StatusBadRequest, err.Error()) } if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing { return nil, 0, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing") } if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, 0, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan") } s.Log.Errorf("Failed get project flock %d for sapronak closing: %+v", projectFlockID, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID) if err != nil { s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock") } var projectFlockKandangIDs []uint if params.KandangID != nil && *params.KandangID > 0 { projectFlockKandangIDs = []uint{*params.KandangID} } else if params.Type == validation.SapronakTypeOutgoing { projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID) if err != nil { s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang") } } offset := (params.Page - 1) * params.Limit rows, totalResults, err := s.Repository.GetSapronak(c.Context(), repository.SapronakQueryParams{ Type: params.Type, WarehouseIDs: warehouseIDs, ProjectFlockKandangIDs: projectFlockKandangIDs, Limit: params.Limit, Offset: offset, }) if err != nil { s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak data") } items := make([]dto.ClosingSapronakItemDTO, 0, len(rows)) for _, row := range rows { dateStr := row.DateText if dateStr == "" && !row.SortDate.IsZero() { dateStr = row.SortDate.Format("02-Jan-2006") } items = append(items, dto.ClosingSapronakItemDTO{ Id: row.Id, Date: dateStr, ReferenceNumber: row.ReferenceNumber, TransactionType: row.TransactionType, ProductName: row.ProductName, ProductCategory: row.ProductCategory, ProductSubCategory: row.ProductSubCategory, SourceWarehouse: row.SourceWarehouse, DestinationWarehouse: row.DestinationWarehouse, // Destination: row.Destination, Quantity: row.Quantity, Unit: row.Unit, FormattedQuantity: formatQuantity(row.Quantity, row.Unit), Notes: row.Notes, SortDate: row.SortDate, }) } return items, totalResults, nil } func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) { var kandangIDs []uint db := s.Repository.DB().WithContext(ctx) if err := db.Model(&entity.ProjectFlockKandang{}). Where("project_flock_id = ?", projectFlockID). Pluck("kandang_id", &kandangIDs).Error; err != nil { return nil, err } if len(kandangIDs) == 0 { return []uint{}, nil } var warehouses []entity.Warehouse if err := db.Where("kandang_id IN ?", kandangIDs).Find(&warehouses).Error; err != nil { return nil, err } unique := make(map[uint]struct{}) for _, warehouse := range warehouses { unique[warehouse.Id] = struct{}{} } ids := make([]uint, 0, len(unique)) for id := range unique { ids = append(ids, id) } return ids, nil } func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) { var ids []uint query := s.Repository.DB().WithContext(ctx). Model(&entity.ProjectFlockKandang{}). Where("project_flock_id = ?", projectFlockID) err := query.Order("id ASC").Pluck("id", &ids).Error if err != nil { return nil, err } return ids, nil } func formatQuantity(qty float64, uom string) string { qtyStr := strconv.FormatFloat(qty, 'f', -1, 64) if uom == "" { return qtyStr } return qtyStr + " " + uom } func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID uint) (string, string, error) { if s.ApprovalSvc == nil { return "", "Belum Selesai", nil } records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlock.String(), &projectFlockID, 1, 1000, "") if err != nil { return "", "", err } var ( minStep uint16 statusProject string completed int latestActionAt time.Time ) for _, rec := range records { if minStep == 0 || rec.StepNumber < minStep { minStep = rec.StepNumber } if latestActionAt.IsZero() || rec.ActionAt.After(latestActionAt) { latestActionAt = rec.ActionAt statusProject = rec.StepName } } if statusProject == "" && minStep > 0 { if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlock, approvalutils.ApprovalStep(minStep)); ok { statusProject = label } } statusClosing := "Belum Selesai" switch { case len(records) == 0 || completed == 0: statusClosing = "Belum Selesai" case completed < len(records): statusClosing = "Sebagian" default: statusClosing = "Selesai" } return statusProject, statusClosing, nil } func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) { budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, err } realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlockID, projectFlockKandangID) if err != nil { return nil, err } projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, err } totalKandangCount := len(projectFlockKandangs) // Build kandang count map for farm expense division projectFlockKandangCountMap := make(map[uint]int) projectFlockKandangCountMap[projectFlockID] = totalKandangCount involvedProjectFlocks := make(map[uint]bool) for _, realization := range realizations { if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Expense != nil && realization.ExpenseNonstock.Expense.ProjectFlockId != nil { var projectFlockIDs []uint if err := json.Unmarshal([]byte(*realization.ExpenseNonstock.Expense.ProjectFlockId), &projectFlockIDs); err == nil { for _, pfID := range projectFlockIDs { if pfID != projectFlockID { involvedProjectFlocks[pfID] = true } } } } } for pfID := range involvedProjectFlocks { if pfKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), pfID); err == nil { projectFlockKandangCountMap[pfID] = len(pfKandangs) } } chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, err } var totalChickinQty float64 var totalDepletion float64 if projectFlockKandangID != nil { for _, chickin := range chickins { if chickin.ProjectFlockKandangId == *projectFlockKandangID { totalChickinQty += chickin.UsageQty } } var depletionResult float64 err = s.RecordingRepo.DB().WithContext(c.Context()). Table("recording_depletions"). Select("COALESCE(SUM(recording_depletions.qty), 0)"). Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id"). Where("recordings.project_flock_kandangs_id = ?", *projectFlockKandangID). Scan(&depletionResult).Error if err != nil { s.Log.Warnf("GetTotalDepletionByProjectFlockKandangID error: %v", err) } else { totalDepletion = depletionResult } } else { for _, chickin := range chickins { totalChickinQty += chickin.UsageQty } totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) } } totalActualPopulation := totalChickinQty - totalDepletion result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation, projectFlockKandangID != nil, totalKandangCount, projectFlockKandangCountMap) return &result, nil } func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) { if projectFlockID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } rows, err := s.Repository.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID) if err != nil { s.Log.Errorf("Failed to get expedition HPP for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch expedition HPP") } expeditionCosts := make([]dto.ExpeditionCostItemDTO, 0, len(rows)) var totalHPP float64 for idx, row := range rows { expeditionCosts = append(expeditionCosts, dto.ExpeditionCostItemDTO{ Id: uint64(idx + 1), ExpeditionVendorName: row.SupplierName, HPPAmount: row.TotalAmount, }) totalHPP += row.TotalAmount } result := &dto.ExpeditionHPPDTO{ ExpeditionCosts: expeditionCosts, TotalHPPAmount: totalHPP, } return result, nil } func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error) { if projectFlockID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } var projectFlockKandangIDs []uint if kandangID != nil && *kandangID > 0 { projectFlockKandangIDs = []uint{*kandangID} } else { var err error projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID) if err != nil { s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs") } } if len(projectFlockKandangIDs) == 0 { return nil, fiber.NewError(fiber.StatusNotFound, "No project flock kandang found") } project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") } if err != nil { s.Log.Errorf("Failed get project flock %d for closing data produksi: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } population, err := s.Repository.SumProjectChickinUsageByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) if err != nil { s.Log.Errorf("Failed to sum population for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data") } isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing)) currentWeek, err := s.determineProductionWeek(c.Context(), projectFlockKandangIDs) if err != nil { s.Log.Errorf("Failed to determine production week for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine production week") } targetAverages, err := s.RecordingRepo.GetAverageTargetMetricsByProjectFlockKandangID(c.Context(), projectFlockKandangIDs[0], !isGrowing) if err != nil { s.Log.Errorf("Failed to calculate target metrics for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch target metrics data") } var fcrActFromRecording *float64 if targetAverages.FcrCount > 0 { fcrAvg := targetAverages.FcrAvg fcrActFromRecording = &fcrAvg } feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) if err != nil { s.Log.Errorf("Failed to sum feed purchase/used qty for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data") } averageFeedIntake := targetAverages.FeedIntakeAvg feedIntakeStd := 0.0 var mortalityStdFromGrowth *float64 if project.ProductionStandardId > 0 && currentWeek > 0 && s.StandardGrowthDetailRepo != nil { growthDetail, growthErr := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek) if growthErr != nil { if !errors.Is(growthErr, gorm.ErrRecordNotFound) { s.Log.Errorf("Failed to fetch growth detail for project flock %d: %+v", projectFlockID, growthErr) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch growth standard data") } } else if growthDetail != nil { if growthDetail.FeedIntake != nil { feedIntakeStd = *growthDetail.FeedIntake } if growthDetail.MaxDepletion != nil { mortalityStdFromGrowth = growthDetail.MaxDepletion } } } var productionStandardDetail *entity.ProductionStandardDetail if project.ProductionStandardId > 0 && currentWeek > 0 && s.ProductionStandardDetailRepo != nil { productionStandardDetail, err = s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { productionStandardDetail = nil } else { s.Log.Errorf("Failed to fetch production standard detail for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch production standard detail data") } } } claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) if err != nil { s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch claim culling data") } finalPopulation := population - claimCulling var standards []entity.FcrStandard if project.FcrId > 0 { standards, err = s.Repository.GetFcrStandardsByFcrID(c.Context(), project.FcrId) if err != nil { s.Log.Errorf("Failed to fetch FCR standards for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data") } } age, err := s.calculateAverageSalesAge(c.Context(), projectFlockID) if err != nil { s.Log.Errorf("Failed to calculate sales age for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data") } // feedUsedPerHead := 0.0 // if population > 0 { // feedUsedPerHead = feedUsed / population // } purchase := dto.ClosingPurchaseDTO{ InitialPopulation: int(population), ClaimCulling: int(claimCulling), FinalPopulation: int(finalPopulation), FeedIn: feedIn, FeedUsed: feedUsed, // FeedUsedPerHead: feedUsedPerHead, } chickenFlagNames := []string{string(utils.FlagPullet)} chickenSalesWeight, chickenSalesQty, chickenSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, chickenFlagNames) if err != nil { s.Log.Errorf("Failed to fetch chicken sales data for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chicken sales data") } var chickenAverageWeight float64 if chickenSalesQty > 0 { chickenAverageWeight = chickenSalesWeight / chickenSalesQty } var chickenAverageSellingPrice float64 if chickenSalesWeight > 0 { chickenAverageSellingPrice = chickenSalesPrice / chickenSalesWeight } chickenSales := dto.ClosingSalesDTO{ SalesPopulation: int(chickenSalesQty), SalesWeight: chickenSalesWeight, AverageWeight: chickenAverageWeight, AverageSellingPrice: chickenAverageSellingPrice, } chickenDepletion := population - chickenSalesQty if chickenDepletion < 0 { chickenDepletion = 0 } chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards) if fcrActFromRecording != nil { chickenPerformance.FcrAct = *fcrActFromRecording } var eggSales *dto.ClosingEggSalesDTO var eggPerformance *dto.ClosingPerformanceDTO if !isGrowing { eggFlagNames := []string{ string(utils.FlagTelur), string(utils.FlagTelurUtuh), string(utils.FlagTelurPecah), string(utils.FlagTelurPutih), string(utils.FlagTelurRetak), } eggSalesWeight, eggSalesQty, eggSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) if err != nil { s.Log.Errorf("Failed to fetch egg sales data for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg sales data") } var averageEggWeight float64 if eggSalesQty > 0 { averageEggWeight = eggSalesWeight / eggSalesQty } var averageEggSellingPrice float64 if eggSalesWeight > 0 { averageEggSellingPrice = eggSalesPrice / eggSalesWeight } eggSales = &dto.ClosingEggSalesDTO{ EggPieces: int(eggSalesQty), EggMassKg: eggSalesWeight, AverageEggWeightKg: averageEggWeight, AverageSellingPrice: averageEggSellingPrice, } harvestEggQty, err := s.Repository.SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) if err != nil { s.Log.Errorf("Failed to fetch recording egg qty for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg harvest data") } eggDepletion := harvestEggQty - eggSalesQty if eggDepletion < 0 { eggDepletion = 0 } eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards) if fcrActFromRecording != nil { eggPerf.FcrAct = *fcrActFromRecording } eggPerformance = &eggPerf } sales := dto.ClosingSalesGroupDTO{ Chicken: chickenSales, Egg: eggSales, } performance := dto.ClosingPerformanceDTO{ Depletion: chickenPerformance.Depletion, Age: age, MortalityStd: chickenPerformance.MortalityStd, MortalityAct: chickenPerformance.MortalityAct, DeffMortality: chickenPerformance.DeffMortality, } if eggPerformance != nil { // performance.FcrStd = eggPerformance.FcrStd performance.FcrAct = eggPerformance.FcrAct // performance.DeffFcr = eggPerformance.DeffFcr performance.AwgAct = eggPerformance.AwgAct } else { // performance.FcrStd = chickenPerformance.FcrStd performance.FcrAct = chickenPerformance.FcrAct // performance.DeffFcr = chickenPerformance.DeffFcr performance.AwgAct = chickenPerformance.AwgAct } performance.FeedIntake = averageFeedIntake performance.FeedIntakeStd = feedIntakeStd if targetAverages.CumDepletionRateCount > 0 { performance.MortalityAct = targetAverages.CumDepletionRateAvg performance.DeffMortality = performance.MortalityAct - performance.MortalityStd } if mortalityStdFromGrowth != nil { performance.MortalityStd = *mortalityStdFromGrowth performance.DeffMortality = performance.MortalityAct - performance.MortalityStd } if !isGrowing { if targetAverages.HenDayCount > 0 { henDayAct := targetAverages.HenDayAvg performance.HenDayAct = &henDayAct } if targetAverages.HenHouseCount > 0 { henHouseAct := targetAverages.HenHouseAvg performance.HenHouseAct = &henHouseAct } if targetAverages.EggWeightCount > 0 { eggWeight := targetAverages.EggWeightAvg performance.EggWeight = &eggWeight } if targetAverages.EggMassCount > 0 { eggMass := targetAverages.EggMassAvg performance.EggMass = &eggMass } } performance.DeffFcr = performance.FcrStd - performance.FcrAct if productionStandardDetail != nil { if productionStandardDetail.StandardFCR != nil { performance.FcrStd = *productionStandardDetail.StandardFCR } if !isGrowing { if productionStandardDetail.TargetHenDayProduction != nil { performance.HendayStd = *productionStandardDetail.TargetHenDayProduction } if productionStandardDetail.TargetHenHouseProduction != nil { performance.HenHouseStd = *productionStandardDetail.TargetHenHouseProduction } if productionStandardDetail.TargetEggWeight != nil { performance.EggWeightStd = *productionStandardDetail.TargetEggWeight } if productionStandardDetail.TargetEggMass != nil { performance.EggMassStd = *productionStandardDetail.TargetEggMass } } } result := dto.ClosingProductionReportDTO{ Purchase: purchase, Sales: sales, Performance: performance, } return &result, nil } func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlockID uint) (float64, error) { deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(ctx, projectFlockID, func(db *gorm.DB) *gorm.DB { return db. Preload("MarketingProduct"). Preload("MarketingProduct.ProductWarehouse"). Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang"). Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins") }) if err != nil { return 0, err } var ( totalQty float64 totalAgeWeeks float64 ) for _, product := range deliveryProducts { if product.UsageQty == 0 { continue } projectFlockKandang := product.MarketingProduct.ProductWarehouse.ProjectFlockKandang ageWeeks := dto.CalculateAgeFromChickinDataProduksi(projectFlockKandang, product.DeliveryDate) totalAgeWeeks += float64(ageWeeks) * product.UsageQty totalQty += product.UsageQty } if totalQty == 0 { return 0, nil } return totalAgeWeeks / totalQty, nil } func (s closingService) determineProductionWeek(ctx context.Context, projectFlockKandangIDs []uint) (int, error) { if len(projectFlockKandangIDs) == 0 { return 0, nil } firstKandangID := projectFlockKandangIDs[0] var chickin entity.ProjectChickin if err := s.Repository.DB().WithContext(ctx). Where("project_flock_kandang_id = ?", firstKandangID). Order("chick_in_date ASC"). First(&chickin).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return 0, nil } return 0, err } recording, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx, firstKandangID) if err != nil { return 0, err } if recording == nil { return 0, nil } if recording.RecordDatetime.Before(chickin.ChickInDate) { return 0, nil } elapsed := recording.RecordDatetime.Sub(chickin.ChickInDate) weekFloat := elapsed.Hours() / (24 * 7) week := int(math.Ceil(weekFloat)) if week <= 0 { week = 1 } return week, nil } func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO { mortalityStd, fcrStd := closestFcrValues(standards, averageWeight) fcrAct := 0.0 if totalWeight > 0 { fcrAct = feedUsed / totalWeight } mortalityAct := 0.0 if basePopulation > 0 { mortalityAct = (depletion / basePopulation) * 100 } deffMortality := mortalityAct - mortalityStd deffFcr := fcrAct - fcrStd awg := 0.0 if age > 0 { awg = averageWeight / age } return dto.ClosingPerformanceDTO{ Depletion: depletion, Age: age, MortalityStd: mortalityStd, MortalityAct: mortalityAct, DeffMortality: deffMortality, FcrStd: fcrStd, FcrAct: fcrAct, DeffFcr: deffFcr, AwgAct: awg, } } func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (float64, float64) { if len(standards) == 0 || averageWeight <= 0 { return 0, 0 } closest := standards[0] minDiff := math.Abs(closest.Weight - averageWeight) for _, std := range standards[1:] { diff := math.Abs(std.Weight - averageWeight) if diff < minDiff { minDiff = diff closest = std } } return closest.Mortality, closest.FcrNumber }