adjust common hpp v2

This commit is contained in:
giovanni
2026-04-19 17:27:42 +07:00
parent 69d6fc165a
commit 04aad18a4c
7 changed files with 1020 additions and 259 deletions
@@ -89,11 +89,21 @@ type HppV2ManualDepreciationInputRow struct {
Note *string Note *string
} }
type HppV2FarmDepreciationSnapshotRow struct {
ID uint
ProjectFlockID uint
PeriodDate time.Time
DepreciationPercentEffective float64
DepreciationValue float64
PulletCostDayNTotal float64
}
type HppV2CostRepository interface { type HppV2CostRepository interface {
GetProjectFlockKandangContext(ctx context.Context, projectFlockKandangId uint) (*HppV2ProjectFlockKandangContext, error) GetProjectFlockKandangContext(ctx context.Context, projectFlockKandangId uint) (*HppV2ProjectFlockKandangContext, error)
GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error) GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error)
GetLatestTransferInputByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint, period time.Time) (*HppV2LatestTransferInputRow, error) GetLatestTransferInputByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint, period time.Time) (*HppV2LatestTransferInputRow, error)
GetManualDepreciationInputByProjectFlockID(ctx context.Context, projectFlockID uint) (*HppV2ManualDepreciationInputRow, 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) GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error)
GetDepreciationPercents(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, 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) ListUsageCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2UsageCostRow, error)
@@ -239,6 +249,29 @@ func (r *HppV2RepositoryImpl) GetManualDepreciationInputByProjectFlockID(
return &row, nil 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) { func (r *HppV2RepositoryImpl) GetEarliestChickInDateByProjectFlockID(ctx context.Context, projectFlockID uint) (*time.Time, error) {
type row struct { type row struct {
ChickInDate *time.Time 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.product_id, ast_pw.product_id, 0) AS source_product_id,
COALESCE(pi_prod.name, ast_prod.name, '') AS source_product_name, COALESCE(pi_prod.name, ast_prod.name, '') AS source_product_name,
COALESCE(SUM(sa.qty), 0) AS qty, 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(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
ELSE 0 ELSE 0
END AS unit_price, END), 0) AS unit_price,
COALESCE(SUM(sa.qty * CASE COALESCE(SUM(sa.qty * CASE
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
@@ -367,12 +400,7 @@ func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags(
sa.stockable_type, sa.stockable_type,
sa.stockable_id, sa.stockable_id,
COALESCE(pi.product_id, ast_pw.product_id, 0), COALESCE(pi.product_id, ast_pw.product_id, 0),
COALESCE(pi_prod.name, ast_prod.name, ''), 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
`). `).
Order("MIN(r.record_datetime) ASC, sa.stockable_type ASC, sa.stockable_id ASC"). Order("MIN(r.record_datetime) ASC, sa.stockable_type ASC, sa.stockable_id ASC").
Scan(&rows).Error Scan(&rows).Error
@@ -417,7 +445,7 @@ func (r *HppV2RepositoryImpl) ListAdjustmentCostRowsByProductFlags(
Joins("JOIN products AS p ON p.id = pw.product_id"). Joins("JOIN products AS p ON p.id = pw.product_id").
Joins("JOIN warehouses AS w ON w.id = pw.warehouse_id"). Joins("JOIN warehouses AS w ON w.id = pw.warehouse_id").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). 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("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). 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"). Order("ast.created_at ASC, ast.id ASC").
@@ -450,6 +478,15 @@ func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags(
stockableTransferToLaying := fifo.StockableKeyTransferToLayingIn.String() stockableTransferToLaying := fifo.StockableKeyTransferToLayingIn.String()
usableProjectChickin := fifo.UsableKeyProjectChickin.String() usableProjectChickin := fifo.UsableKeyProjectChickin.String()
usableStockTransferOut := fifo.UsableKeyStockTransferOut.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) rows := make([]HppV2ChickinCostRow, 0)
query := r.db.WithContext(ctx). query := r.db.WithContext(ctx).
@@ -479,30 +516,9 @@ func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags(
'' ''
) AS source_product_name, ) AS source_product_name,
COALESCE(SUM(sa.qty), 0) AS qty, COALESCE(SUM(sa.qty), 0) AS qty,
CASE `+unitPriceExpr+` AS unit_price,
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) COALESCE(SUM(sa.qty * (`+unitPriceExpr+`)), 0) AS total_cost
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,
).
Joins( Joins(
"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.status = ? AND sa.allocation_purpose = ?", "JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.status = ? AND sa.allocation_purpose = ?",
usableProjectChickin, usableProjectChickin,
@@ -563,7 +579,7 @@ func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags(
} }
err := query. err := query.
Group(` Group(fmt.Sprintf(`
pc.id, pc.id,
pc.project_flock_kandang_id, pc.project_flock_kandang_id,
pc.chick_in_date, pc.chick_in_date,
@@ -587,14 +603,8 @@ func (r *HppV2RepositoryImpl) ListChickinCostRowsByProductFlags(
sast_prod.name, sast_prod.name,
'' ''
), ),
CASE %s
WHEN sa.stockable_type = '` + stockablePurchase + `' THEN COALESCE(pi.price, 0) `, unitPriceExpr)).
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
`).
Order("pc.chick_in_date ASC, pc.id ASC, sa.stockable_type ASC, sa.stockable_id ASC"). Order("pc.chick_in_date ASC, pc.id ASC, sa.stockable_type ASC, sa.stockable_id ASC").
Scan(&rows).Error Scan(&rows).Error
if err != nil { if err != nil {
+37 -101
View File
@@ -2,7 +2,6 @@ package service
import ( import (
"context" "context"
"log"
"math" "math"
"time" "time"
@@ -40,108 +39,91 @@ func NewHppService(hppRepo commonRepo.HppCostRepository) HppService {
} }
func (s *hppService) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) { 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 { if date == nil {
now := time.Now() now := time.Now()
date = &now date = &now
} }
logHpp("CalculateHppCost", "normalized_date=%s", formatTimePtr(date))
location, err := time.LoadLocation("Asia/Jakarta") location, err := time.LoadLocation("Asia/Jakarta")
if err != nil { if err != nil {
logHpp("CalculateHppCost", "load_location_error=%v", err)
return nil, err return nil, err
} }
logHpp("CalculateHppCost", "location=%s", location.String())
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location) startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location)
endOfDay := startOfDay.Add(24 * time.Hour) 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) depresiasiTransfer, err := s.GetDepresiasiTransfer(projectFlockKandangId, &endOfDay)
if err != nil { if err != nil {
logHpp("CalculateHppCost", "get_depresiasi_transfer_error=%v", err)
return nil, err return nil, err
} }
logHpp("CalculateHppCost", "depresiasi_transfer=%f", depresiasiTransfer)
totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, &endOfDay, depresiasiTransfer) totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, &endOfDay, depresiasiTransfer)
if err != nil { if err != nil {
logHpp("CalculateHppCost", "get_total_production_cost_error=%v", err)
return nil, err return nil, err
} }
logHpp("CalculateHppCost", "total_production_cost=%f", totalProductionCost)
result, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay) result, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
if err != nil { if err != nil {
logHpp("CalculateHppCost", "get_hpp_estimation_dan_realisasi_error=%v", err)
return nil, err return nil, err
} }
logHpp("CalculateHppCost", "done estimation=%+v real=%+v", result.Estimation, result.Real)
return result, nil return result, nil
} }
func (s *hppService) GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error) { 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 { if date == nil {
now := time.Now() now := time.Now()
date = &now date = &now
} }
logHpp("GetTotalDepresiasiFlockGrowing", "normalized_date=%s", formatTimePtr(date))
if s.hppRepo == nil { if s.hppRepo == nil {
logHpp("GetTotalDepresiasiFlockGrowing", "repo_nil return=0")
return 0, nil return 0, nil
} }
kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID) kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
if err != nil { if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_project_flock_kandang_ids_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalDepresiasiFlockGrowing", "kandang_ids=%v", kandangIDs)
docCost, err := s.hppRepo.GetDocCost(context.Background(), kandangIDs) docCost, err := s.hppRepo.GetDocCost(context.Background(), kandangIDs)
if err != nil { if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_doc_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalDepresiasiFlockGrowing", "doc_cost=%f", docCost)
budgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), sourceProjectFlockID) budgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), sourceProjectFlockID)
if err != nil { if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_budget_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalDepresiasiFlockGrowing", "budget_cost=%f", budgetCost)
expedisionCost, err := s.hppRepo.GetExpedisionCost(context.Background(), kandangIDs) expedisionCost, err := s.hppRepo.GetExpedisionCost(context.Background(), kandangIDs)
if err != nil { if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_expedision_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalDepresiasiFlockGrowing", "expedision_cost=%f", expedisionCost)
feedCost, err := s.hppRepo.GetFeedUsageCost(context.Background(), kandangIDs, date) feedCost, err := s.hppRepo.GetFeedUsageCost(context.Background(), kandangIDs, date)
if err != nil { if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_feed_usage_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalDepresiasiFlockGrowing", "feed_cost=%f", feedCost)
ovkCost, err := s.hppRepo.GetOvkUsageCost(context.Background(), kandangIDs, date) ovkCost, err := s.hppRepo.GetOvkUsageCost(context.Background(), kandangIDs, date)
if err != nil { if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_ovk_usage_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalDepresiasiFlockGrowing", "ovk_cost=%f", ovkCost)
total := docCost + budgetCost + expedisionCost + feedCost + ovkCost total := docCost + budgetCost + expedisionCost + feedCost + ovkCost
logHpp("GetTotalDepresiasiFlockGrowing", "done total=%f", total)
return total, nil return total, nil
} }
func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error) { 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 { // if date == nil {
// now := time.Now() // now := time.Now()
// date = &now // date = &now
@@ -149,248 +131,210 @@ func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate
costPullet, err := s.hppRepo.GetPulletCost(context.Background(), projectFlockKandangId) costPullet, err := s.hppRepo.GetPulletCost(context.Background(), projectFlockKandangId)
if err != nil { if err != nil {
logHpp("GetTotalProductionCost", "get_pullet_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalProductionCost", "cost_pullet=%f", costPullet)
costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate) costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil { if err != nil {
logHpp("GetTotalProductionCost", "get_feed_usage_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalProductionCost", "cost_feed=%f", costFeed)
costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate) costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil { if err != nil {
logHpp("GetTotalProductionCost", "get_ovk_usage_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalProductionCost", "cost_ovk=%f", costOvk)
costExpedision, err := s.hppRepo.GetExpedisionCost(context.Background(), []uint{projectFlockKandangId}) costExpedision, err := s.hppRepo.GetExpedisionCost(context.Background(), []uint{projectFlockKandangId})
if err != nil { if err != nil {
logHpp("GetTotalProductionCost", "get_expedision_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalProductionCost", "cost_expedision=%f", costExpedision)
costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, endDate) costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, endDate)
if err != nil { if err != nil {
logHpp("GetTotalProductionCost", "get_budget_kandang_laying_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalProductionCost", "cost_budget=%f", costBudget)
// fmt.Println(costBudget, costExpedision, costOvk, costFeed, costPullet, depresiasiTransfer) // fmt.Println(costBudget, costExpedision, costOvk, costFeed, costPullet, depresiasiTransfer)
// depresiasiTransfer = 0 // depresiasiTransfer = 0
total := depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget total := depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget
logHpp("GetTotalProductionCost", "done total=%f", total)
return total, nil return total, nil
} }
func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error) { 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 { // if date == nil {
// now := time.Now() // now := time.Now()
// date = &now // date = &now
// } // }
if s.hppRepo == nil { if s.hppRepo == nil {
logHpp("GetBudgetKandangLaying", "repo_nil return=0")
return 0, nil return 0, nil
} }
projectFlockId, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId) projectFlockId, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId)
if err != nil { if err != nil {
logHpp("GetBudgetKandangLaying", "get_project_flock_id_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetBudgetKandangLaying", "project_flock_id=%d", projectFlockId)
projectFlockKandangIds, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockId) projectFlockKandangIds, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockId)
if err != nil { if err != nil {
logHpp("GetBudgetKandangLaying", "get_project_flock_kandang_ids_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetBudgetKandangLaying", "project_flock_kandang_ids=%v", projectFlockKandangIds)
eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, endDate) eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, endDate)
if err != nil { if err != nil {
logHpp("GetBudgetKandangLaying", "get_egg_produksi_pieces_flock_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_flock=%f", eggProduksiPiecesFlock)
eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil { if err != nil {
logHpp("GetBudgetKandangLaying", "get_egg_produksi_pieces_kandang_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_kandang=%f", eggProduksiPiecesKandang)
totalBudgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), projectFlockId) totalBudgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), projectFlockId)
if err != nil { if err != nil {
logHpp("GetBudgetKandangLaying", "get_budget_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetBudgetKandangLaying", "total_budget_cost=%f", totalBudgetCost)
if eggProduksiPiecesFlock == 0 { if eggProduksiPiecesFlock == 0 {
logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_flock_zero return=0")
return 0, nil return 0, nil
} }
result := (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock result := (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock
logHpp("GetBudgetKandangLaying", "done result=%f", result)
return result, nil return result, nil
} }
func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, endDate *time.Time) (float64, error) { 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 { if endDate == nil {
now := time.Now() now := time.Now()
endDate = &now endDate = &now
} }
logHpp("GetDepresiasiTransfer", "normalized_end_date=%s", formatTimePtr(endDate))
if s.hppRepo == nil { if s.hppRepo == nil {
logHpp("GetDepresiasiTransfer", "repo_nil return=0")
return 0, nil return 0, nil
} }
sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId) sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId)
if err != nil { if err != nil {
logHpp("GetDepresiasiTransfer", "get_transfer_source_summary_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetDepresiasiTransfer", "source_project_flock_id=%d transfer_total_qty=%f", sourceProjectFlockID, transferTotalQty)
if sourceProjectFlockID == 0 || transferTotalQty <= 0 { if sourceProjectFlockID == 0 || transferTotalQty <= 0 {
logHpp("GetDepresiasiTransfer", "use_manual_fallback=true")
result, fallbackErr := s.getManualDepresiasiTransferFallback(projectFlockKandangId) result, fallbackErr := s.getManualDepresiasiTransferFallback(projectFlockKandangId)
if fallbackErr != nil { if fallbackErr != nil {
logHpp("GetDepresiasiTransfer", "manual_fallback_error=%v", fallbackErr)
return 0, fallbackErr return 0, fallbackErr
} }
logHpp("GetDepresiasiTransfer", "done_fallback result=%f", result)
return result, nil return result, nil
} }
kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID) kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
if err != nil { if err != nil {
logHpp("GetDepresiasiTransfer", "get_project_flock_kandang_ids_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetDepresiasiTransfer", "kandang_ids_growing=%v", kandangIDsGrowing)
totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing) totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing)
if err != nil { if err != nil {
logHpp("GetDepresiasiTransfer", "get_total_population_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetDepresiasiTransfer", "total_population_flock_growing=%f", totalPopulationFlockGrowing)
if totalPopulationFlockGrowing == 0 { if totalPopulationFlockGrowing == 0 {
logHpp("GetDepresiasiTransfer", "total_population_flock_growing_zero return=0")
return 0, nil return 0, nil
} }
totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, endDate) totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, endDate)
if err != nil { if err != nil {
logHpp("GetDepresiasiTransfer", "get_total_depresiasi_flock_growing_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetDepresiasiTransfer", "total_depresiasi_flock_growing=%f", totalDepresiasiFlockGrowing)
result := (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing result := (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing
logHpp("GetDepresiasiTransfer", "done result=%f", result)
return result, nil return result, nil
} }
func (s *hppService) getManualDepresiasiTransferFallback(projectFlockKandangId uint) (float64, error) { 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) projectFlockID, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId)
if err != nil { if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_project_flock_id_error=%v", err)
return 0, err return 0, err
} }
logHpp("getManualDepresiasiTransferFallback", "project_flock_id=%d", projectFlockID)
if projectFlockID == 0 { if projectFlockID == 0 {
logHpp("getManualDepresiasiTransferFallback", "project_flock_id_zero return=0")
return 0, nil return 0, nil
} }
manualCost, err := s.hppRepo.GetManualDepreciationCostByProjectFlockID(context.Background(), projectFlockID) manualCost, err := s.hppRepo.GetManualDepreciationCostByProjectFlockID(context.Background(), projectFlockID)
if err != nil { if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_manual_depreciation_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("getManualDepresiasiTransferFallback", "manual_cost=%f", manualCost)
if manualCost <= 0 { if manualCost <= 0 {
logHpp("getManualDepresiasiTransferFallback", "manual_cost_non_positive return=0")
return 0, nil return 0, nil
} }
kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockID) kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockID)
if err != nil { if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_project_flock_kandang_ids_error=%v", err)
return 0, err return 0, err
} }
logHpp("getManualDepresiasiTransferFallback", "kandang_ids=%v", kandangIDs)
if len(kandangIDs) == 0 { if len(kandangIDs) == 0 {
logHpp("getManualDepresiasiTransferFallback", "kandang_ids_empty return=0")
return 0, nil return 0, nil
} }
totalUsageQty, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDs) totalUsageQty, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDs)
if err != nil { if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_total_usage_qty_error=%v", err)
return 0, err return 0, err
} }
logHpp("getManualDepresiasiTransferFallback", "total_usage_qty=%f", totalUsageQty)
if totalUsageQty <= 0 { if totalUsageQty <= 0 {
logHpp("getManualDepresiasiTransferFallback", "total_usage_qty_non_positive return=0")
return 0, nil return 0, nil
} }
kandangUsageQty, err := s.hppRepo.GetTotalPopulation(context.Background(), []uint{projectFlockKandangId}) kandangUsageQty, err := s.hppRepo.GetTotalPopulation(context.Background(), []uint{projectFlockKandangId})
if err != nil { if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_kandang_usage_qty_error=%v", err)
return 0, err return 0, err
} }
logHpp("getManualDepresiasiTransferFallback", "kandang_usage_qty=%f", kandangUsageQty)
if kandangUsageQty <= 0 { if kandangUsageQty <= 0 {
logHpp("getManualDepresiasiTransferFallback", "kandang_usage_qty_non_positive return=0")
return 0, nil return 0, nil
} }
result := manualCost * (kandangUsageQty / totalUsageQty) result := manualCost * (kandangUsageQty / totalUsageQty)
logHpp("getManualDepresiasiTransferFallback", "done result=%f", result)
return result, nil return result, nil
} }
func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) { 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 { if s.hppRepo == nil {
logHpp("GetHppEstimationDanRealisasi", "repo_nil return_empty_response")
return &HppCostResponse{}, nil return &HppCostResponse{}, nil
} }
estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil { if err != nil {
logHpp("GetHppEstimationDanRealisasi", "get_egg_produksi_error=%v", err)
return nil, 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) realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate)
if err != nil { if err != nil {
logHpp("GetHppEstimationDanRealisasi", "get_egg_terjual_error=%v", err)
return nil, err return nil, err
} }
logHpp("GetHppEstimationDanRealisasi", "real_pieces=%f real_weight_kg=%f", realPieces, realWeightKg)
estimation := HppCostDetail{ estimation := HppCostDetail{
Total: totalProductionCost, Total: totalProductionCost,
@@ -403,7 +347,6 @@ func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, p
if estimPieces > 0 { if estimPieces > 0 {
estimation.HargaButir = roundToTwoDecimals(totalProductionCost / estimPieces) estimation.HargaButir = roundToTwoDecimals(totalProductionCost / estimPieces)
} }
logHpp("GetHppEstimationDanRealisasi", "estimation=%+v", estimation)
real := HppCostDetail{ real := HppCostDetail{
Total: totalProductionCost, Total: totalProductionCost,
@@ -416,19 +359,16 @@ func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, p
if realPieces > 0 { if realPieces > 0 {
real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces) real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces)
} }
logHpp("GetHppEstimationDanRealisasi", "real=%+v", real)
result := &HppCostResponse{ result := &HppCostResponse{
Estimation: estimation, Estimation: estimation,
Real: real, Real: real,
} }
logHpp("GetHppEstimationDanRealisasi", "done response=%+v", *result)
return result, nil return result, nil
} }
func roundToTwoDecimals(value float64) float64 { func roundToTwoDecimals(value float64) float64 {
result := math.Round(value*100) / 100 result := math.Round(value*100) / 100
logHpp("roundToTwoDecimals", "input=%f output=%f", value, result)
return result return result
} }
@@ -438,7 +378,3 @@ func formatTimePtr(value *time.Time) string {
} }
return value.Format(time.RFC3339) return value.Format(time.RFC3339)
} }
func logHpp(method, format string, args ...any) {
log.Printf("[HPP][%s] "+format, append([]any{method}, args...)...)
}
@@ -48,6 +48,7 @@ type HppV2Breakdown struct {
ProjectFlockKandangID uint `json:"project_flock_kandang_id"` ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
ProjectFlockID uint `json:"project_flock_id"` ProjectFlockID uint `json:"project_flock_id"`
ProjectFlockCategory string `json:"project_flock_category,omitempty"` ProjectFlockCategory string `json:"project_flock_category,omitempty"`
HouseType string `json:"house_type,omitempty"`
KandangID uint `json:"kandang_id,omitempty"` KandangID uint `json:"kandang_id,omitempty"`
KandangName string `json:"kandang_name,omitempty"` KandangName string `json:"kandang_name,omitempty"`
LocationID uint `json:"location_id,omitempty"` LocationID uint `json:"location_id,omitempty"`
+211 -14
View File
@@ -28,13 +28,14 @@ const (
hppV2PartManualCutover = "manual_cutover" hppV2PartManualCutover = "manual_cutover"
hppV2PartDepreciationNormal = "normal_transfer" hppV2PartDepreciationNormal = "normal_transfer"
hppV2PartDepreciationCutover = "manual_cutover" hppV2PartDepreciationCutover = "manual_cutover"
hppV2PartDepreciationFarmSnapshot = "farm_snapshot"
hppV2ProrationPopulation = "growing_population_share" hppV2ProrationPopulation = "growing_population_share"
hppV2ProrationEggWeight = "laying_egg_weight_share" hppV2ProrationEggWeight = "laying_egg_weight_share"
hppV2ProrationEggPiece = "laying_egg_piece_share" hppV2ProrationEggPiece = "laying_egg_piece_share"
hppV2ScopePulletCost = "pullet_cost" hppV2ScopePulletCost = "pullet_cost"
hppV2ScopeProductionCost = "production_cost" hppV2ScopeProductionCost = "production_cost"
hppV2CutoverFlagPakan = "PAKAN-CUTOVER" hppV2CutoverFlagPakan = "PAKAN-CUTOVER"
hppV2CutoverFlagOvk = "OVK-CUTOVER" hppV2CutoverFlagOvk = "OVK"
) )
type HppV2Service interface { type HppV2Service interface {
@@ -115,57 +116,101 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t
totalPulletCost := 0.0 totalPulletCost := 0.0
totalProductionCost := 0.0 totalProductionCost := 0.0
components := make([]HppV2Component, 0, 8) 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) { 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 return
} }
pulletAdded := componentScopeTotal(component, hppV2ScopePulletCost)
productionAdded := componentScopeTotal(component, hppV2ScopeProductionCost)
components = append(components, *component) components = append(components, *component)
totalPulletCost += componentScopeTotal(component, hppV2ScopePulletCost) totalPulletCost += pulletAdded
totalProductionCost += componentScopeTotal(component, hppV2ScopeProductionCost) 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) ovkComponent, err := s.GetOvkBreakdown(projectFlockKandangId, &endOfDay)
if err != nil { if err != nil {
return nil, err return nil, err
} }
appendComponent(ovkComponent) appendComponent(hppV2ComponentOvk, ovkComponent)
docComponent, err := s.GetDocChickinBreakdown(projectFlockKandangId, &endOfDay) docComponent, err := s.GetDocChickinBreakdown(projectFlockKandangId, &endOfDay)
if err != nil { if err != nil {
return nil, err return nil, err
} }
appendComponent(docComponent) appendComponent(hppV2ComponentDocChickin, docComponent)
directPulletComponent, err := s.GetDirectPulletPurchaseBreakdown(projectFlockKandangId, &endOfDay) directPulletComponent, err := s.GetDirectPulletPurchaseBreakdown(projectFlockKandangId, &endOfDay)
if err != nil { if err != nil {
return nil, err return nil, err
} }
appendComponent(directPulletComponent) appendComponent(hppV2ComponentDirectPulletPurchase, directPulletComponent)
bopRegularComponent, err := s.GetBopRegularBreakdown(projectFlockKandangId, &endOfDay) bopRegularComponent, err := s.GetBopRegularBreakdown(projectFlockKandangId, &endOfDay)
if err != nil { if err != nil {
return nil, err return nil, err
} }
appendComponent(bopRegularComponent) appendComponent(hppV2ComponentBopRegular, bopRegularComponent)
bopEkspedisiComponent, err := s.GetBopEkspedisiBreakdown(projectFlockKandangId, &endOfDay) bopEkspedisiComponent, err := s.GetBopEkspedisiBreakdown(projectFlockKandangId, &endOfDay)
if err != nil { if err != nil {
return nil, err return nil, err
} }
appendComponent(bopEkspedisiComponent) appendComponent(hppV2ComponentBopEksp, bopEkspedisiComponent)
manualPulletComponent, err := s.getManualPulletCostComponent(projectFlockKandangId, contextRow, startOfDay) manualPulletComponent, err := s.getManualPulletCostComponent(projectFlockKandangId, contextRow, startOfDay)
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err 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) hppCost, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
if err != nil { if err != nil {
@@ -179,6 +224,7 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t
ProjectFlockKandangID: projectFlockKandangId, ProjectFlockKandangID: projectFlockKandangId,
ProjectFlockID: contextRow.ProjectFlockID, ProjectFlockID: contextRow.ProjectFlockID,
ProjectFlockCategory: contextRow.ProjectFlockCategory, ProjectFlockCategory: contextRow.ProjectFlockCategory,
HouseType: contextRow.HouseType,
KandangID: contextRow.KandangID, KandangID: contextRow.KandangID,
KandangName: contextRow.KandangName, KandangName: contextRow.KandangName,
LocationID: contextRow.LocationID, LocationID: contextRow.LocationID,
@@ -1022,9 +1068,28 @@ func (s *hppV2Service) getDepreciationComponent(
projectFlockKandangId uint, projectFlockKandangId uint,
contextRow *commonRepo.HppV2ProjectFlockKandangContext, contextRow *commonRepo.HppV2ProjectFlockKandangContext,
periodDate time.Time, periodDate time.Time,
endDate time.Time,
totalPulletCost float64, totalPulletCost float64,
) (*HppV2Component, error) { ) (*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 return nil, nil
} }
@@ -1058,6 +1123,101 @@ func (s *hppV2Service) getDepreciationComponent(
}, nil }, 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( func (s *hppV2Service) buildNormalTransferDepreciationPart(
contextRow *commonRepo.HppV2ProjectFlockKandangContext, contextRow *commonRepo.HppV2ProjectFlockKandangContext,
transferInput *commonRepo.HppV2LatestTransferInputRow, 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) { 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 { if s.hppRepo == nil {
utils.Log.Warnf(
"GetHppEstimationDanRealisasi skipped: hpp repository is nil (project_flock_kandang_id=%d)",
projectFlockKandangId,
)
return &HppCostResponse{}, nil return &HppCostResponse{}, nil
} }
estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil { 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 return nil, err
} }
realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate) realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate)
if err != nil { 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 return nil, err
} }
@@ -1249,6 +1432,20 @@ func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64,
real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces) 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{ return &HppCostResponse{
Estimation: estimation, Estimation: estimation,
Real: real, Real: real,
@@ -17,6 +17,7 @@ type hppV2RepoStub struct {
pfkIDsByProject map[uint][]uint pfkIDsByProject map[uint][]uint
latestTransferByPFK map[uint]*commonRepo.HppV2LatestTransferInputRow latestTransferByPFK map[uint]*commonRepo.HppV2LatestTransferInputRow
manualInputByProject map[uint]*commonRepo.HppV2ManualDepreciationInputRow manualInputByProject map[uint]*commonRepo.HppV2ManualDepreciationInputRow
snapshotByProjectKey map[string]*commonRepo.HppV2FarmDepreciationSnapshotRow
chickInDateByProject map[uint]*time.Time chickInDateByProject map[uint]*time.Time
depreciationByHouse map[string]map[int]float64 depreciationByHouse map[string]map[int]float64
usageRowsByKey map[string][]commonRepo.HppV2UsageCostRow usageRowsByKey map[string][]commonRepo.HppV2UsageCostRow
@@ -59,6 +60,13 @@ func (s *hppV2RepoStub) GetManualDepreciationInputByProjectFlockID(_ context.Con
return s.manualInputByProject[projectFlockID], nil 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) { func (s *hppV2RepoStub) GetEarliestChickInDateByProjectFlockID(_ context.Context, projectFlockID uint) (*time.Time, error) {
return s.chickInDateByProject[projectFlockID], nil return s.chickInDateByProject[projectFlockID], nil
} }
@@ -319,10 +327,10 @@ func TestHppV2CalculateHppBreakdown_IncludesOvkComponent(t *testing.T) {
}, },
}, },
adjustRowsByKey: map[string][]commonRepo.HppV2AdjustmentCostRow{ 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}, {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}, {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 { func stubKey(ids []uint, flags []string) string {
idParts := make([]string, 0, len(ids)) idParts := make([]string, 0, len(ids))
for _, id := range ids { for _, id := range ids {
@@ -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)
}
}
@@ -416,6 +416,13 @@ func (s *repportService) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, re
if err := s.ExpenseDepreciationRepo.UpsertManualInput(ctx.Context(), &row); err != nil { if err := s.ExpenseDepreciationRepo.UpsertManualInput(ctx.Context(), &row); err != nil {
return nil, err return nil, err
} }
if err := s.ExpenseDepreciationRepo.DeleteSnapshotsFromDate(
ctx.Context(),
cutoverDate,
[]uint{row.ProjectFlockId},
); err != nil {
return nil, err
}
response := &dto.ExpenseDepreciationManualInputRowDTO{ response := &dto.ExpenseDepreciationManualInputRowDTO{
ID: int64(row.Id), ID: int64(row.Id),
@@ -456,6 +463,11 @@ type depreciationKandangComponent struct {
TransferQty float64 `json:"transfer_qty"` TransferQty float64 `json:"transfer_qty"`
PulletCostDayN float64 `json:"pullet_cost_day_n"` PulletCostDayN float64 `json:"pullet_cost_day_n"`
DepreciationValue float64 `json:"depreciation_value"` 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 { type depreciationFarmComponents struct {
@@ -469,124 +481,98 @@ func (s *repportService) computeExpenseDepreciationSnapshots(
farmIDs []uint, farmIDs []uint,
farmNameByID map[uint]string, farmNameByID map[uint]string,
) ([]entity.FarmDepreciationSnapshot, error) { ) ([]entity.FarmDepreciationSnapshot, error) {
_ = farmNameByID
if len(farmIDs) == 0 { if len(farmIDs) == 0 {
return []entity.FarmDepreciationSnapshot{}, nil return []entity.FarmDepreciationSnapshot{}, nil
} }
if s.HppCostRepo == nil {
inputRows, err := s.ExpenseDepreciationRepo.GetLatestTransferInputsByFarms(ctx, periodDate, farmIDs) return nil, errors.New("hpp cost repository is not configured")
if err != nil {
return nil, err
} }
if s.HppV2Svc == nil {
groupedByFarm := make(map[uint][]repportRepo.FarmDepreciationLatestTransferRow, len(farmIDs)) return nil, errors.New("hpp v2 service is not configured")
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{}{}
}
} }
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)) result := make([]entity.FarmDepreciationSnapshot, 0, len(farmIDs))
for _, farmID := range farmIDs { for _, farmID := range farmIDs {
farmRows := groupedByFarm[farmID] kandangIDs, err := s.HppCostRepo.GetProjectFlockKandangIDs(ctx, farmID)
if err != nil {
return nil, err
}
components := depreciationFarmComponents{ components := depreciationFarmComponents{
KandangCount: len(farmRows), Kandang: make([]depreciationKandangComponent, 0, len(kandangIDs)),
Kandang: make([]depreciationKandangComponent, 0, len(farmRows)),
} }
totalDepreciationValue := 0.0 totalDepreciationValue := 0.0
totalPulletCostDayN := 0.0 totalPulletCostDayN := 0.0
for _, row := range farmRows { for _, kandangID := range kandangIDs {
dayN := approvalService.DepreciationScheduleDay(row.TransferDate, periodDate, valueOrEmptyString(row.HouseType)) breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &periodDate)
houseType := approvalService.NormalizeDepreciationHouseType(valueOrEmptyString(row.HouseType)) if err != nil {
return nil, err
}
if breakdown == nil {
continue
}
transferDateKey := row.TransferDate.Format("2006-01-02") depreciationComponent := hppV2FindDepreciationComponent(breakdown)
cacheKey := fmt.Sprintf("%d|%s", row.SourceProjectFlockID, transferDateKey) if depreciationComponent == nil {
cached, exists := sourceCostCache[cacheKey] continue
if !exists { }
endOfDay := row.TransferDate.Add(24 * time.Hour)
sourceDepCost, calcErr := s.HppSvc.GetTotalDepresiasiFlockGrowing(row.SourceProjectFlockID, &endOfDay) for _, part := range depreciationComponent.Parts {
if calcErr != nil { if part.Total <= 0 {
return nil, calcErr continue
} }
cached = sourceCostCacheItem{totalDepCost: sourceDepCost}
sourceCostCache[cacheKey] = cached
}
sourcePopulation, popExists := sourcePopulationCache[row.SourceProjectFlockID] houseType := approvalService.NormalizeDepreciationHouseType(breakdown.HouseType)
if !popExists { component := depreciationKandangComponent{
if s.HppCostRepo == nil { ProjectFlockKandangID: breakdown.ProjectFlockKandangID,
sourcePopulation = 0 KandangID: breakdown.KandangID,
} else { KandangName: breakdown.KandangName,
kandangIDs, idsErr := s.HppCostRepo.GetProjectFlockKandangIDs(ctx, row.SourceProjectFlockID) SourceProjectFlockID: hppV2DetailUint(part.Details, "source_project_flock_id"),
if idsErr != nil { HouseType: houseType,
return nil, idsErr DayN: hppV2DetailInt(part.Details, "schedule_day"),
} DepreciationPercent: hppV2DetailFloat(part.Details, "depreciation_percent"),
population, popErr := s.HppCostRepo.GetTotalPopulation(ctx, kandangIDs) PulletCostDayN: hppV2DetailFloat(part.Details, "pullet_cost_day_n"),
if popErr != nil { DepreciationValue: part.Total,
return nil, popErr DepreciationSource: part.Code,
} OriginDate: hppV2DetailString(part.Details, "origin_date"),
sourcePopulation = population
} }
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) effectivePercent := approvalService.CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN)
componentsJSON, marshalErr := json.Marshal(components) componentsJSON, marshalErr := json.Marshal(components)
@@ -607,6 +593,106 @@ func (s *repportService) computeExpenseDepreciationSnapshots(
return result, nil 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 { func parseSnapshotComponents(raw []byte) any {
if len(raw) == 0 { if len(raw) == 0 {
return map[string]any{} return map[string]any{}
@@ -2280,13 +2366,15 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
} }
if hppCost != nil { if hppCost != nil {
eggPiecesFloatRemaining = hppCost.Estimation.Butir - hppCost.Real.Butir eggPiecesFloatRemaining = hppCost.Estimation.Butir - hppCost.Real.Butir
eggHpp = hppCost.Estimation.HargaKg // eggHpp = hppCost.Estimation.HargaKg
eggHpp = hppCost.Real.HargaKg
eggTotalPiecesFloat = hppCost.Estimation.Butir eggTotalPiecesFloat = hppCost.Estimation.Butir
eggWeightFloat = hppCost.Estimation.Kg eggWeightFloat = hppCost.Estimation.Kg
if eggTotalPiecesFloat > 0 { if eggTotalPiecesFloat > 0 {
avgWeight = eggWeightFloat / eggTotalPiecesFloat avgWeight = eggWeightFloat / eggTotalPiecesFloat
} }
eggRemainingWeightFloatRemaining = avgWeight * eggPiecesFloatRemaining // eggRemainingWeightFloatRemaining = avgWeight * eggPiecesFloatRemaining
eggRemainingWeightFloatRemaining = hppCost.Estimation.Kg - hppCost.Real.Kg
} }
} }
if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) { if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {