diff --git a/internal/common/repository/common.hppv2.repository.go b/internal/common/repository/common.hppv2.repository.go index 80fe7438..81b59829 100644 --- a/internal/common/repository/common.hppv2.repository.go +++ b/internal/common/repository/common.hppv2.repository.go @@ -89,11 +89,21 @@ type HppV2ManualDepreciationInputRow struct { Note *string } +type HppV2FarmDepreciationSnapshotRow struct { + ID uint + ProjectFlockID uint + PeriodDate time.Time + DepreciationPercentEffective float64 + DepreciationValue float64 + PulletCostDayNTotal float64 +} + type HppV2CostRepository interface { GetProjectFlockKandangContext(ctx context.Context, projectFlockKandangId uint) (*HppV2ProjectFlockKandangContext, error) GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error) GetLatestTransferInputByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint, period time.Time) (*HppV2LatestTransferInputRow, error) GetManualDepreciationInputByProjectFlockID(ctx context.Context, projectFlockID uint) (*HppV2ManualDepreciationInputRow, error) + GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(ctx context.Context, projectFlockID uint, periodDate time.Time) (*HppV2FarmDepreciationSnapshotRow, error) GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error) GetDepreciationPercents(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error) ListUsageCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2UsageCostRow, error) @@ -239,6 +249,29 @@ func (r *HppV2RepositoryImpl) GetManualDepreciationInputByProjectFlockID( return &row, nil } +func (r *HppV2RepositoryImpl) GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod( + ctx context.Context, + projectFlockID uint, + periodDate time.Time, +) (*HppV2FarmDepreciationSnapshotRow, error) { + var row HppV2FarmDepreciationSnapshotRow + err := r.db.WithContext(ctx). + Table("farm_depreciation_snapshots"). + Select("id, project_flock_id, period_date, depreciation_percent_effective, depreciation_value, pullet_cost_day_n_total"). + Where("project_flock_id = ?", projectFlockID). + Where("period_date = DATE(?)", periodDate). + Limit(1). + Take(&row).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + if err != nil { + return nil, err + } + + return &row, nil +} + func (r *HppV2RepositoryImpl) GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error) { type row struct { ChickInDate *time.Time @@ -327,11 +360,11 @@ func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags( COALESCE(pi.product_id, ast_pw.product_id, 0) AS source_product_id, COALESCE(pi_prod.name, ast_prod.name, '') AS source_product_name, COALESCE(SUM(sa.qty), 0) AS qty, - CASE + COALESCE(MAX(CASE WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0) ELSE 0 - END AS unit_price, + END), 0) AS unit_price, COALESCE(SUM(sa.qty * CASE WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0) @@ -367,12 +400,7 @@ func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags( sa.stockable_type, sa.stockable_id, COALESCE(pi.product_id, ast_pw.product_id, 0), - COALESCE(pi_prod.name, ast_prod.name, ''), - CASE - WHEN sa.stockable_type = '` + stockablePurchase + `' THEN COALESCE(pi.price, 0) - WHEN sa.stockable_type = '` + stockableAdjustment + `' THEN COALESCE(ast.price, 0) - ELSE 0 - END + COALESCE(pi_prod.name, ast_prod.name, '') `). Order("MIN(r.record_datetime) ASC, sa.stockable_type ASC, sa.stockable_id ASC"). Scan(&rows).Error @@ -417,7 +445,7 @@ func (r *HppV2RepositoryImpl) ListAdjustmentCostRowsByProductFlags( Joins("JOIN products AS p ON p.id = pw.product_id"). Joins("JOIN warehouses AS w ON w.id = pw.warehouse_id"). Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). - Where("ast.created_at <= ?", *date). + // Where("ast.created_at <= ?", *date). Where("COALESCE(ast.total_qty, 0) > 0"). Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flagNames). Order("ast.created_at ASC, ast.id ASC"). @@ -450,6 +478,15 @@ func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags( stockableTransferToLaying := fifo.StockableKeyTransferToLayingIn.String() usableProjectChickin := fifo.UsableKeyProjectChickin.String() usableStockTransferOut := fifo.UsableKeyStockTransferOut.String() + unitPriceExpr := fmt.Sprintf(` + CASE + WHEN sa.stockable_type = '%s' THEN COALESCE(pi.price, 0) + WHEN sa.stockable_type = '%s' THEN COALESCE(ast.price, 0) + WHEN sa.stockable_type = '%s' THEN COALESCE(spi.price, sast.price, 0) + WHEN sa.stockable_type = '%s' THEN COALESCE(tpi.price, tast.price, 0) + ELSE 0 + END + `, stockablePurchase, stockableAdjustment, stockableTransferIn, stockableTransferToLaying) rows := make([]HppV2ChickinCostRow, 0) query := r.db.WithContext(ctx). @@ -479,30 +516,9 @@ func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags( '' ) AS source_product_name, COALESCE(SUM(sa.qty), 0) AS qty, - CASE - WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) - WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0) - WHEN sa.stockable_type = ? THEN COALESCE(spi.price, sast.price, 0) - WHEN sa.stockable_type = ? THEN COALESCE(tpi.price, tast.price, 0) - ELSE 0 - END AS unit_price, - COALESCE(SUM(sa.qty * CASE - WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) - WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0) - WHEN sa.stockable_type = ? THEN COALESCE(spi.price, sast.price, 0) - WHEN sa.stockable_type = ? THEN COALESCE(tpi.price, tast.price, 0) - ELSE 0 - END), 0) AS total_cost - `, - stockablePurchase, - stockableAdjustment, - stockableTransferIn, - stockableTransferToLaying, - stockablePurchase, - stockableAdjustment, - stockableTransferIn, - stockableTransferToLaying, - ). + `+unitPriceExpr+` AS unit_price, + COALESCE(SUM(sa.qty * (`+unitPriceExpr+`)), 0) AS total_cost + `). Joins( "JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.status = ? AND sa.allocation_purpose = ?", usableProjectChickin, @@ -563,7 +579,7 @@ func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags( } err := query. - Group(` + Group(fmt.Sprintf(` pc.id, pc.project_flock_kandang_id, pc.chick_in_date, @@ -587,14 +603,8 @@ func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags( sast_prod.name, '' ), - CASE - WHEN sa.stockable_type = '` + stockablePurchase + `' THEN COALESCE(pi.price, 0) - WHEN sa.stockable_type = '` + stockableAdjustment + `' THEN COALESCE(ast.price, 0) - WHEN sa.stockable_type = '` + stockableTransferIn + `' THEN COALESCE(spi.price, sast.price, 0) - WHEN sa.stockable_type = '` + stockableTransferToLaying + `' THEN COALESCE(tpi.price, tast.price, 0) - ELSE 0 - END - `). + %s + `, unitPriceExpr)). Order("pc.chick_in_date ASC, pc.id ASC, sa.stockable_type ASC, sa.stockable_id ASC"). Scan(&rows).Error if err != nil { diff --git a/internal/common/service/common.hpp.service.go b/internal/common/service/common.hpp.service.go index 6ea9ffa3..db83d5a6 100644 --- a/internal/common/service/common.hpp.service.go +++ b/internal/common/service/common.hpp.service.go @@ -2,7 +2,6 @@ package service import ( "context" - "log" "math" "time" @@ -40,108 +39,91 @@ func NewHppService(hppRepo commonRepo.HppCostRepository) HppService { } func (s *hppService) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) { - logHpp("CalculateHppCost", "start project_flock_kandang_id=%d input_date=%s", projectFlockKandangId, formatTimePtr(date)) if date == nil { now := time.Now() date = &now } - logHpp("CalculateHppCost", "normalized_date=%s", formatTimePtr(date)) location, err := time.LoadLocation("Asia/Jakarta") if err != nil { - logHpp("CalculateHppCost", "load_location_error=%v", err) + return nil, err } - logHpp("CalculateHppCost", "location=%s", location.String()) startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location) endOfDay := startOfDay.Add(24 * time.Hour) - logHpp("CalculateHppCost", "start_of_day=%s end_of_day=%s", startOfDay.Format(time.RFC3339), endOfDay.Format(time.RFC3339)) depresiasiTransfer, err := s.GetDepresiasiTransfer(projectFlockKandangId, &endOfDay) if err != nil { - logHpp("CalculateHppCost", "get_depresiasi_transfer_error=%v", err) + return nil, err } - logHpp("CalculateHppCost", "depresiasi_transfer=%f", depresiasiTransfer) totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, &endOfDay, depresiasiTransfer) if err != nil { - logHpp("CalculateHppCost", "get_total_production_cost_error=%v", err) + return nil, err } - logHpp("CalculateHppCost", "total_production_cost=%f", totalProductionCost) result, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay) if err != nil { - logHpp("CalculateHppCost", "get_hpp_estimation_dan_realisasi_error=%v", err) + return nil, err } - logHpp("CalculateHppCost", "done estimation=%+v real=%+v", result.Estimation, result.Real) return result, nil } func (s *hppService) GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error) { - logHpp("GetTotalDepresiasiFlockGrowing", "start source_project_flock_id=%d input_date=%s", sourceProjectFlockID, formatTimePtr(date)) if date == nil { now := time.Now() date = &now } - logHpp("GetTotalDepresiasiFlockGrowing", "normalized_date=%s", formatTimePtr(date)) if s.hppRepo == nil { - logHpp("GetTotalDepresiasiFlockGrowing", "repo_nil return=0") + return 0, nil } kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID) if err != nil { - logHpp("GetTotalDepresiasiFlockGrowing", "get_project_flock_kandang_ids_error=%v", err) + return 0, err } - logHpp("GetTotalDepresiasiFlockGrowing", "kandang_ids=%v", kandangIDs) docCost, err := s.hppRepo.GetDocCost(context.Background(), kandangIDs) if err != nil { - logHpp("GetTotalDepresiasiFlockGrowing", "get_doc_cost_error=%v", err) + return 0, err } - logHpp("GetTotalDepresiasiFlockGrowing", "doc_cost=%f", docCost) budgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), sourceProjectFlockID) if err != nil { - logHpp("GetTotalDepresiasiFlockGrowing", "get_budget_cost_error=%v", err) + return 0, err } - logHpp("GetTotalDepresiasiFlockGrowing", "budget_cost=%f", budgetCost) expedisionCost, err := s.hppRepo.GetExpedisionCost(context.Background(), kandangIDs) if err != nil { - logHpp("GetTotalDepresiasiFlockGrowing", "get_expedision_cost_error=%v", err) + return 0, err } - logHpp("GetTotalDepresiasiFlockGrowing", "expedision_cost=%f", expedisionCost) feedCost, err := s.hppRepo.GetFeedUsageCost(context.Background(), kandangIDs, date) if err != nil { - logHpp("GetTotalDepresiasiFlockGrowing", "get_feed_usage_cost_error=%v", err) + return 0, err } - logHpp("GetTotalDepresiasiFlockGrowing", "feed_cost=%f", feedCost) ovkCost, err := s.hppRepo.GetOvkUsageCost(context.Background(), kandangIDs, date) if err != nil { - logHpp("GetTotalDepresiasiFlockGrowing", "get_ovk_usage_cost_error=%v", err) + return 0, err } - logHpp("GetTotalDepresiasiFlockGrowing", "ovk_cost=%f", ovkCost) total := docCost + budgetCost + expedisionCost + feedCost + ovkCost - logHpp("GetTotalDepresiasiFlockGrowing", "done total=%f", total) return total, nil } func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error) { - logHpp("GetTotalProductionCost", "start project_flock_kandang_id=%d end_date=%s depresiasi_transfer=%f", projectFlockKandangId, formatTimePtr(endDate), depresiasiTransfer) // if date == nil { // now := time.Now() // date = &now @@ -149,248 +131,210 @@ func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate costPullet, err := s.hppRepo.GetPulletCost(context.Background(), projectFlockKandangId) if err != nil { - logHpp("GetTotalProductionCost", "get_pullet_cost_error=%v", err) + return 0, err } - logHpp("GetTotalProductionCost", "cost_pullet=%f", costPullet) costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { - logHpp("GetTotalProductionCost", "get_feed_usage_cost_error=%v", err) + return 0, err } - logHpp("GetTotalProductionCost", "cost_feed=%f", costFeed) costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { - logHpp("GetTotalProductionCost", "get_ovk_usage_cost_error=%v", err) + return 0, err } - logHpp("GetTotalProductionCost", "cost_ovk=%f", costOvk) costExpedision, err := s.hppRepo.GetExpedisionCost(context.Background(), []uint{projectFlockKandangId}) if err != nil { - logHpp("GetTotalProductionCost", "get_expedision_cost_error=%v", err) + return 0, err } - logHpp("GetTotalProductionCost", "cost_expedision=%f", costExpedision) costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, endDate) if err != nil { - logHpp("GetTotalProductionCost", "get_budget_kandang_laying_error=%v", err) + return 0, err } - logHpp("GetTotalProductionCost", "cost_budget=%f", costBudget) // fmt.Println(costBudget, costExpedision, costOvk, costFeed, costPullet, depresiasiTransfer) // depresiasiTransfer = 0 total := depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget - logHpp("GetTotalProductionCost", "done total=%f", total) return total, nil } func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error) { - logHpp("GetBudgetKandangLaying", "start project_flock_kandang_id=%d end_date=%s", projectFlockKandangId, formatTimePtr(endDate)) // if date == nil { // now := time.Now() // date = &now // } if s.hppRepo == nil { - logHpp("GetBudgetKandangLaying", "repo_nil return=0") + return 0, nil } projectFlockId, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId) if err != nil { - logHpp("GetBudgetKandangLaying", "get_project_flock_id_error=%v", err) + return 0, err } - logHpp("GetBudgetKandangLaying", "project_flock_id=%d", projectFlockId) projectFlockKandangIds, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockId) if err != nil { - logHpp("GetBudgetKandangLaying", "get_project_flock_kandang_ids_error=%v", err) + return 0, err } - logHpp("GetBudgetKandangLaying", "project_flock_kandang_ids=%v", projectFlockKandangIds) eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, endDate) if err != nil { - logHpp("GetBudgetKandangLaying", "get_egg_produksi_pieces_flock_error=%v", err) + return 0, err } - logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_flock=%f", eggProduksiPiecesFlock) eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { - logHpp("GetBudgetKandangLaying", "get_egg_produksi_pieces_kandang_error=%v", err) + return 0, err } - logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_kandang=%f", eggProduksiPiecesKandang) totalBudgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), projectFlockId) if err != nil { - logHpp("GetBudgetKandangLaying", "get_budget_cost_error=%v", err) + return 0, err } - logHpp("GetBudgetKandangLaying", "total_budget_cost=%f", totalBudgetCost) if eggProduksiPiecesFlock == 0 { - logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_flock_zero return=0") + return 0, nil } result := (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock - logHpp("GetBudgetKandangLaying", "done result=%f", result) return result, nil } func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, endDate *time.Time) (float64, error) { - logHpp("GetDepresiasiTransfer", "start project_flock_kandang_id=%d end_date=%s", projectFlockKandangId, formatTimePtr(endDate)) if endDate == nil { now := time.Now() endDate = &now } - logHpp("GetDepresiasiTransfer", "normalized_end_date=%s", formatTimePtr(endDate)) if s.hppRepo == nil { - logHpp("GetDepresiasiTransfer", "repo_nil return=0") + return 0, nil } sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId) if err != nil { - logHpp("GetDepresiasiTransfer", "get_transfer_source_summary_error=%v", err) return 0, err } - logHpp("GetDepresiasiTransfer", "source_project_flock_id=%d transfer_total_qty=%f", sourceProjectFlockID, transferTotalQty) if sourceProjectFlockID == 0 || transferTotalQty <= 0 { - logHpp("GetDepresiasiTransfer", "use_manual_fallback=true") result, fallbackErr := s.getManualDepresiasiTransferFallback(projectFlockKandangId) if fallbackErr != nil { - logHpp("GetDepresiasiTransfer", "manual_fallback_error=%v", fallbackErr) return 0, fallbackErr } - logHpp("GetDepresiasiTransfer", "done_fallback result=%f", result) return result, nil } kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID) if err != nil { - logHpp("GetDepresiasiTransfer", "get_project_flock_kandang_ids_error=%v", err) return 0, err } - logHpp("GetDepresiasiTransfer", "kandang_ids_growing=%v", kandangIDsGrowing) totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing) if err != nil { - logHpp("GetDepresiasiTransfer", "get_total_population_error=%v", err) return 0, err } - logHpp("GetDepresiasiTransfer", "total_population_flock_growing=%f", totalPopulationFlockGrowing) if totalPopulationFlockGrowing == 0 { - logHpp("GetDepresiasiTransfer", "total_population_flock_growing_zero return=0") return 0, nil } totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, endDate) if err != nil { - logHpp("GetDepresiasiTransfer", "get_total_depresiasi_flock_growing_error=%v", err) return 0, err } - logHpp("GetDepresiasiTransfer", "total_depresiasi_flock_growing=%f", totalDepresiasiFlockGrowing) result := (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing - logHpp("GetDepresiasiTransfer", "done result=%f", result) return result, nil } func (s *hppService) getManualDepresiasiTransferFallback(projectFlockKandangId uint) (float64, error) { - logHpp("getManualDepresiasiTransferFallback", "start project_flock_kandang_id=%d", projectFlockKandangId) projectFlockID, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId) if err != nil { - logHpp("getManualDepresiasiTransferFallback", "get_project_flock_id_error=%v", err) + return 0, err } - logHpp("getManualDepresiasiTransferFallback", "project_flock_id=%d", projectFlockID) if projectFlockID == 0 { - logHpp("getManualDepresiasiTransferFallback", "project_flock_id_zero return=0") + return 0, nil } manualCost, err := s.hppRepo.GetManualDepreciationCostByProjectFlockID(context.Background(), projectFlockID) if err != nil { - logHpp("getManualDepresiasiTransferFallback", "get_manual_depreciation_cost_error=%v", err) + return 0, err } - logHpp("getManualDepresiasiTransferFallback", "manual_cost=%f", manualCost) if manualCost <= 0 { - logHpp("getManualDepresiasiTransferFallback", "manual_cost_non_positive return=0") + return 0, nil } kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockID) if err != nil { - logHpp("getManualDepresiasiTransferFallback", "get_project_flock_kandang_ids_error=%v", err) + return 0, err } - logHpp("getManualDepresiasiTransferFallback", "kandang_ids=%v", kandangIDs) if len(kandangIDs) == 0 { - logHpp("getManualDepresiasiTransferFallback", "kandang_ids_empty return=0") + return 0, nil } totalUsageQty, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDs) if err != nil { - logHpp("getManualDepresiasiTransferFallback", "get_total_usage_qty_error=%v", err) + return 0, err } - logHpp("getManualDepresiasiTransferFallback", "total_usage_qty=%f", totalUsageQty) if totalUsageQty <= 0 { - logHpp("getManualDepresiasiTransferFallback", "total_usage_qty_non_positive return=0") + return 0, nil } kandangUsageQty, err := s.hppRepo.GetTotalPopulation(context.Background(), []uint{projectFlockKandangId}) if err != nil { - logHpp("getManualDepresiasiTransferFallback", "get_kandang_usage_qty_error=%v", err) + return 0, err } - logHpp("getManualDepresiasiTransferFallback", "kandang_usage_qty=%f", kandangUsageQty) if kandangUsageQty <= 0 { - logHpp("getManualDepresiasiTransferFallback", "kandang_usage_qty_non_positive return=0") + return 0, nil } result := manualCost * (kandangUsageQty / totalUsageQty) - logHpp("getManualDepresiasiTransferFallback", "done result=%f", result) return result, nil } func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) { - logHpp("GetHppEstimationDanRealisasi", "start total_production_cost=%f project_flock_kandang_id=%d start_date=%s end_date=%s", totalProductionCost, projectFlockKandangId, formatTimePtr(startDate), formatTimePtr(endDate)) if s.hppRepo == nil { - logHpp("GetHppEstimationDanRealisasi", "repo_nil return_empty_response") + return &HppCostResponse{}, nil } estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { - logHpp("GetHppEstimationDanRealisasi", "get_egg_produksi_error=%v", err) + return nil, err } - logHpp("GetHppEstimationDanRealisasi", "estim_pieces=%f estim_weight_kg=%f", estimPieces, estimWeightKg) realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate) if err != nil { - logHpp("GetHppEstimationDanRealisasi", "get_egg_terjual_error=%v", err) + return nil, err } - logHpp("GetHppEstimationDanRealisasi", "real_pieces=%f real_weight_kg=%f", realPieces, realWeightKg) estimation := HppCostDetail{ Total: totalProductionCost, @@ -403,7 +347,6 @@ func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, p if estimPieces > 0 { estimation.HargaButir = roundToTwoDecimals(totalProductionCost / estimPieces) } - logHpp("GetHppEstimationDanRealisasi", "estimation=%+v", estimation) real := HppCostDetail{ Total: totalProductionCost, @@ -416,19 +359,16 @@ func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, p if realPieces > 0 { real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces) } - logHpp("GetHppEstimationDanRealisasi", "real=%+v", real) result := &HppCostResponse{ Estimation: estimation, Real: real, } - logHpp("GetHppEstimationDanRealisasi", "done response=%+v", *result) return result, nil } func roundToTwoDecimals(value float64) float64 { result := math.Round(value*100) / 100 - logHpp("roundToTwoDecimals", "input=%f output=%f", value, result) return result } @@ -438,7 +378,3 @@ func formatTimePtr(value *time.Time) string { } return value.Format(time.RFC3339) } - -func logHpp(method, format string, args ...any) { - log.Printf("[HPP][%s] "+format, append([]any{method}, args...)...) -} diff --git a/internal/common/service/common.hppv2.model.go b/internal/common/service/common.hppv2.model.go index faf7cb33..f6f94bf9 100644 --- a/internal/common/service/common.hppv2.model.go +++ b/internal/common/service/common.hppv2.model.go @@ -48,6 +48,7 @@ type HppV2Breakdown struct { ProjectFlockKandangID uint `json:"project_flock_kandang_id"` ProjectFlockID uint `json:"project_flock_id"` ProjectFlockCategory string `json:"project_flock_category,omitempty"` + HouseType string `json:"house_type,omitempty"` KandangID uint `json:"kandang_id,omitempty"` KandangName string `json:"kandang_name,omitempty"` LocationID uint `json:"location_id,omitempty"` diff --git a/internal/common/service/common.hppv2.service.go b/internal/common/service/common.hppv2.service.go index bebbc9b3..c392cf8b 100644 --- a/internal/common/service/common.hppv2.service.go +++ b/internal/common/service/common.hppv2.service.go @@ -28,13 +28,14 @@ const ( hppV2PartManualCutover = "manual_cutover" hppV2PartDepreciationNormal = "normal_transfer" hppV2PartDepreciationCutover = "manual_cutover" + hppV2PartDepreciationFarmSnapshot = "farm_snapshot" hppV2ProrationPopulation = "growing_population_share" hppV2ProrationEggWeight = "laying_egg_weight_share" hppV2ProrationEggPiece = "laying_egg_piece_share" hppV2ScopePulletCost = "pullet_cost" hppV2ScopeProductionCost = "production_cost" hppV2CutoverFlagPakan = "PAKAN-CUTOVER" - hppV2CutoverFlagOvk = "OVK-CUTOVER" + hppV2CutoverFlagOvk = "OVK" ) type HppV2Service interface { @@ -115,57 +116,101 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t totalPulletCost := 0.0 totalProductionCost := 0.0 components := make([]HppV2Component, 0, 8) - appendComponent := func(component *HppV2Component) { + appendComponent := func(requestedCode string, component *HppV2Component) { + pulletBefore := totalPulletCost + productionBefore := totalProductionCost + if component == nil || (component.Total == 0 && len(component.Parts) == 0) { + utils.Log.Infof( + "HPP v2 component skipped: project_flock_kandang_id=%d period_date=%s component=%s reason=empty_or_nil total_pullet_cost=%.2f total_production_cost=%.2f", + projectFlockKandangId, + startOfDay.Format("2006-01-02"), + requestedCode, + totalPulletCost, + totalProductionCost, + ) return } + + pulletAdded := componentScopeTotal(component, hppV2ScopePulletCost) + productionAdded := componentScopeTotal(component, hppV2ScopeProductionCost) components = append(components, *component) - totalPulletCost += componentScopeTotal(component, hppV2ScopePulletCost) - totalProductionCost += componentScopeTotal(component, hppV2ScopeProductionCost) + totalPulletCost += pulletAdded + totalProductionCost += productionAdded + utils.Log.Infof( + "HPP v2 component applied: project_flock_kandang_id=%d period_date=%s component=%s component_total=%.2f pullet_added=%.2f production_added=%.2f total_pullet_before=%.2f total_pullet_after=%.2f total_production_before=%.2f total_production_after=%.2f parts_count=%d", + projectFlockKandangId, + startOfDay.Format("2006-01-02"), + component.Code, + component.Total, + pulletAdded, + productionAdded, + pulletBefore, + totalPulletCost, + productionBefore, + totalProductionCost, + len(component.Parts), + ) } - appendComponent(pakanComponent) + appendComponent(hppV2ComponentPakan, pakanComponent) ovkComponent, err := s.GetOvkBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } - appendComponent(ovkComponent) + appendComponent(hppV2ComponentOvk, ovkComponent) docComponent, err := s.GetDocChickinBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } - appendComponent(docComponent) + appendComponent(hppV2ComponentDocChickin, docComponent) directPulletComponent, err := s.GetDirectPulletPurchaseBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } - appendComponent(directPulletComponent) + appendComponent(hppV2ComponentDirectPulletPurchase, directPulletComponent) bopRegularComponent, err := s.GetBopRegularBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } - appendComponent(bopRegularComponent) + appendComponent(hppV2ComponentBopRegular, bopRegularComponent) bopEkspedisiComponent, err := s.GetBopEkspedisiBreakdown(projectFlockKandangId, &endOfDay) if err != nil { return nil, err } - appendComponent(bopEkspedisiComponent) + appendComponent(hppV2ComponentBopEksp, bopEkspedisiComponent) manualPulletComponent, err := s.getManualPulletCostComponent(projectFlockKandangId, contextRow, startOfDay) if err != nil { return nil, err } - appendComponent(manualPulletComponent) + appendComponent(hppV2ComponentManualPulletCost, manualPulletComponent) - depreciationComponent, err := s.getDepreciationComponent(projectFlockKandangId, contextRow, startOfDay, totalPulletCost) + depreciationComponent, err := s.getDepreciationComponent(projectFlockKandangId, contextRow, startOfDay, endOfDay, totalPulletCost) if err != nil { return nil, err } - appendComponent(depreciationComponent) + + depreciationCostToProduction := componentScopeTotal(depreciationComponent, hppV2ScopeProductionCost) + depreciationSource := "" + if depreciationComponent != nil && len(depreciationComponent.Parts) > 0 { + depreciationSource = depreciationComponent.Parts[0].Code + } + productionCostBeforeDepreciation := totalProductionCost + appendComponent(hppV2ComponentDepreciation, depreciationComponent) + utils.Log.Infof( + "HPP v2 depreciation cost applied: project_flock_kandang_id=%d period_date=%s depreciation_source=%s depreciation_cost=%.2f production_cost_before=%.2f production_cost_after=%.2f", + projectFlockKandangId, + startOfDay.Format("2006-01-02"), + depreciationSource, + depreciationCostToProduction, + productionCostBeforeDepreciation, + totalProductionCost, + ) hppCost, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay) if err != nil { @@ -179,6 +224,7 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t ProjectFlockKandangID: projectFlockKandangId, ProjectFlockID: contextRow.ProjectFlockID, ProjectFlockCategory: contextRow.ProjectFlockCategory, + HouseType: contextRow.HouseType, KandangID: contextRow.KandangID, KandangName: contextRow.KandangName, LocationID: contextRow.LocationID, @@ -1022,9 +1068,28 @@ func (s *hppV2Service) getDepreciationComponent( projectFlockKandangId uint, contextRow *commonRepo.HppV2ProjectFlockKandangContext, periodDate time.Time, + endDate time.Time, totalPulletCost float64, ) (*HppV2Component, error) { - if s.hppRepo == nil || contextRow == nil || totalPulletCost <= 0 { + if s.hppRepo == nil || contextRow == nil { + return nil, nil + } + + snapshotPart, err := s.buildFarmSnapshotDepreciationPart(projectFlockKandangId, contextRow, periodDate, endDate) + if err != nil { + return nil, err + } + if snapshotPart != nil { + return &HppV2Component{ + Code: hppV2ComponentDepreciation, + Title: "Depreciation", + Scopes: []string{hppV2ScopeProductionCost}, + Total: snapshotPart.Total, + Parts: []HppV2ComponentPart{*snapshotPart}, + }, nil + } + + if totalPulletCost <= 0 { return nil, nil } @@ -1058,6 +1123,101 @@ func (s *hppV2Service) getDepreciationComponent( }, nil } +func (s *hppV2Service) buildFarmSnapshotDepreciationPart( + projectFlockKandangId uint, + contextRow *commonRepo.HppV2ProjectFlockKandangContext, + periodDate time.Time, + endDate time.Time, +) (*HppV2ComponentPart, error) { + if contextRow == nil { + return nil, nil + } + + snapshot, err := s.hppRepo.GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(context.Background(), contextRow.ProjectFlockID, periodDate) + if err != nil { + return nil, err + } + if snapshot == nil || snapshot.DepreciationValue <= 0 { + return nil, nil + } + + farmPFKIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), contextRow.ProjectFlockID) + if err != nil { + return nil, err + } + if len(farmPFKIDs) == 0 { + return nil, nil + } + + end := endDate + targetPieces, targetWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, &end) + if err != nil { + return nil, err + } + farmPieces, farmWeight, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), farmPFKIDs, &end) + if err != nil { + return nil, err + } + + basis := hppV2ProrationEggWeight + numerator := targetWeight + denominator := farmWeight + if denominator <= 0 { + basis = hppV2ProrationEggPiece + numerator = targetPieces + denominator = farmPieces + } + if denominator <= 0 { + return nil, nil + } + + ratio := numerator / denominator + if ratio <= 0 { + return nil, nil + } + + appliedDepreciation := snapshot.DepreciationValue * ratio + if appliedDepreciation <= 0 { + return nil, nil + } + appliedPulletCostDayN := snapshot.PulletCostDayNTotal * ratio + depreciationPercent := snapshot.DepreciationPercentEffective + if appliedPulletCostDayN > 0 { + depreciationPercent = (appliedDepreciation / appliedPulletCostDayN) * 100 + } + + return &HppV2ComponentPart{ + Code: hppV2PartDepreciationFarmSnapshot, + Title: "Farm Snapshot", + Scopes: []string{hppV2ScopeProductionCost}, + Total: appliedDepreciation, + Proration: &HppV2Proration{ + Basis: basis, + Numerator: numerator, + Denominator: denominator, + Ratio: ratio, + }, + Details: map[string]any{ + "basis_total": snapshot.DepreciationValue, + "pullet_cost_day_n": appliedPulletCostDayN, + "depreciation_percent": depreciationPercent, + "snapshot_id": snapshot.ID, + "snapshot_period_date": formatDateOnly(snapshot.PeriodDate), + "snapshot_project_flock": snapshot.ProjectFlockID, + }, + References: []HppV2Reference{ + { + Type: "farm_depreciation_snapshot", + ID: snapshot.ID, + Date: formatDateOnly(snapshot.PeriodDate), + Qty: 1, + Total: snapshot.DepreciationValue, + AppliedTotal: appliedDepreciation, + }, + }, + }, nil +} + func (s *hppV2Service) buildNormalTransferDepreciationPart( contextRow *commonRepo.HppV2ProjectFlockKandangContext, transferInput *commonRepo.HppV2LatestTransferInputRow, @@ -1211,17 +1371,40 @@ func (s *hppV2Service) buildManualCutoverDepreciationPart( } func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) { + utils.Log.Infof( + "GetHppEstimationDanRealisasi started: project_flock_kandang_id=%d total_production_cost=%.2f start_date=%s end_date=%s", + projectFlockKandangId, + totalProductionCost, + formatTimePtr(startDate), + formatTimePtr(endDate), + ) + if s.hppRepo == nil { + utils.Log.Warnf( + "GetHppEstimationDanRealisasi skipped: hpp repository is nil (project_flock_kandang_id=%d)", + projectFlockKandangId, + ) return &HppCostResponse{}, nil } estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) if err != nil { + utils.Log.WithError(err).Errorf( + "GetHppEstimationDanRealisasi failed to get estimation egg production: project_flock_kandang_id=%d end_date=%s", + projectFlockKandangId, + formatTimePtr(endDate), + ) return nil, err } realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate) if err != nil { + utils.Log.WithError(err).Errorf( + "GetHppEstimationDanRealisasi failed to get realization egg sales: project_flock_kandang_id=%d start_date=%s end_date=%s", + projectFlockKandangId, + formatTimePtr(startDate), + formatTimePtr(endDate), + ) return nil, err } @@ -1249,6 +1432,20 @@ func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64, real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces) } + utils.Log.Infof( + "GetHppEstimationDanRealisasi success: project_flock_kandang_id=%d estimation_butir=%.2f estimation_kg=%.2f estimation_harga_butir=%.2f estimation_harga_kg=%.2f real_butir=%.2f real_kg=%.2f real_harga_butir=%.2f real_harga_kg=%.2f totalProductionCost=%.2f", + projectFlockKandangId, + estimation.Butir, + estimation.Kg, + estimation.HargaButir, + estimation.HargaKg, + real.Butir, + real.Kg, + real.HargaButir, + real.HargaKg, + totalProductionCost, + ) + return &HppCostResponse{ Estimation: estimation, Real: real, diff --git a/internal/common/service/common.hppv2.service_test.go b/internal/common/service/common.hppv2.service_test.go index 574bad8a..a2a8d27e 100644 --- a/internal/common/service/common.hppv2.service_test.go +++ b/internal/common/service/common.hppv2.service_test.go @@ -17,6 +17,7 @@ type hppV2RepoStub struct { pfkIDsByProject map[uint][]uint latestTransferByPFK map[uint]*commonRepo.HppV2LatestTransferInputRow manualInputByProject map[uint]*commonRepo.HppV2ManualDepreciationInputRow + snapshotByProjectKey map[string]*commonRepo.HppV2FarmDepreciationSnapshotRow chickInDateByProject map[uint]*time.Time depreciationByHouse map[string]map[int]float64 usageRowsByKey map[string][]commonRepo.HppV2UsageCostRow @@ -59,6 +60,13 @@ func (s *hppV2RepoStub) GetManualDepreciationInputByProjectFlockID(_ context.Con return s.manualInputByProject[projectFlockID], nil } +func (s *hppV2RepoStub) GetFarmDepreciationSnapshotByProjectFlockIDAndPeriod(_ context.Context, projectFlockID uint, periodDate time.Time) (*commonRepo.HppV2FarmDepreciationSnapshotRow, error) { + if s.snapshotByProjectKey == nil { + return nil, nil + } + return s.snapshotByProjectKey[fmt.Sprintf("%d|%s", projectFlockID, periodDate.Format("2006-01-02"))], nil +} + func (s *hppV2RepoStub) GetEarliestChickInDateByProjectFlockID(_ context.Context, projectFlockID uint) (*time.Time, error) { return s.chickInDateByProject[projectFlockID], nil } @@ -319,10 +327,10 @@ func TestHppV2CalculateHppBreakdown_IncludesOvkComponent(t *testing.T) { }, }, adjustRowsByKey: map[string][]commonRepo.HppV2AdjustmentCostRow{ - stubKey([]uint{301, 302}, []string{"OVK-CUTOVER"}): { + stubKey([]uint{301, 302}, []string{"OVK"}): { {AdjustmentID: 8201, ProductID: 34, ProductName: "OVK Growing Cut-over", Qty: 10, Price: 10, GrandTotal: 100}, }, - stubKey([]uint{30}, []string{"OVK-CUTOVER"}): { + stubKey([]uint{30}, []string{"OVK"}): { {AdjustmentID: 8202, ProductID: 35, ProductName: "OVK Laying Cut-over", Qty: 5, Price: 10, GrandTotal: 50}, }, }, @@ -743,6 +751,82 @@ func TestHppV2CalculateHppBreakdown_AddsDepreciationForManualCutoverFromCutoverD } } +func TestHppV2CalculateHppBreakdown_UsesFarmSnapshotDepreciationProratedByEggProduction(t *testing.T) { + reportDate := mustTime(t, "2026-06-05") + + repo := &hppV2RepoStub{ + contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{ + 70: { + ProjectFlockKandangID: 70, + ProjectFlockID: 15, + ProjectFlockCategory: "LAYING", + KandangID: 700, + KandangName: "Kandang Snapshot", + LocationID: 25, + HouseType: "close_house", + }, + }, + pfkIDsByProject: map[uint][]uint{ + 15: {70, 71}, + }, + snapshotByProjectKey: map[string]*commonRepo.HppV2FarmDepreciationSnapshotRow{ + "15|2026-06-05": { + ID: 901, + ProjectFlockID: 15, + PeriodDate: reportDate, + DepreciationPercentEffective: 10, + DepreciationValue: 1000, + PulletCostDayNTotal: 10000, + }, + }, + eggProductionByPFK: map[uint]struct { + pieces float64 + kg float64 + }{ + 70: {pieces: 200, kg: 20}, + 71: {pieces: 800, kg: 80}, + }, + } + + svc := NewHppV2Service(repo) + result, err := svc.CalculateHppBreakdown(70, &reportDate) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if result == nil { + t.Fatal("expected breakdown result") + } + + var depreciation *HppV2Component + for i := range result.Components { + if result.Components[i].Code == hppV2ComponentDepreciation { + depreciation = &result.Components[i] + break + } + } + if depreciation == nil { + t.Fatal("expected depreciation component") + } + if depreciation.Total != 200 { + t.Fatalf("expected depreciation total 200, got %v", depreciation.Total) + } + if result.TotalProductionCost != 200 { + t.Fatalf("expected total production cost 200, got %v", result.TotalProductionCost) + } + if len(depreciation.Parts) != 1 { + t.Fatalf("expected one depreciation part, got %d", len(depreciation.Parts)) + } + if depreciation.Parts[0].Code != hppV2PartDepreciationFarmSnapshot { + t.Fatalf("expected farm snapshot depreciation part, got %s", depreciation.Parts[0].Code) + } + if depreciation.Parts[0].Proration == nil || depreciation.Parts[0].Proration.Ratio != 0.2 { + t.Fatalf("expected proration ratio 0.2, got %+v", depreciation.Parts[0].Proration) + } + if depreciation.Parts[0].Details["snapshot_id"] != uint(901) { + t.Fatalf("expected snapshot id 901, got %+v", depreciation.Parts[0].Details) + } +} + func stubKey(ids []uint, flags []string) string { idParts := make([]string, 0, len(ids)) for _, id := range ids { diff --git a/internal/modules/repports/services/repport.expense_depreciation_test.go b/internal/modules/repports/services/repport.expense_depreciation_test.go new file mode 100644 index 00000000..820fbaa6 --- /dev/null +++ b/internal/modules/repports/services/repport.expense_depreciation_test.go @@ -0,0 +1,445 @@ +package service + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + dto "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" + repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" + "gorm.io/gorm" +) + +type expenseDepreciationRepoMock struct { + repportRepo.ExpenseDepreciationRepository + manualInputs []repportRepo.FarmDepreciationManualInputRow + + upsertedRow *entity.FarmDepreciationManualInput + deleteCalled bool + deleteDate time.Time + deleteFarmIDs []uint +} + +func (m *expenseDepreciationRepoMock) DB() *gorm.DB { + return nil +} + +func (m *expenseDepreciationRepoMock) UpsertManualInput(_ context.Context, row *entity.FarmDepreciationManualInput) error { + if row == nil { + return nil + } + cloned := *row + if cloned.Id == 0 { + cloned.Id = 123 + } + m.upsertedRow = &cloned + row.Id = cloned.Id + return nil +} + +func (m *expenseDepreciationRepoMock) DeleteSnapshotsFromDate(_ context.Context, fromDate time.Time, farmIDs []uint) error { + m.deleteCalled = true + m.deleteDate = fromDate + m.deleteFarmIDs = append([]uint{}, farmIDs...) + return nil +} + +func (m *expenseDepreciationRepoMock) GetLatestManualInputsByFarms(_ context.Context, _ []int64, _ []int64, _ []int64) ([]repportRepo.FarmDepreciationManualInputRow, error) { + return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil +} + +type hppCostRepoMock struct { + commonRepo.HppCostRepository + kandangIDsByFarm map[uint][]uint +} + +func (m *hppCostRepoMock) GetProjectFlockKandangIDs(_ context.Context, projectFlockID uint) ([]uint, error) { + return append([]uint{}, m.kandangIDsByFarm[projectFlockID]...), nil +} + +type hppV2ServiceMock struct { + approvalService.HppV2Service + breakdownByPFK map[uint]*approvalService.HppV2Breakdown +} + +func (m *hppV2ServiceMock) CalculateHppBreakdown(projectFlockKandangId uint, _ *time.Time) (*approvalService.HppV2Breakdown, error) { + return m.breakdownByPFK[projectFlockKandangId], nil +} + +func TestComputeExpenseDepreciationSnapshots_FromHppV2NormalTransfer(t *testing.T) { + periodDate := mustJakartaDate(t, "2026-06-05") + svc := &repportService{ + HppCostRepo: &hppCostRepoMock{ + kandangIDsByFarm: map[uint][]uint{ + 1: {10}, + }, + }, + HppV2Svc: &hppV2ServiceMock{ + breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{ + 10: { + ProjectFlockKandangID: 10, + KandangID: 100, + KandangName: "Kandang A", + HouseType: "close_house", + Components: []approvalService.HppV2Component{ + { + Code: "DEPRECIATION", + Title: "Depreciation", + Total: 100, + Parts: []approvalService.HppV2ComponentPart{ + { + Code: "normal_transfer", + Total: 100, + Details: map[string]any{ + "schedule_day": 2, + "depreciation_percent": 10.0, + "pullet_cost_day_n": 1000.0, + "source_project_flock_id": 77, + "origin_date": "2026-01-01", + }, + References: []approvalService.HppV2Reference{ + { + Type: "laying_transfer", + ID: 701, + Date: "2026-05-20", + Qty: 150, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + rows, err := svc.computeExpenseDepreciationSnapshots(context.Background(), periodDate, []uint{1}, map[uint]string{1: "Farm A"}) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(rows) != 1 { + t.Fatalf("expected 1 row, got %d", len(rows)) + } + if rows[0].DepreciationValue != 100 { + t.Fatalf("expected depreciation value 100, got %v", rows[0].DepreciationValue) + } + if rows[0].PulletCostDayNTotal != 1000 { + t.Fatalf("expected pullet cost day n 1000, got %v", rows[0].PulletCostDayNTotal) + } + assertFloatEqual(t, rows[0].DepreciationPercentEffective, 10) + + components := decodeDepreciationComponents(t, rows[0].Components) + if components.KandangCount != 1 { + t.Fatalf("expected kandang_count 1, got %d", components.KandangCount) + } + entry := components.Kandang[0] + if entry.ProjectFlockKandangID != 10 || entry.KandangID != 100 || entry.KandangName != "Kandang A" { + t.Fatalf("unexpected kandang identity: %+v", entry) + } + if entry.TransferID != 701 || entry.TransferDate != "2026-05-20" || entry.TransferQty != 150 { + t.Fatalf("unexpected transfer metadata: %+v", entry) + } + if entry.DepreciationSource != "normal_transfer" { + t.Fatalf("expected depreciation_source normal_transfer, got %q", entry.DepreciationSource) + } + if entry.ManualInputID != nil || entry.CutoverDate != "" || entry.StartScheduleDay != nil { + t.Fatalf("expected manual fields empty for normal transfer, got %+v", entry) + } +} + +func TestComputeExpenseDepreciationSnapshots_FromHppV2ManualCutover(t *testing.T) { + periodDate := mustJakartaDate(t, "2026-06-05") + svc := &repportService{ + HppCostRepo: &hppCostRepoMock{ + kandangIDsByFarm: map[uint][]uint{ + 2: {20}, + }, + }, + HppV2Svc: &hppV2ServiceMock{ + breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{ + 20: { + ProjectFlockKandangID: 20, + KandangID: 200, + KandangName: "Kandang B", + HouseType: "open_house", + Components: []approvalService.HppV2Component{ + { + Code: "DEPRECIATION", + Title: "Depreciation", + Total: 200, + Parts: []approvalService.HppV2ComponentPart{ + { + Code: "manual_cutover", + Total: 200, + Details: map[string]any{ + "schedule_day": 2, + "start_schedule_day": 2, + "depreciation_percent": 25.0, + "pullet_cost_day_n": 800.0, + "manual_input_id": 901, + "cutover_date": "2026-06-01", + "origin_date": "2026-01-01", + }, + References: []approvalService.HppV2Reference{ + { + Type: "farm_depreciation_manual_input", + ID: 901, + Date: "2026-06-01", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + rows, err := svc.computeExpenseDepreciationSnapshots(context.Background(), periodDate, []uint{2}, map[uint]string{2: "Farm B"}) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(rows) != 1 { + t.Fatalf("expected 1 row, got %d", len(rows)) + } + assertFloatEqual(t, rows[0].DepreciationPercentEffective, 25) + + components := decodeDepreciationComponents(t, rows[0].Components) + if components.KandangCount != 1 { + t.Fatalf("expected kandang_count 1, got %d", components.KandangCount) + } + entry := components.Kandang[0] + if entry.DepreciationSource != "manual_cutover" { + t.Fatalf("expected depreciation_source manual_cutover, got %q", entry.DepreciationSource) + } + if entry.TransferID != 0 || entry.TransferDate != "" || entry.TransferQty != 0 { + t.Fatalf("expected transfer fields empty for manual path, got %+v", entry) + } + if entry.ManualInputID == nil || *entry.ManualInputID != 901 { + t.Fatalf("expected manual_input_id 901, got %+v", entry.ManualInputID) + } + if entry.CutoverDate != "2026-06-01" || entry.OriginDate != "2026-01-01" { + t.Fatalf("unexpected manual date fields: %+v", entry) + } + if entry.StartScheduleDay == nil || *entry.StartScheduleDay != 2 { + t.Fatalf("expected start_schedule_day 2, got %+v", entry.StartScheduleDay) + } +} + +func TestComputeExpenseDepreciationSnapshots_AggregatesMultipleKandang(t *testing.T) { + periodDate := mustJakartaDate(t, "2026-06-05") + svc := &repportService{ + HppCostRepo: &hppCostRepoMock{ + kandangIDsByFarm: map[uint][]uint{ + 3: {30, 31}, + }, + }, + HppV2Svc: &hppV2ServiceMock{ + breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{ + 30: { + ProjectFlockKandangID: 30, + KandangID: 300, + KandangName: "Kandang C1", + Components: []approvalService.HppV2Component{ + { + Code: "DEPRECIATION", + Parts: []approvalService.HppV2ComponentPart{ + { + Code: "normal_transfer", + Total: 50, + Details: map[string]any{ + "schedule_day": 1, + "depreciation_percent": 10.0, + "pullet_cost_day_n": 500.0, + }, + }, + }, + }, + }, + }, + 31: { + ProjectFlockKandangID: 31, + KandangID: 301, + KandangName: "Kandang C2", + Components: []approvalService.HppV2Component{ + { + Code: "DEPRECIATION", + Parts: []approvalService.HppV2ComponentPart{ + { + Code: "normal_transfer", + Total: 100, + Details: map[string]any{ + "schedule_day": 2, + "depreciation_percent": 10.0, + "pullet_cost_day_n": 1000.0, + }, + }, + }, + }, + }, + }, + }, + }, + } + + rows, err := svc.computeExpenseDepreciationSnapshots(context.Background(), periodDate, []uint{3}, map[uint]string{3: "Farm C"}) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(rows) != 1 { + t.Fatalf("expected 1 row, got %d", len(rows)) + } + if rows[0].DepreciationValue != 150 { + t.Fatalf("expected depreciation value 150, got %v", rows[0].DepreciationValue) + } + if rows[0].PulletCostDayNTotal != 1500 { + t.Fatalf("expected pullet cost day n 1500, got %v", rows[0].PulletCostDayNTotal) + } + assertFloatEqual(t, rows[0].DepreciationPercentEffective, 10) + + components := decodeDepreciationComponents(t, rows[0].Components) + if components.KandangCount != 2 { + t.Fatalf("expected kandang_count 2, got %d", components.KandangCount) + } +} + +func TestComputeExpenseDepreciationSnapshots_ZeroWhenDepreciationMissing(t *testing.T) { + periodDate := mustJakartaDate(t, "2026-06-05") + svc := &repportService{ + HppCostRepo: &hppCostRepoMock{ + kandangIDsByFarm: map[uint][]uint{ + 4: {40}, + }, + }, + HppV2Svc: &hppV2ServiceMock{ + breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{ + 40: { + ProjectFlockKandangID: 40, + KandangID: 400, + KandangName: "Kandang D", + Components: []approvalService.HppV2Component{ + {Code: "PAKAN", Total: 123}, + }, + }, + }, + }, + } + + rows, err := svc.computeExpenseDepreciationSnapshots(context.Background(), periodDate, []uint{4}, map[uint]string{4: "Farm D"}) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(rows) != 1 { + t.Fatalf("expected 1 row, got %d", len(rows)) + } + if rows[0].DepreciationValue != 0 || rows[0].PulletCostDayNTotal != 0 || rows[0].DepreciationPercentEffective != 0 { + t.Fatalf("expected zero snapshot values, got %+v", rows[0]) + } + components := decodeDepreciationComponents(t, rows[0].Components) + if components.KandangCount != 0 || len(components.Kandang) != 0 { + t.Fatalf("expected empty components, got %+v", components) + } +} + +func TestUpsertExpenseDepreciationManualInput_InvalidatesSnapshotsFromCutoverDate(t *testing.T) { + repo := &expenseDepreciationRepoMock{ + manualInputs: []repportRepo.FarmDepreciationManualInputRow{ + { + Id: 123, + ProjectFlockID: 99, + FarmName: "Farm Z", + TotalCost: 1000, + CutoverDate: mustJakartaDate(t, "2026-06-01"), + }, + }, + } + + svc := &repportService{ + Validate: validator.New(), + ExpenseDepreciationRepo: repo, + } + + reqPayload := &validation.ExpenseDepreciationManualInputUpsert{ + ProjectFlockID: 99, + TotalCost: 1000, + CutoverDate: "2026-06-01", + } + + app := fiber.New() + var response *dto.ExpenseDepreciationManualInputRowDTO + app.Put("/", func(c *fiber.Ctx) error { + result, err := svc.UpsertExpenseDepreciationManualInput(c, reqPayload) + if err != nil { + return err + } + response = result + return c.SendStatus(fiber.StatusOK) + }) + + httpResp, err := app.Test(httptest.NewRequest(http.MethodPut, "/", nil)) + if err != nil { + t.Fatalf("expected no app error, got %v", err) + } + if httpResp.StatusCode != fiber.StatusOK { + t.Fatalf("expected status %d, got %d", fiber.StatusOK, httpResp.StatusCode) + } + if !repo.deleteCalled { + t.Fatal("expected DeleteSnapshotsFromDate to be called") + } + if len(repo.deleteFarmIDs) != 1 || repo.deleteFarmIDs[0] != 99 { + t.Fatalf("expected delete farm ids [99], got %v", repo.deleteFarmIDs) + } + if repo.deleteDate.Format("2006-01-02") != "2026-06-01" { + t.Fatalf("expected delete date 2026-06-01, got %s", repo.deleteDate.Format("2006-01-02")) + } + if response == nil { + t.Fatal("expected response") + } + if response.FarmName != "Farm Z" { + t.Fatalf("expected farm name Farm Z, got %s", response.FarmName) + } +} + +func decodeDepreciationComponents(t *testing.T, raw []byte) depreciationFarmComponents { + t.Helper() + var out depreciationFarmComponents + if len(raw) == 0 { + return out + } + if err := json.Unmarshal(raw, &out); err != nil { + t.Fatalf("failed to decode components: %v", err) + } + return out +} + +func mustJakartaDate(t *testing.T, raw string) time.Time { + t.Helper() + location, err := time.LoadLocation("Asia/Jakarta") + if err != nil { + t.Fatalf("failed loading timezone: %v", err) + } + value, err := time.ParseInLocation("2006-01-02", raw, location) + if err != nil { + t.Fatalf("failed parsing date %q: %v", raw, err) + } + return value +} + +func assertFloatEqual(t *testing.T, got float64, want float64) { + t.Helper() + const epsilon = 0.000001 + if got > want+epsilon || got < want-epsilon { + t.Fatalf("expected %.6f, got %.6f", want, got) + } +} diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index edd90f04..3ffa1e09 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -416,6 +416,13 @@ func (s *repportService) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, re if err := s.ExpenseDepreciationRepo.UpsertManualInput(ctx.Context(), &row); err != nil { return nil, err } + if err := s.ExpenseDepreciationRepo.DeleteSnapshotsFromDate( + ctx.Context(), + cutoverDate, + []uint{row.ProjectFlockId}, + ); err != nil { + return nil, err + } response := &dto.ExpenseDepreciationManualInputRowDTO{ ID: int64(row.Id), @@ -456,6 +463,11 @@ type depreciationKandangComponent struct { TransferQty float64 `json:"transfer_qty"` PulletCostDayN float64 `json:"pullet_cost_day_n"` DepreciationValue float64 `json:"depreciation_value"` + DepreciationSource string `json:"depreciation_source,omitempty"` + ManualInputID *uint `json:"manual_input_id,omitempty"` + CutoverDate string `json:"cutover_date,omitempty"` + OriginDate string `json:"origin_date,omitempty"` + StartScheduleDay *int `json:"start_schedule_day,omitempty"` } type depreciationFarmComponents struct { @@ -469,124 +481,98 @@ func (s *repportService) computeExpenseDepreciationSnapshots( farmIDs []uint, farmNameByID map[uint]string, ) ([]entity.FarmDepreciationSnapshot, error) { + _ = farmNameByID + if len(farmIDs) == 0 { return []entity.FarmDepreciationSnapshot{}, nil } - - inputRows, err := s.ExpenseDepreciationRepo.GetLatestTransferInputsByFarms(ctx, periodDate, farmIDs) - if err != nil { - return nil, err + if s.HppCostRepo == nil { + return nil, errors.New("hpp cost repository is not configured") } - - groupedByFarm := make(map[uint][]repportRepo.FarmDepreciationLatestTransferRow, len(farmIDs)) - houseTypeSet := make(map[string]struct{}) - maxDay := 0 - - for _, row := range inputRows { - groupedByFarm[row.ProjectFlockID] = append(groupedByFarm[row.ProjectFlockID], row) - dayN := approvalService.DepreciationScheduleDay(row.TransferDate, periodDate, valueOrEmptyString(row.HouseType)) - if dayN > maxDay { - maxDay = dayN - } - houseType := approvalService.NormalizeDepreciationHouseType(valueOrEmptyString(row.HouseType)) - if houseType != "" { - houseTypeSet[houseType] = struct{}{} - } + if s.HppV2Svc == nil { + return nil, errors.New("hpp v2 service is not configured") } - houseTypes := make([]string, 0, len(houseTypeSet)) - for houseType := range houseTypeSet { - houseTypes = append(houseTypes, houseType) - } - sort.Strings(houseTypes) - - percentByHouseType, err := s.ExpenseDepreciationRepo.GetDepreciationPercents(ctx, houseTypes, maxDay) - if err != nil { - return nil, err - } - - type sourceCostCacheItem struct { - totalDepCost float64 - } - sourceCostCache := make(map[string]sourceCostCacheItem) - sourcePopulationCache := make(map[uint]float64) - result := make([]entity.FarmDepreciationSnapshot, 0, len(farmIDs)) for _, farmID := range farmIDs { - farmRows := groupedByFarm[farmID] + kandangIDs, err := s.HppCostRepo.GetProjectFlockKandangIDs(ctx, farmID) + if err != nil { + return nil, err + } + components := depreciationFarmComponents{ - KandangCount: len(farmRows), - Kandang: make([]depreciationKandangComponent, 0, len(farmRows)), + Kandang: make([]depreciationKandangComponent, 0, len(kandangIDs)), } totalDepreciationValue := 0.0 totalPulletCostDayN := 0.0 - for _, row := range farmRows { - dayN := approvalService.DepreciationScheduleDay(row.TransferDate, periodDate, valueOrEmptyString(row.HouseType)) - houseType := approvalService.NormalizeDepreciationHouseType(valueOrEmptyString(row.HouseType)) + for _, kandangID := range kandangIDs { + breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &periodDate) + if err != nil { + return nil, err + } + if breakdown == nil { + continue + } - transferDateKey := row.TransferDate.Format("2006-01-02") - cacheKey := fmt.Sprintf("%d|%s", row.SourceProjectFlockID, transferDateKey) - cached, exists := sourceCostCache[cacheKey] - if !exists { - endOfDay := row.TransferDate.Add(24 * time.Hour) - sourceDepCost, calcErr := s.HppSvc.GetTotalDepresiasiFlockGrowing(row.SourceProjectFlockID, &endOfDay) - if calcErr != nil { - return nil, calcErr + depreciationComponent := hppV2FindDepreciationComponent(breakdown) + if depreciationComponent == nil { + continue + } + + for _, part := range depreciationComponent.Parts { + if part.Total <= 0 { + continue } - cached = sourceCostCacheItem{totalDepCost: sourceDepCost} - sourceCostCache[cacheKey] = cached - } - sourcePopulation, popExists := sourcePopulationCache[row.SourceProjectFlockID] - if !popExists { - if s.HppCostRepo == nil { - sourcePopulation = 0 - } else { - kandangIDs, idsErr := s.HppCostRepo.GetProjectFlockKandangIDs(ctx, row.SourceProjectFlockID) - if idsErr != nil { - return nil, idsErr - } - population, popErr := s.HppCostRepo.GetTotalPopulation(ctx, kandangIDs) - if popErr != nil { - return nil, popErr - } - sourcePopulation = population + houseType := approvalService.NormalizeDepreciationHouseType(breakdown.HouseType) + component := depreciationKandangComponent{ + ProjectFlockKandangID: breakdown.ProjectFlockKandangID, + KandangID: breakdown.KandangID, + KandangName: breakdown.KandangName, + SourceProjectFlockID: hppV2DetailUint(part.Details, "source_project_flock_id"), + HouseType: houseType, + DayN: hppV2DetailInt(part.Details, "schedule_day"), + DepreciationPercent: hppV2DetailFloat(part.Details, "depreciation_percent"), + PulletCostDayN: hppV2DetailFloat(part.Details, "pullet_cost_day_n"), + DepreciationValue: part.Total, + DepreciationSource: part.Code, + OriginDate: hppV2DetailString(part.Details, "origin_date"), } - sourcePopulationCache[row.SourceProjectFlockID] = sourcePopulation + + if component.HouseType == "" { + component.HouseType = approvalService.NormalizeDepreciationHouseType(hppV2DetailString(part.Details, "house_type")) + } + + if ref := hppV2FindReference(part.References, "laying_transfer"); ref != nil { + component.TransferID = ref.ID + component.TransferDate = ref.Date + component.TransferQty = ref.Qty + } + + if part.Code == "manual_cutover" { + if startDay := hppV2DetailInt(part.Details, "start_schedule_day"); startDay > 0 { + component.StartScheduleDay = &startDay + } + component.CutoverDate = hppV2DetailString(part.Details, "cutover_date") + if manualID := hppV2DetailUint(part.Details, "manual_input_id"); manualID > 0 { + component.ManualInputID = &manualID + } + if component.ManualInputID == nil { + if ref := hppV2FindReference(part.References, "farm_depreciation_manual_input"); ref != nil && ref.ID > 0 { + manualID := ref.ID + component.ManualInputID = &manualID + } + } + } + + totalPulletCostDayN += component.PulletCostDayN + totalDepreciationValue += component.DepreciationValue + components.Kandang = append(components.Kandang, component) } - - initialPulletCost := 0.0 - if sourcePopulation > 0 { - initialPulletCost = (cached.totalDepCost * row.TransferQty) / sourcePopulation - } - - pulletCostDayN, depreciationValue, depreciationPercent := approvalService.CalculateDepreciationAtDayN( - initialPulletCost, - dayN, - houseType, - percentByHouseType, - ) - - totalPulletCostDayN += pulletCostDayN - totalDepreciationValue += depreciationValue - - components.Kandang = append(components.Kandang, depreciationKandangComponent{ - ProjectFlockKandangID: row.ProjectFlockKandangID, - KandangID: row.KandangID, - KandangName: row.KandangName, - TransferID: row.TransferID, - TransferDate: row.TransferDate.Format("2006-01-02"), - SourceProjectFlockID: row.SourceProjectFlockID, - HouseType: houseType, - DayN: dayN, - DepreciationPercent: depreciationPercent, - TransferQty: row.TransferQty, - PulletCostDayN: pulletCostDayN, - DepreciationValue: depreciationValue, - }) } + components.KandangCount = len(components.Kandang) effectivePercent := approvalService.CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN) componentsJSON, marshalErr := json.Marshal(components) @@ -607,6 +593,106 @@ func (s *repportService) computeExpenseDepreciationSnapshots( return result, nil } +func hppV2FindDepreciationComponent(breakdown *approvalService.HppV2Breakdown) *approvalService.HppV2Component { + if breakdown == nil { + return nil + } + for idx := range breakdown.Components { + if breakdown.Components[idx].Code == "DEPRECIATION" { + return &breakdown.Components[idx] + } + } + return nil +} + +func hppV2FindReference(references []approvalService.HppV2Reference, refType string) *approvalService.HppV2Reference { + if refType == "" { + return nil + } + for idx := range references { + if references[idx].Type == refType { + return &references[idx] + } + } + return nil +} + +func hppV2DetailFloat(details map[string]any, key string) float64 { + if details == nil || key == "" { + return 0 + } + + raw, exists := details[key] + if !exists || raw == nil { + return 0 + } + + switch value := raw.(type) { + case float64: + return value + case float32: + return float64(value) + case int: + return float64(value) + case int8: + return float64(value) + case int16: + return float64(value) + case int32: + return float64(value) + case int64: + return float64(value) + case uint: + return float64(value) + case uint8: + return float64(value) + case uint16: + return float64(value) + case uint32: + return float64(value) + case uint64: + return float64(value) + case string: + parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64) + if err != nil { + return 0 + } + return parsed + default: + return 0 + } +} + +func hppV2DetailInt(details map[string]any, key string) int { + return int(math.Round(hppV2DetailFloat(details, key))) +} + +func hppV2DetailUint(details map[string]any, key string) uint { + value := hppV2DetailInt(details, key) + if value < 0 { + return 0 + } + return uint(value) +} + +func hppV2DetailString(details map[string]any, key string) string { + if details == nil || key == "" { + return "" + } + raw, exists := details[key] + if !exists || raw == nil { + return "" + } + switch value := raw.(type) { + case string: + return value + case time.Time: + return value.Format("2006-01-02") + default: + return fmt.Sprintf("%v", value) + } +} + func parseSnapshotComponents(raw []byte) any { if len(raw) == 0 { return map[string]any{} @@ -2280,13 +2366,15 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes } if hppCost != nil { eggPiecesFloatRemaining = hppCost.Estimation.Butir - hppCost.Real.Butir - eggHpp = hppCost.Estimation.HargaKg + // eggHpp = hppCost.Estimation.HargaKg + eggHpp = hppCost.Real.HargaKg eggTotalPiecesFloat = hppCost.Estimation.Butir eggWeightFloat = hppCost.Estimation.Kg if eggTotalPiecesFloat > 0 { avgWeight = eggWeightFloat / eggTotalPiecesFloat } - eggRemainingWeightFloatRemaining = avgWeight * eggPiecesFloatRemaining + // eggRemainingWeightFloatRemaining = avgWeight * eggPiecesFloatRemaining + eggRemainingWeightFloatRemaining = hppCost.Estimation.Kg - hppCost.Real.Kg } } if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {