package service import ( "context" "encoding/json" "errors" "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" 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) (*dto.ClosingSummaryDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, 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 } 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, 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, } } 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) (*dto.ClosingSummaryDTO, error) { if projectFlockID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } 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) 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.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 err := s.Repository.DB().WithContext(ctx). Model(&entity.ProjectFlockKandang{}). Where("project_flock_id = ?", projectFlockID). 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) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) { if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists}, ); err != nil { return nil, err } projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") } actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost") } purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows) realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations") } deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { return db.Preload("MarketingProduct"). Preload("MarketingProduct.ProductWarehouse"). Preload("MarketingProduct.ProductWarehouse.Product") }) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products") } chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") } totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) } totalEggWeightKg, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetTotalEggProductionWeightByProjectFlockID error: %v", err) } totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) } input := dto.ClosingKeuanganInput{ ProjectFlockCategory: projectFlock.Category, PurchaseItems: purchaseItems, Budgets: budgets, Realizations: realizations, DeliveryProducts: deliveryProducts, Chickins: chickins, TotalWeightProduced: totalWeightProduced, TotalEggWeightKg: totalEggWeightKg, TotalDepletion: totalDepletion, } report := dto.ToClosingKeuanganReport(input) return &report, 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) (*dto.ClosingProductionReportDTO, error) { if projectFlockID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } 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 data produksi: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } var population float64 for _, history := range project.KandangHistory { for _, chickin := range history.Chickins { population += chickin.UsageQty } } isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing)) 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") } 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") } 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) 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) 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.Awg = eggPerformance.Awg } else { performance.FcrStd = chickenPerformance.FcrStd performance.FcrAct = chickenPerformance.FcrAct performance.DeffFcr = chickenPerformance.DeffFcr performance.Awg = chickenPerformance.Awg } 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 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, Awg: 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 } func (s closingService) convertActualUsageToPurchaseItems(ctx context.Context, actualUsageRows []repository.ActualUsageCostRow) []entity.PurchaseItem { if len(actualUsageRows) == 0 { return []entity.PurchaseItem{} } // Collect all product IDs productIDs := make([]uint, len(actualUsageRows)) for i, row := range actualUsageRows { productIDs[i] = row.ProductID } // Fetch products with flags from repository products, err := s.Repository.GetProductsWithFlagsByIDs(ctx, productIDs) if err != nil { s.Log.Warnf("Failed to fetch products for actual usage: %v", err) products = []entity.Product{} } // Create product map productMap := make(map[uint]*entity.Product) for i := range products { productMap[products[i].Id] = &products[i] } // Convert to pseudo purchase items purchaseItems := make([]entity.PurchaseItem, 0, len(actualUsageRows)) for _, row := range actualUsageRows { product := productMap[row.ProductID] // Skip if product not found if product == nil { s.Log.Warnf("Product ID %d not found for actual usage", row.ProductID) continue } purchaseItem := entity.PurchaseItem{ Id: 0, // Pseudo item, no ID ProductId: row.ProductID, TotalQty: row.TotalQty, TotalPrice: row.TotalPrice, Price: row.AveragePrice, Product: product, } purchaseItems = append(purchaseItems, purchaseItem) } return purchaseItems }