package service import ( "context" "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" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "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 activeKandangMetric struct { ProjectFlockKandangID uint ProjectFlockID uint KandangID uint Category string Metric float64 } 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) GetClosingSapronakSummary(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, 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 } scope, err := m.ResolveLocationScope(c, s.Repository.DB()) if err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit statusFilter := "" if params.ProjectStatus != nil { switch *params.ProjectStatus { case 1: statusFilter = "Pengajuan" case 2: statusFilter = "Aktif" } } closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withClosingRelations(db) if scope.Restrict { if len(scope.IDs) == 0 { return db.Where("1 = 0") } db = m.ApplyScopeFilter(db, scope, "project_flocks.location_id") } if params.LocationID != nil { db = db.Where("location_id = ?", *params.LocationID) } if statusFilter != "" { latestApprovalSubQuery := s.Repository.DB(). WithContext(c.Context()). Table("approvals"). Select("DISTINCT ON (approvable_id) approvable_id, step_name, id"). Where("approvable_type = ?", utils.ApprovalWorkflowProjectFlock.String()). Order("approvable_id, id DESC") db = db.Joins("JOIN (?) AS latest_approval ON latest_approval.approvable_id = project_flocks.id", latestApprovalSubQuery). Where("LOWER(latest_approval.step_name) = LOWER(?)", statusFilter) } 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) { if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), id); err != nil { return nil, err } 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) { if projectFlockKandangID != nil { if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), projectFlockID, *projectFlockKandangID); err != nil { return nil, err } } else if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { return nil, err } 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 err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { return nil, err } 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 err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { return nil, 0, err } 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") } warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID, params.KandangID) 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 { 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 startDate, endDate, err := s.getSapronakDateRange(c.Context(), projectFlockID, params.KandangID) if err != nil { s.Log.Errorf("Failed to resolve sapronak date range for project flock %d: %+v", projectFlockID, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve sapronak date range") } rows, totalResults, err := s.Repository.GetSapronak(c.Context(), repository.SapronakQueryParams{ Type: params.Type, WarehouseIDs: warehouseIDs, ProjectFlockKandangIDs: projectFlockKandangIDs, Limit: params.Limit, Offset: offset, Search: params.Search, StartDate: startDate, EndDate: endDate, }) 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) GetClosingSapronakSummary(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error) { if projectFlockID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } if params == nil { params = &validation.ClosingSapronakQuery{} } if err := s.Validate.Struct(params); err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) } if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing { return nil, 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, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan") } s.Log.Errorf("Failed get project flock %d for sapronak closing summary: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID, params.KandangID) if err != nil { s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err) return nil, 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 { 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, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang") } } startDate, endDate, err := s.getSapronakDateRange(c.Context(), projectFlockID, params.KandangID) if err != nil { s.Log.Errorf("Failed to resolve sapronak date range for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve sapronak date range") } rows, err := s.Repository.GetSapronakSummary(c.Context(), repository.SapronakQueryParams{ Type: params.Type, WarehouseIDs: warehouseIDs, ProjectFlockKandangIDs: projectFlockKandangIDs, Search: params.Search, StartDate: startDate, EndDate: endDate, }) if err != nil { s.Log.Errorf("Failed to fetch sapronak %s summary for project flock %d: %+v", params.Type, projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak summary data") } items := make([]dto.ClosingSapronakSummaryItemDTO, 0, len(rows)) for _, row := range rows { items = append(items, dto.ClosingSapronakSummaryItemDTO{ Category: row.Category, TotalQty: row.TotalQty, Uom: dto.UomSummaryDTO{ ID: row.UomID, Name: row.UomName, }, }) } return items, nil } func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint, kandangID *uint) ([]uint, error) { var kandangIDs []uint db := s.Repository.DB().WithContext(ctx) query := db.Model(&entity.ProjectFlockKandang{}). Where("project_flock_id = ?", projectFlockID) if kandangID != nil && *kandangID > 0 { query = query.Where("id = ?", *kandangID) } if err := query.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 (s closingService) getSapronakDateRange(ctx context.Context, projectFlockID uint, kandangID *uint) (*time.Time, *time.Time, error) { db := s.Repository.DB().WithContext(ctx) if kandangID != nil && *kandangID > 0 { var pfk entity.ProjectFlockKandang if err := db.Select("id, created_at, closed_at").First(&pfk, *kandangID).Error; err != nil { return nil, nil, err } var firstChickin struct { ChickInDate time.Time `gorm:"column:chick_in_date"` } if err := db.Table("project_chickins"). Select("chick_in_date"). Where("project_flock_kandang_id = ? AND chick_in_date IS NOT NULL", pfk.Id). Order("chick_in_date ASC"). Limit(1). Scan(&firstChickin).Error; err != nil { return nil, nil, err } start := pfk.CreatedAt if !firstChickin.ChickInDate.IsZero() { start = firstChickin.ChickInDate } startDate := dateOnlyUTC(start) var endDate *time.Time if pfk.ClosedAt != nil { d := dateOnlyUTC(*pfk.ClosedAt) endDate = &d } return &startDate, endDate, nil } var firstPFK entity.ProjectFlockKandang if err := db.Model(&entity.ProjectFlockKandang{}). Select("created_at"). Where("project_flock_id = ?", projectFlockID). Order("created_at ASC"). First(&firstPFK).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, nil } return nil, nil, err } var firstChickin struct { ChickInDate time.Time `gorm:"column:chick_in_date"` } if err := db.Table("project_chickins pc"). Select("pc.chick_in_date"). Joins("JOIN project_flock_kandangs pfk ON pfk.id = pc.project_flock_kandang_id"). Where("pfk.project_flock_id = ? AND pc.chick_in_date IS NOT NULL", projectFlockID). Order("pc.chick_in_date ASC"). Limit(1). Scan(&firstChickin).Error; err != nil { return nil, nil, err } start := firstPFK.CreatedAt if !firstChickin.ChickInDate.IsZero() { start = firstChickin.ChickInDate } startDate := dateOnlyUTC(start) var endDate *time.Time var openCount int64 if err := db.Model(&entity.ProjectFlockKandang{}). Where("project_flock_id = ? AND closed_at IS NULL", projectFlockID). Count(&openCount).Error; err != nil { return nil, nil, err } if openCount == 0 { var latestClosed entity.ProjectFlockKandang if err := db.Model(&entity.ProjectFlockKandang{}). Select("closed_at"). Where("project_flock_id = ? AND closed_at IS NOT NULL", projectFlockID). Order("closed_at DESC"). First(&latestClosed).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return &startDate, nil, nil } return nil, nil, err } if latestClosed.ClosedAt != nil && !latestClosed.ClosedAt.IsZero() { d := dateOnlyUTC(*latestClosed.ClosedAt) endDate = &d } } return &startDate, endDate, nil } func dateOnlyUTC(t time.Time) time.Time { u := t.UTC() return time.Date(u.Year(), u.Month(), u.Day(), 0, 0, 0, 0, time.UTC) } 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) { if projectFlockKandangID != nil { if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), projectFlockID, *projectFlockKandangID); err != nil { return nil, err } } else if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { return nil, err } 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 } realizations, err = s.allocateFarmOverheadRealizations(c.Context(), projectFlockID, projectFlockKandangID, realizations) if err != nil { return nil, err } projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, err } totalKandangCount := len(projectFlockKandangs) 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) return &result, nil } type activeKandangMetricRow struct { ProjectFlockKandangID uint `gorm:"column:project_flock_kandang_id"` ProjectFlockID uint `gorm:"column:project_flock_id"` KandangID uint `gorm:"column:kandang_id"` Category string `gorm:"column:category"` ChickinQty float64 `gorm:"column:chickin_qty"` DepletionQty float64 `gorm:"column:depletion_qty"` EggQty float64 `gorm:"column:egg_qty"` } func (s closingService) getActiveKandangMetrics(ctx context.Context, locationID uint, transactionDate time.Time) ([]activeKandangMetric, error) { db := s.Repository.DB().WithContext(ctx) rows := []activeKandangMetricRow{} rawSQL := ` SELECT pfk.id AS project_flock_kandang_id, pfk.project_flock_id AS project_flock_id, pfk.kandang_id AS kandang_id, pf.category AS category, COALESCE(( SELECT SUM(pc.usage_qty) FROM project_chickins pc WHERE pc.project_flock_kandang_id = pfk.id AND pc.chick_in_date::date <= ? ), 0) AS chickin_qty, COALESCE(( SELECT SUM(rd.qty) FROM recording_depletions rd JOIN recordings r ON r.id = rd.recording_id WHERE r.project_flock_kandangs_id = pfk.id AND r.record_datetime::date <= ? ), 0) AS depletion_qty, COALESCE(( SELECT SUM(re.qty) FROM recording_eggs re JOIN recordings r2 ON r2.id = re.recording_id WHERE r2.project_flock_kandangs_id = pfk.id AND r2.record_datetime::date <= ? ), 0) AS egg_qty FROM project_flock_kandangs pfk JOIN project_flocks pf ON pf.id = pfk.project_flock_id WHERE pf.location_id = ? AND (pfk.closed_at IS NULL OR pfk.closed_at::date > ?) AND EXISTS ( SELECT 1 FROM project_chickins pc2 WHERE pc2.project_flock_kandang_id = pfk.id AND pc2.chick_in_date::date <= ? ) ` if err := db.Raw(rawSQL, transactionDate, transactionDate, transactionDate, locationID, transactionDate, transactionDate).Scan(&rows).Error; err != nil { return nil, err } result := make([]activeKandangMetric, 0, len(rows)) for _, row := range rows { metric := 0.0 switch strings.ToLower(strings.TrimSpace(row.Category)) { case "growing": metric = row.ChickinQty case "laying": metric = row.EggQty default: s.Log.Warnf("Unknown project flock category for overhead allocation: %s (pfk=%d)", row.Category, row.ProjectFlockKandangID) } result = append(result, activeKandangMetric{ ProjectFlockKandangID: row.ProjectFlockKandangID, ProjectFlockID: row.ProjectFlockID, KandangID: row.KandangID, Category: row.Category, Metric: metric, }) } return result, nil } func round2(value float64) float64 { return math.Round(value*100) / 100 } func allocateFarmLevelQty(totalQty float64, metrics []activeKandangMetric) map[uint]float64 { allocations := make(map[uint]float64, len(metrics)) if totalQty == 0 || len(metrics) == 0 { return allocations } totalMetric := 0.0 var maxMetric float64 var maxMetricID uint for _, m := range metrics { if m.Metric <= 0 { continue } totalMetric += m.Metric if m.Metric > maxMetric || maxMetricID == 0 { maxMetric = m.Metric maxMetricID = m.ProjectFlockKandangID } } if totalMetric == 0 { return allocations } sumRounded := 0.0 for _, m := range metrics { if m.Metric <= 0 { continue } portion := totalQty * (m.Metric / totalMetric) rounded := round2(portion) allocations[m.ProjectFlockKandangID] = rounded sumRounded += rounded } diff := totalQty - sumRounded if maxMetricID != 0 && diff != 0 { allocations[maxMetricID] = round2(allocations[maxMetricID] + diff) } return allocations } func (s closingService) allocateFarmOverheadRealizations(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint, realizations []entity.ExpenseRealization) ([]entity.ExpenseRealization, error) { if len(realizations) == 0 { return realizations, nil } cache := make(map[string][]activeKandangMetric) allocated := make([]entity.ExpenseRealization, 0, len(realizations)) for _, realization := range realizations { expenseNonstock := realization.ExpenseNonstock if expenseNonstock == nil || expenseNonstock.Expense == nil { allocated = append(allocated, realization) continue } // If already bound to a specific project flock kandang, don't re-allocate. if expenseNonstock.ProjectFlockKandangId != nil { allocated = append(allocated, realization) continue } expense := expenseNonstock.Expense locationID := uint(expense.LocationId) txDate := expense.RealizationDate cacheKey := fmt.Sprintf("%d|%s", locationID, txDate.Format("2006-01-02")) metrics, exists := cache[cacheKey] if !exists { var err error metrics, err = s.getActiveKandangMetrics(ctx, locationID, txDate) if err != nil { return nil, err } cache[cacheKey] = metrics } allocations := allocateFarmLevelQty(realization.Qty, metrics) allocatedQty := 0.0 if projectFlockKandangID != nil { allocatedQty = allocations[*projectFlockKandangID] } else { for _, m := range metrics { if m.ProjectFlockID == projectFlockID { allocatedQty += allocations[m.ProjectFlockKandangID] } } allocatedQty = round2(allocatedQty) } adj := realization adj.Qty = allocatedQty if adj.Qty == 0 { adj.Price = realization.Price } allocated = append(allocated, adj) } return allocated, nil } func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) { if projectFlockKandangID != nil { if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), projectFlockID, *projectFlockKandangID); err != nil { return nil, err } } else if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { return nil, err } 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") } if !isGrowing && currentWeek != 0 { currentWeek = currentWeek + 17 } 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 age, err := s.calculateAverageSalesAge(c.Context(), projectFlockID, kandangID) 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), string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagLayer)} 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) 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) 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 } } if productionStandardDetail != nil { if productionStandardDetail.StandardFCR != nil { performance.FcrStd = *productionStandardDetail.StandardFCR performance.DeffFcr = performance.FcrStd - performance.FcrAct } 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, projectFlockKandangID *uint) (float64, error) { penjualan, err := s.MarketingDeliveryProductRepo.GetClosingPenjualanForAgeChickDataProduction(ctx, projectFlockID, projectFlockKandangID) if err != nil { return 0, err } acumulateAgeQty := 0.0 totalQty := 0.0 for _, v := range penjualan { sale := dto.ToSalesAgeDTO(v) acumulateAgeQty += float64(sale.Age) * sale.Qty totalQty += sale.Qty } if totalQty > 0 { averageAge := acumulateAgeQty / totalQty return averageAge, nil } return 0, err } 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) dto.ClosingPerformanceDTO { mortalityStd, fcrStd := 0.0, 0.0 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, } }