mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
feat: manual pullet cost
This commit is contained in:
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -72,9 +73,29 @@ type HppV2ChickinCostRow struct {
|
||||
TotalCost float64
|
||||
}
|
||||
|
||||
type HppV2LatestTransferInputRow struct {
|
||||
ProjectFlockKandangID uint
|
||||
SourceProjectFlockID uint
|
||||
TransferDate time.Time
|
||||
TransferQty float64
|
||||
TransferID uint
|
||||
}
|
||||
|
||||
type HppV2ManualDepreciationInputRow struct {
|
||||
ID uint
|
||||
ProjectFlockID uint
|
||||
TotalCost float64
|
||||
CutoverDate time.Time
|
||||
Note *string
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
ListAdjustmentCostRowsByProductFlags(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, date *time.Time) ([]HppV2AdjustmentCostRow, error)
|
||||
ListExpenseRealizationRowsByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time, ekspedisi bool) ([]HppV2ExpenseCostRow, error)
|
||||
@@ -136,6 +157,149 @@ func (r *HppV2RepositoryImpl) GetProjectFlockKandangIDs(ctx context.Context, pro
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (r *HppV2RepositoryImpl) GetLatestTransferInputByProjectFlockKandangID(
|
||||
ctx context.Context,
|
||||
projectFlockKandangId uint,
|
||||
period time.Time,
|
||||
) (*HppV2LatestTransferInputRow, error) {
|
||||
var row HppV2LatestTransferInputRow
|
||||
query := `
|
||||
WITH latest_transfer_approval AS (
|
||||
SELECT a.approvable_id, a.action
|
||||
FROM approvals a
|
||||
JOIN (
|
||||
SELECT approvable_id, MAX(action_at) AS latest_action_at
|
||||
FROM approvals
|
||||
WHERE approvable_type = @approval_type
|
||||
GROUP BY approvable_id
|
||||
) la
|
||||
ON la.approvable_id = a.approvable_id
|
||||
AND la.latest_action_at = a.action_at
|
||||
WHERE a.approvable_type = @approval_type
|
||||
),
|
||||
approved_transfers AS (
|
||||
SELECT
|
||||
lt.id,
|
||||
lt.from_project_flock_id,
|
||||
COALESCE(DATE(lt.effective_move_date), DATE(lt.economic_cutoff_date), DATE(lt.transfer_date)) AS effective_date
|
||||
FROM laying_transfers lt
|
||||
JOIN latest_transfer_approval lta ON lta.approvable_id = lt.id
|
||||
WHERE lt.deleted_at IS NULL
|
||||
AND lt.executed_at IS NOT NULL
|
||||
AND lta.action = 'APPROVED'
|
||||
)
|
||||
SELECT
|
||||
ltt.target_project_flock_kandang_id AS project_flock_kandang_id,
|
||||
at.from_project_flock_id AS source_project_flock_id,
|
||||
at.effective_date AS transfer_date,
|
||||
ltt.total_qty AS transfer_qty,
|
||||
at.id AS transfer_id
|
||||
FROM laying_transfer_targets ltt
|
||||
JOIN approved_transfers at ON at.id = ltt.laying_transfer_id
|
||||
WHERE ltt.deleted_at IS NULL
|
||||
AND ltt.target_project_flock_kandang_id = @project_flock_kandang_id
|
||||
AND at.effective_date <= DATE(@period_date)
|
||||
ORDER BY at.effective_date DESC, at.id DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
err := r.db.WithContext(ctx).Raw(query, map[string]any{
|
||||
"approval_type": utils.ApprovalWorkflowTransferToLaying.String(),
|
||||
"project_flock_kandang_id": projectFlockKandangId,
|
||||
"period_date": period,
|
||||
}).Scan(&row).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if row.TransferID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
func (r *HppV2RepositoryImpl) GetManualDepreciationInputByProjectFlockID(
|
||||
ctx context.Context,
|
||||
projectFlockID uint,
|
||||
) (*HppV2ManualDepreciationInputRow, error) {
|
||||
var row HppV2ManualDepreciationInputRow
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("farm_depreciation_manual_inputs").
|
||||
Select("id, project_flock_id, total_cost, cutover_date, note").
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
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
|
||||
}
|
||||
|
||||
var selected row
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("project_chickins AS pc").
|
||||
Select("MIN(pc.chick_in_date) AS chick_in_date").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pc.project_flock_kandang_id").
|
||||
Where("pc.deleted_at IS NULL").
|
||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||
Scan(&selected).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if selected.ChickInDate == nil || selected.ChickInDate.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return selected.ChickInDate, nil
|
||||
}
|
||||
|
||||
func (r *HppV2RepositoryImpl) GetDepreciationPercents(
|
||||
ctx context.Context,
|
||||
houseTypes []string,
|
||||
maxDay int,
|
||||
) (map[string]map[int]float64, error) {
|
||||
result := make(map[string]map[int]float64)
|
||||
if len(houseTypes) == 0 || maxDay <= 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type row struct {
|
||||
HouseType string
|
||||
Day int
|
||||
DepreciationPercent float64
|
||||
}
|
||||
|
||||
rows := make([]row, 0)
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("house_depreciation_standards").
|
||||
Select("house_type::text AS house_type, day, depreciation_percent").
|
||||
Where("house_type::text IN ?", houseTypes).
|
||||
Where("day <= ?", maxDay).
|
||||
Order("house_type ASC, day ASC").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range rows {
|
||||
if _, exists := result[item.HouseType]; !exists {
|
||||
result[item.HouseType] = make(map[int]float64)
|
||||
}
|
||||
result[item.HouseType][item.Day] = item.DepreciationPercent
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *HppV2RepositoryImpl) ListUsageCostRowsByProductFlags(
|
||||
ctx context.Context,
|
||||
projectFlockKandangIDs []uint,
|
||||
|
||||
@@ -49,7 +49,23 @@ func CalculateDepreciationAtDayN(
|
||||
houseType string,
|
||||
percentByHouseType map[string]map[int]float64,
|
||||
) (float64, float64, float64) {
|
||||
if initialPulletCost <= 0 || dayN <= 0 {
|
||||
return CalculateDepreciationFromDayRange(initialPulletCost, 1, dayN, houseType, percentByHouseType)
|
||||
}
|
||||
|
||||
func CalculateDepreciationFromDayRange(
|
||||
initialPulletCost float64,
|
||||
startDay int,
|
||||
endDay int,
|
||||
houseType string,
|
||||
percentByHouseType map[string]map[int]float64,
|
||||
) (float64, float64, float64) {
|
||||
if initialPulletCost <= 0 || endDay <= 0 {
|
||||
return 0, 0, 0
|
||||
}
|
||||
if startDay <= 0 {
|
||||
startDay = 1
|
||||
}
|
||||
if endDay < startDay {
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
@@ -63,10 +79,10 @@ func CalculateDepreciationAtDayN(
|
||||
pulletCostDayN := 0.0
|
||||
depreciationValue := 0.0
|
||||
depreciationPercent := 0.0
|
||||
for day := 1; day <= dayN; day++ {
|
||||
for day := startDay; day <= endDay; day++ {
|
||||
pct := housePercent[day]
|
||||
dep := current * (pct / 100)
|
||||
if day == dayN {
|
||||
if day == endDay {
|
||||
pulletCostDayN = current
|
||||
depreciationValue = dep
|
||||
depreciationPercent = pct
|
||||
|
||||
@@ -43,6 +43,27 @@ func TestCalculateDepreciationAtDayN_UsesRemainingBasisRecursively(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateDepreciationFromDayRange_StartsFromProvidedScheduleDay(t *testing.T) {
|
||||
percentByHouseType := map[string]map[int]float64{
|
||||
"close_house": {
|
||||
1: 10,
|
||||
2: 20,
|
||||
3: 5,
|
||||
},
|
||||
}
|
||||
|
||||
pulletCostDayN, depreciationValue, depreciationPercent := CalculateDepreciationFromDayRange(1000, 2, 3, "close_house", percentByHouseType)
|
||||
if pulletCostDayN != 800 {
|
||||
t.Fatalf("expected remaining basis entering day 3 to be 800, got %v", pulletCostDayN)
|
||||
}
|
||||
if depreciationValue != 40 {
|
||||
t.Fatalf("expected day 3 depreciation to be 40, got %v", depreciationValue)
|
||||
}
|
||||
if depreciationPercent != 5 {
|
||||
t.Fatalf("expected day 3 depreciation percent to be 5, got %v", depreciationPercent)
|
||||
}
|
||||
}
|
||||
|
||||
func mustDepreciationDate(t *testing.T, raw string) time.Time {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -29,8 +29,10 @@ type HppV2Reference struct {
|
||||
type HppV2ComponentPart struct {
|
||||
Code string `json:"code"`
|
||||
Title string `json:"title"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
Total float64 `json:"total"`
|
||||
Proration *HppV2Proration `json:"proration,omitempty"`
|
||||
Details map[string]any `json:"details,omitempty"`
|
||||
References []HppV2Reference `json:"references,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ const (
|
||||
hppV2ComponentDirectPulletPurchase = "DIRECT_PULLET_PURCHASE"
|
||||
hppV2ComponentBopRegular = "BOP_REGULAR"
|
||||
hppV2ComponentBopEksp = "BOP_EKSPEDISI"
|
||||
hppV2ComponentManualPulletCost = "MANUAL_PULLET_COST"
|
||||
hppV2ComponentDepreciation = "DEPRECIATION"
|
||||
hppV2PartGrowingNormal = "growing_normal"
|
||||
hppV2PartGrowingCutover = "growing_cutover"
|
||||
hppV2PartLayingNormal = "laying_normal"
|
||||
@@ -23,6 +25,9 @@ const (
|
||||
hppV2PartGrowingFarm = "growing_farm"
|
||||
hppV2PartLayingDirect = "laying_direct"
|
||||
hppV2PartLayingFarm = "laying_farm"
|
||||
hppV2PartManualCutover = "manual_cutover"
|
||||
hppV2PartDepreciationNormal = "normal_transfer"
|
||||
hppV2PartDepreciationCutover = "manual_cutover"
|
||||
hppV2ProrationPopulation = "growing_population_share"
|
||||
hppV2ProrationEggWeight = "laying_egg_weight_share"
|
||||
hppV2ProrationEggPiece = "laying_egg_piece_share"
|
||||
@@ -109,18 +114,14 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t
|
||||
|
||||
totalPulletCost := 0.0
|
||||
totalProductionCost := 0.0
|
||||
components := make([]HppV2Component, 0, 6)
|
||||
components := make([]HppV2Component, 0, 8)
|
||||
appendComponent := func(component *HppV2Component) {
|
||||
if component == nil || (component.Total == 0 && len(component.Parts) == 0) {
|
||||
return
|
||||
}
|
||||
components = append(components, *component)
|
||||
if componentHasScope(component, hppV2ScopePulletCost) {
|
||||
totalPulletCost += component.Total
|
||||
}
|
||||
if componentHasScope(component, hppV2ScopeProductionCost) {
|
||||
totalProductionCost += component.Total
|
||||
}
|
||||
totalPulletCost += componentScopeTotal(component, hppV2ScopePulletCost)
|
||||
totalProductionCost += componentScopeTotal(component, hppV2ScopeProductionCost)
|
||||
}
|
||||
appendComponent(pakanComponent)
|
||||
|
||||
@@ -154,6 +155,18 @@ func (s *hppV2Service) CalculateHppBreakdown(projectFlockKandangId uint, date *t
|
||||
}
|
||||
appendComponent(bopEkspedisiComponent)
|
||||
|
||||
manualPulletComponent, err := s.getManualPulletCostComponent(projectFlockKandangId, contextRow, startOfDay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appendComponent(manualPulletComponent)
|
||||
|
||||
depreciationComponent, err := s.getDepreciationComponent(projectFlockKandangId, contextRow, startOfDay, totalPulletCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appendComponent(depreciationComponent)
|
||||
|
||||
hppCost, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -527,6 +540,7 @@ func (s *hppV2Service) buildGrowingChickinPart(
|
||||
rows,
|
||||
partCode,
|
||||
partTitle,
|
||||
[]string{hppV2ScopePulletCost},
|
||||
&HppV2Proration{
|
||||
Basis: hppV2ProrationPopulation,
|
||||
Numerator: transferTotalQty,
|
||||
@@ -550,7 +564,7 @@ func (s *hppV2Service) buildLayingChickinPart(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buildChickinPartFromRows(rows, partCode, partTitle, nil, 1), nil
|
||||
return buildChickinPartFromRows(rows, partCode, partTitle, []string{hppV2ScopeProductionCost}, nil, 1), nil
|
||||
}
|
||||
|
||||
func (s *hppV2Service) buildGrowingUsagePart(
|
||||
@@ -653,9 +667,10 @@ func (s *hppV2Service) buildGrowingUsagePart(
|
||||
}
|
||||
|
||||
return &HppV2ComponentPart{
|
||||
Code: partCode,
|
||||
Title: partTitle,
|
||||
Total: baseTotal * ratio,
|
||||
Code: partCode,
|
||||
Title: partTitle,
|
||||
Scopes: []string{hppV2ScopePulletCost},
|
||||
Total: baseTotal * ratio,
|
||||
Proration: &HppV2Proration{
|
||||
Basis: hppV2ProrationPopulation,
|
||||
Numerator: transferTotalQty,
|
||||
@@ -703,6 +718,7 @@ func (s *hppV2Service) buildLayingUsagePart(
|
||||
return &HppV2ComponentPart{
|
||||
Code: hppV2PartLayingCutover,
|
||||
Title: "Laying Cut-over",
|
||||
Scopes: []string{hppV2ScopeProductionCost},
|
||||
Total: total,
|
||||
References: references,
|
||||
}, nil
|
||||
@@ -741,6 +757,7 @@ func (s *hppV2Service) buildLayingUsagePart(
|
||||
return &HppV2ComponentPart{
|
||||
Code: hppV2PartLayingNormal,
|
||||
Title: "Laying",
|
||||
Scopes: []string{hppV2ScopeProductionCost},
|
||||
Total: total,
|
||||
References: references,
|
||||
}, nil
|
||||
@@ -818,6 +835,7 @@ func (s *hppV2Service) buildGrowingExpensePart(
|
||||
rows,
|
||||
map[bool]string{false: hppV2PartGrowingDirect, true: hppV2PartGrowingFarm}[farmLevel],
|
||||
map[bool]string{false: "Growing Direct", true: "Growing Farm"}[farmLevel],
|
||||
[]string{hppV2ScopePulletCost},
|
||||
&HppV2Proration{
|
||||
Basis: hppV2ProrationPopulation,
|
||||
Numerator: transferTotalQty,
|
||||
@@ -838,7 +856,7 @@ func (s *hppV2Service) buildLayingExpenseDirectPart(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buildExpensePartFromRows(rows, hppV2PartLayingDirect, "Laying Direct", nil, 1), nil
|
||||
return buildExpensePartFromRows(rows, hppV2PartLayingDirect, "Laying Direct", []string{hppV2ScopeProductionCost}, nil, 1), nil
|
||||
}
|
||||
|
||||
func (s *hppV2Service) buildLayingExpenseFarmPart(
|
||||
@@ -893,6 +911,7 @@ func (s *hppV2Service) buildLayingExpenseFarmPart(
|
||||
rows,
|
||||
hppV2PartLayingFarm,
|
||||
"Laying Farm",
|
||||
[]string{hppV2ScopeProductionCost},
|
||||
&HppV2Proration{
|
||||
Basis: basis,
|
||||
Numerator: numerator,
|
||||
@@ -903,6 +922,294 @@ func (s *hppV2Service) buildLayingExpenseFarmPart(
|
||||
), nil
|
||||
}
|
||||
|
||||
func (s *hppV2Service) getManualPulletCostComponent(
|
||||
projectFlockKandangId uint,
|
||||
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||
periodDate time.Time,
|
||||
) (*HppV2Component, error) {
|
||||
if s.hppRepo == nil || contextRow == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sourceProjectFlockID != 0 && transferTotalQty > 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
manualInput, err := s.hppRepo.GetManualDepreciationInputByProjectFlockID(context.Background(), contextRow.ProjectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if manualInput == nil || manualInput.TotalCost <= 0 || manualInput.CutoverDate.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
if dateOnly(periodDate).Before(dateOnly(manualInput.CutoverDate)) {
|
||||
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
|
||||
}
|
||||
|
||||
totalPopulation, err := s.hppRepo.GetTotalPopulation(context.Background(), farmPFKIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if totalPopulation <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
targetPopulation, err := s.hppRepo.GetTotalPopulation(context.Background(), []uint{projectFlockKandangId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if targetPopulation <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ratio := targetPopulation / totalPopulation
|
||||
if ratio <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
appliedTotal := manualInput.TotalCost * ratio
|
||||
part := HppV2ComponentPart{
|
||||
Code: hppV2PartManualCutover,
|
||||
Title: "Manual Cut-over",
|
||||
Scopes: []string{hppV2ScopePulletCost},
|
||||
Total: appliedTotal,
|
||||
Proration: &HppV2Proration{
|
||||
Basis: hppV2ProrationPopulation,
|
||||
Numerator: targetPopulation,
|
||||
Denominator: totalPopulation,
|
||||
Ratio: ratio,
|
||||
},
|
||||
Details: map[string]any{
|
||||
"cutover_date": formatDateOnly(manualInput.CutoverDate),
|
||||
"farm_total_cost": manualInput.TotalCost,
|
||||
"target_population": targetPopulation,
|
||||
"farm_population": totalPopulation,
|
||||
},
|
||||
References: []HppV2Reference{
|
||||
{
|
||||
Type: "farm_depreciation_manual_input",
|
||||
ID: manualInput.ID,
|
||||
Date: formatDateOnly(manualInput.CutoverDate),
|
||||
Qty: 1,
|
||||
Total: manualInput.TotalCost,
|
||||
AppliedTotal: appliedTotal,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return &HppV2Component{
|
||||
Code: hppV2ComponentManualPulletCost,
|
||||
Title: "Manual Pullet Cost",
|
||||
Scopes: []string{hppV2ScopePulletCost},
|
||||
Total: appliedTotal,
|
||||
Parts: []HppV2ComponentPart{part},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *hppV2Service) getDepreciationComponent(
|
||||
projectFlockKandangId uint,
|
||||
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||
periodDate time.Time,
|
||||
totalPulletCost float64,
|
||||
) (*HppV2Component, error) {
|
||||
if s.hppRepo == nil || contextRow == nil || totalPulletCost <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
transferInput, err := s.hppRepo.GetLatestTransferInputByProjectFlockKandangID(context.Background(), projectFlockKandangId, periodDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var part *HppV2ComponentPart
|
||||
if transferInput != nil && transferInput.SourceProjectFlockID > 0 {
|
||||
part, err = s.buildNormalTransferDepreciationPart(contextRow, transferInput, periodDate, totalPulletCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
part, err = s.buildManualCutoverDepreciationPart(projectFlockKandangId, contextRow, periodDate, totalPulletCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if part == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &HppV2Component{
|
||||
Code: hppV2ComponentDepreciation,
|
||||
Title: "Depreciation",
|
||||
Scopes: []string{hppV2ScopeProductionCost},
|
||||
Total: part.Total,
|
||||
Parts: []HppV2ComponentPart{*part},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *hppV2Service) buildNormalTransferDepreciationPart(
|
||||
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||
transferInput *commonRepo.HppV2LatestTransferInputRow,
|
||||
periodDate time.Time,
|
||||
totalPulletCost float64,
|
||||
) (*HppV2ComponentPart, error) {
|
||||
if contextRow == nil || transferInput == nil || totalPulletCost <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
originDate, err := s.hppRepo.GetEarliestChickInDateByProjectFlockID(context.Background(), transferInput.SourceProjectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if originDate == nil || originDate.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
scheduleDay := DepreciationScheduleDay(*originDate, periodDate, contextRow.HouseType)
|
||||
if scheduleDay <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
houseType := NormalizeDepreciationHouseType(contextRow.HouseType)
|
||||
percentByHouseType, err := s.hppRepo.GetDepreciationPercents(context.Background(), []string{houseType}, scheduleDay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pulletCostDayN, depreciationValue, depreciationPercent := CalculateDepreciationAtDayN(
|
||||
totalPulletCost,
|
||||
scheduleDay,
|
||||
contextRow.HouseType,
|
||||
percentByHouseType,
|
||||
)
|
||||
if depreciationValue <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &HppV2ComponentPart{
|
||||
Code: hppV2PartDepreciationNormal,
|
||||
Title: "Normal Transfer",
|
||||
Scopes: []string{hppV2ScopeProductionCost},
|
||||
Total: depreciationValue,
|
||||
Details: map[string]any{
|
||||
"basis_total": totalPulletCost,
|
||||
"pullet_cost_day_n": pulletCostDayN,
|
||||
"depreciation_percent": depreciationPercent,
|
||||
"schedule_day": scheduleDay,
|
||||
"origin_date": formatDateOnly(*originDate),
|
||||
"transfer_date": formatDateOnly(transferInput.TransferDate),
|
||||
"source_project_flock_id": transferInput.SourceProjectFlockID,
|
||||
},
|
||||
References: []HppV2Reference{
|
||||
{
|
||||
Type: "laying_transfer",
|
||||
ID: transferInput.TransferID,
|
||||
Date: formatDateOnly(transferInput.TransferDate),
|
||||
Qty: transferInput.TransferQty,
|
||||
Total: totalPulletCost,
|
||||
AppliedTotal: depreciationValue,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *hppV2Service) buildManualCutoverDepreciationPart(
|
||||
projectFlockKandangId uint,
|
||||
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||
periodDate time.Time,
|
||||
totalPulletCost float64,
|
||||
) (*HppV2ComponentPart, error) {
|
||||
if contextRow == nil || totalPulletCost <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
manualInput, err := s.hppRepo.GetManualDepreciationInputByProjectFlockID(context.Background(), contextRow.ProjectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if manualInput == nil || manualInput.TotalCost <= 0 || manualInput.CutoverDate.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
if dateOnly(periodDate).Before(dateOnly(manualInput.CutoverDate)) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
originDate, err := s.hppRepo.GetEarliestChickInDateByProjectFlockID(context.Background(), contextRow.ProjectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if originDate == nil || originDate.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
reportScheduleDay := DepreciationScheduleDay(*originDate, periodDate, contextRow.HouseType)
|
||||
if reportScheduleDay <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cutoverScheduleDay := DepreciationScheduleDay(*originDate, manualInput.CutoverDate, contextRow.HouseType)
|
||||
startDay := 1
|
||||
if cutoverScheduleDay > 0 {
|
||||
startDay = cutoverScheduleDay
|
||||
}
|
||||
|
||||
houseType := NormalizeDepreciationHouseType(contextRow.HouseType)
|
||||
percentByHouseType, err := s.hppRepo.GetDepreciationPercents(context.Background(), []string{houseType}, reportScheduleDay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pulletCostDayN, depreciationValue, depreciationPercent := CalculateDepreciationFromDayRange(
|
||||
totalPulletCost,
|
||||
startDay,
|
||||
reportScheduleDay,
|
||||
contextRow.HouseType,
|
||||
percentByHouseType,
|
||||
)
|
||||
if depreciationValue <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &HppV2ComponentPart{
|
||||
Code: hppV2PartDepreciationCutover,
|
||||
Title: "Manual Cut-over",
|
||||
Scopes: []string{hppV2ScopeProductionCost},
|
||||
Total: depreciationValue,
|
||||
Details: map[string]any{
|
||||
"basis_total": totalPulletCost,
|
||||
"pullet_cost_day_n": pulletCostDayN,
|
||||
"depreciation_percent": depreciationPercent,
|
||||
"schedule_day": reportScheduleDay,
|
||||
"start_schedule_day": startDay,
|
||||
"origin_date": formatDateOnly(*originDate),
|
||||
"cutover_date": formatDateOnly(manualInput.CutoverDate),
|
||||
"manual_input_id": manualInput.ID,
|
||||
"project_flock_kandang": projectFlockKandangId,
|
||||
},
|
||||
References: []HppV2Reference{
|
||||
{
|
||||
Type: "farm_depreciation_manual_input",
|
||||
ID: manualInput.ID,
|
||||
Date: formatDateOnly(manualInput.CutoverDate),
|
||||
Qty: 1,
|
||||
Total: totalPulletCost,
|
||||
AppliedTotal: depreciationValue,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *hppV2Service) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) {
|
||||
if s.hppRepo == nil {
|
||||
return &HppCostResponse{}, nil
|
||||
@@ -975,6 +1282,7 @@ func buildExpensePartFromRows(
|
||||
rows []commonRepo.HppV2ExpenseCostRow,
|
||||
code string,
|
||||
title string,
|
||||
scopes []string,
|
||||
proration *HppV2Proration,
|
||||
ratio float64,
|
||||
) *HppV2ComponentPart {
|
||||
@@ -1005,6 +1313,7 @@ func buildExpensePartFromRows(
|
||||
return &HppV2ComponentPart{
|
||||
Code: code,
|
||||
Title: title,
|
||||
Scopes: append([]string{}, scopes...),
|
||||
Total: total,
|
||||
Proration: proration,
|
||||
References: references,
|
||||
@@ -1015,6 +1324,7 @@ func buildChickinPartFromRows(
|
||||
rows []commonRepo.HppV2ChickinCostRow,
|
||||
code string,
|
||||
title string,
|
||||
scopes []string,
|
||||
proration *HppV2Proration,
|
||||
ratio float64,
|
||||
) *HppV2ComponentPart {
|
||||
@@ -1048,6 +1358,7 @@ func buildChickinPartFromRows(
|
||||
return &HppV2ComponentPart{
|
||||
Code: code,
|
||||
Title: title,
|
||||
Scopes: append([]string{}, scopes...),
|
||||
Total: total,
|
||||
Proration: proration,
|
||||
References: references,
|
||||
@@ -1065,3 +1376,48 @@ func componentHasScope(component *HppV2Component, scope string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func componentScopeTotal(component *HppV2Component, scope string) float64 {
|
||||
if component == nil || scope == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
total := 0.0
|
||||
hasPartScopes := false
|
||||
for _, part := range component.Parts {
|
||||
if len(part.Scopes) == 0 {
|
||||
continue
|
||||
}
|
||||
hasPartScopes = true
|
||||
if partHasScope(&part, scope) {
|
||||
total += part.Total
|
||||
}
|
||||
}
|
||||
if hasPartScopes {
|
||||
return total
|
||||
}
|
||||
if componentHasScope(component, scope) {
|
||||
return component.Total
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func partHasScope(part *HppV2ComponentPart, scope string) bool {
|
||||
if part == nil || scope == "" {
|
||||
return false
|
||||
}
|
||||
for _, candidate := range part.Scopes {
|
||||
if candidate == scope {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func dateOnly(value time.Time) time.Time {
|
||||
return time.Date(value.Year(), value.Month(), value.Day(), 0, 0, 0, 0, value.Location())
|
||||
}
|
||||
|
||||
func formatDateOnly(value time.Time) string {
|
||||
return dateOnly(value).Format("2006-01-02")
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ import (
|
||||
type hppV2RepoStub struct {
|
||||
contextByPFK map[uint]*commonRepo.HppV2ProjectFlockKandangContext
|
||||
pfkIDsByProject map[uint][]uint
|
||||
latestTransferByPFK map[uint]*commonRepo.HppV2LatestTransferInputRow
|
||||
manualInputByProject map[uint]*commonRepo.HppV2ManualDepreciationInputRow
|
||||
chickInDateByProject map[uint]*time.Time
|
||||
depreciationByHouse map[string]map[int]float64
|
||||
usageRowsByKey map[string][]commonRepo.HppV2UsageCostRow
|
||||
adjustRowsByKey map[string][]commonRepo.HppV2AdjustmentCostRow
|
||||
chickinRowsByKey map[string][]commonRepo.HppV2ChickinCostRow
|
||||
@@ -47,6 +51,35 @@ func (s *hppV2RepoStub) GetProjectFlockKandangIDs(_ context.Context, projectFloc
|
||||
return append([]uint{}, s.pfkIDsByProject[projectFlockId]...), nil
|
||||
}
|
||||
|
||||
func (s *hppV2RepoStub) GetLatestTransferInputByProjectFlockKandangID(_ context.Context, projectFlockKandangId uint, _ time.Time) (*commonRepo.HppV2LatestTransferInputRow, error) {
|
||||
return s.latestTransferByPFK[projectFlockKandangId], nil
|
||||
}
|
||||
|
||||
func (s *hppV2RepoStub) GetManualDepreciationInputByProjectFlockID(_ context.Context, projectFlockID uint) (*commonRepo.HppV2ManualDepreciationInputRow, error) {
|
||||
return s.manualInputByProject[projectFlockID], nil
|
||||
}
|
||||
|
||||
func (s *hppV2RepoStub) GetEarliestChickInDateByProjectFlockID(_ context.Context, projectFlockID uint) (*time.Time, error) {
|
||||
return s.chickInDateByProject[projectFlockID], nil
|
||||
}
|
||||
|
||||
func (s *hppV2RepoStub) GetDepreciationPercents(_ context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error) {
|
||||
result := make(map[string]map[int]float64)
|
||||
for _, houseType := range houseTypes {
|
||||
source := s.depreciationByHouse[houseType]
|
||||
if len(source) == 0 {
|
||||
continue
|
||||
}
|
||||
result[houseType] = make(map[int]float64)
|
||||
for day, pct := range source {
|
||||
if day <= maxDay {
|
||||
result[houseType][day] = pct
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *hppV2RepoStub) ListUsageCostRowsByProductFlags(_ context.Context, projectFlockKandangIDs []uint, flagNames []string, _ *time.Time) ([]commonRepo.HppV2UsageCostRow, error) {
|
||||
return append([]commonRepo.HppV2UsageCostRow{}, s.usageRowsByKey[stubKey(projectFlockKandangIDs, flagNames)]...), nil
|
||||
}
|
||||
@@ -161,8 +194,11 @@ func TestHppV2CalculateHppBreakdown_ComposesPakanSlices(t *testing.T) {
|
||||
if result == nil {
|
||||
t.Fatal("expected breakdown result")
|
||||
}
|
||||
if got := result.TotalProductionCost; got != 2950 {
|
||||
t.Fatalf("expected total production cost 2950, got %v", got)
|
||||
if got := result.TotalPulletCost; got != 1150 {
|
||||
t.Fatalf("expected total pullet cost 1150, got %v", got)
|
||||
}
|
||||
if got := result.TotalProductionCost; got != 1800 {
|
||||
t.Fatalf("expected total production cost 1800, got %v", got)
|
||||
}
|
||||
if len(result.Components) != 1 {
|
||||
t.Fatalf("expected 1 component, got %d", len(result.Components))
|
||||
@@ -190,11 +226,11 @@ func TestHppV2CalculateHppBreakdown_ComposesPakanSlices(t *testing.T) {
|
||||
if component.Parts[0].Proration == nil || component.Parts[0].Proration.Ratio != 0.25 {
|
||||
t.Fatalf("expected growing proration ratio 0.25, got %+v", component.Parts[0].Proration)
|
||||
}
|
||||
if result.Hpp.Estimation.HargaKg != 295 {
|
||||
t.Fatalf("expected estimation harga/kg 295, got %v", result.Hpp.Estimation.HargaKg)
|
||||
if result.Hpp.Estimation.HargaKg != 180 {
|
||||
t.Fatalf("expected estimation harga/kg 180, got %v", result.Hpp.Estimation.HargaKg)
|
||||
}
|
||||
if result.Hpp.Real.HargaKg != 737.5 {
|
||||
t.Fatalf("expected real harga/kg 737.5, got %v", result.Hpp.Real.HargaKg)
|
||||
if result.Hpp.Real.HargaKg != 450 {
|
||||
t.Fatalf("expected real harga/kg 450, got %v", result.Hpp.Real.HargaKg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,11 +372,14 @@ func TestHppV2CalculateHppBreakdown_IncludesOvkComponent(t *testing.T) {
|
||||
if componentTotals[hppV2ComponentOvk] != 450 {
|
||||
t.Fatalf("expected ovk total 450, got %v", componentTotals[hppV2ComponentOvk])
|
||||
}
|
||||
if result.TotalProductionCost != 950 {
|
||||
t.Fatalf("expected total production cost 950, got %v", result.TotalProductionCost)
|
||||
if result.TotalPulletCost != 250 {
|
||||
t.Fatalf("expected total pullet cost 250, got %v", result.TotalPulletCost)
|
||||
}
|
||||
if result.Hpp.Estimation.HargaKg != 79.17 {
|
||||
t.Fatalf("expected estimation harga/kg 79.17, got %v", result.Hpp.Estimation.HargaKg)
|
||||
if result.TotalProductionCost != 700 {
|
||||
t.Fatalf("expected total production cost 700, got %v", result.TotalProductionCost)
|
||||
}
|
||||
if result.Hpp.Estimation.HargaKg != 58.33 {
|
||||
t.Fatalf("expected estimation harga/kg 58.33, got %v", result.Hpp.Estimation.HargaKg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,11 +542,204 @@ func TestHppV2CalculateHppBreakdown_IncludesBopRegularAndEkspedisi(t *testing.T)
|
||||
if componentTotals[hppV2ComponentBopEksp] != 88 {
|
||||
t.Fatalf("expected expedition BOP total 88, got %v", componentTotals[hppV2ComponentBopEksp])
|
||||
}
|
||||
if result.TotalProductionCost != 358 {
|
||||
t.Fatalf("expected total production cost 358, got %v", result.TotalProductionCost)
|
||||
if result.TotalPulletCost != 190 {
|
||||
t.Fatalf("expected total pullet cost 190, got %v", result.TotalPulletCost)
|
||||
}
|
||||
if result.Hpp.Estimation.HargaKg != 119.33 {
|
||||
t.Fatalf("expected estimation harga/kg 119.33, got %v", result.Hpp.Estimation.HargaKg)
|
||||
if result.TotalProductionCost != 168 {
|
||||
t.Fatalf("expected total production cost 168, got %v", result.TotalProductionCost)
|
||||
}
|
||||
if result.Hpp.Estimation.HargaKg != 56 {
|
||||
t.Fatalf("expected estimation harga/kg 56, got %v", result.Hpp.Estimation.HargaKg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHppV2CalculateHppBreakdown_AddsDepreciationForNormalTransfer(t *testing.T) {
|
||||
sourceChickIn := mustTime(t, "2026-01-01")
|
||||
reportDate := sourceChickIn.AddDate(0, 0, 154)
|
||||
|
||||
repo := &hppV2RepoStub{
|
||||
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
|
||||
50: {
|
||||
ProjectFlockKandangID: 50,
|
||||
ProjectFlockID: 10,
|
||||
ProjectFlockCategory: "LAYING",
|
||||
KandangID: 500,
|
||||
KandangName: "Kandang F",
|
||||
LocationID: 21,
|
||||
HouseType: "close_house",
|
||||
},
|
||||
},
|
||||
pfkIDsByProject: map[uint][]uint{
|
||||
11: {501},
|
||||
},
|
||||
latestTransferByPFK: map[uint]*commonRepo.HppV2LatestTransferInputRow{
|
||||
50: {
|
||||
ProjectFlockKandangID: 50,
|
||||
SourceProjectFlockID: 11,
|
||||
TransferDate: mustTime(t, "2026-05-20"),
|
||||
TransferQty: 100,
|
||||
TransferID: 701,
|
||||
},
|
||||
},
|
||||
chickInDateByProject: map[uint]*time.Time{
|
||||
11: &sourceChickIn,
|
||||
},
|
||||
depreciationByHouse: map[string]map[int]float64{
|
||||
"close_house": {
|
||||
1: 10,
|
||||
},
|
||||
},
|
||||
usageRowsByKey: map[string][]commonRepo.HppV2UsageCostRow{
|
||||
stubKey([]uint{501}, []string{"PAKAN"}): {
|
||||
{StockableType: "purchase_items", StockableID: 9301, SourceProductID: 41, SourceProductName: "Pakan Growing", Qty: 25, UnitPrice: 40, TotalCost: 1000},
|
||||
},
|
||||
},
|
||||
totalPopulationByKey: map[string]float64{
|
||||
stubKey([]uint{501}, nil): 100,
|
||||
},
|
||||
transferSummaryByPFK: map[uint]struct {
|
||||
projectFlockID uint
|
||||
totalQty float64
|
||||
}{
|
||||
50: {projectFlockID: 11, totalQty: 100},
|
||||
},
|
||||
eggProductionByPFK: map[uint]struct {
|
||||
pieces float64
|
||||
kg float64
|
||||
}{
|
||||
50: {pieces: 20, kg: 10},
|
||||
},
|
||||
}
|
||||
|
||||
svc := NewHppV2Service(repo)
|
||||
result, err := svc.CalculateHppBreakdown(50, &reportDate)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if result.TotalPulletCost != 1000 {
|
||||
t.Fatalf("expected total pullet cost 1000, got %v", result.TotalPulletCost)
|
||||
}
|
||||
if result.TotalProductionCost != 100 {
|
||||
t.Fatalf("expected total production cost 100, got %v", result.TotalProductionCost)
|
||||
}
|
||||
|
||||
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 != 100 {
|
||||
t.Fatalf("expected depreciation total 100, got %v", depreciation.Total)
|
||||
}
|
||||
if len(depreciation.Parts) != 1 {
|
||||
t.Fatalf("expected single depreciation part, got %d", len(depreciation.Parts))
|
||||
}
|
||||
if depreciation.Parts[0].Details["schedule_day"] != 1 {
|
||||
t.Fatalf("expected schedule day 1, got %+v", depreciation.Parts[0].Details)
|
||||
}
|
||||
if depreciation.Parts[0].Details["origin_date"] != "2026-01-01" {
|
||||
t.Fatalf("expected origin date 2026-01-01, got %+v", depreciation.Parts[0].Details)
|
||||
}
|
||||
if result.Hpp.Estimation.HargaKg != 10 {
|
||||
t.Fatalf("expected estimation harga/kg 10, got %v", result.Hpp.Estimation.HargaKg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHppV2CalculateHppBreakdown_AddsDepreciationForManualCutoverFromCutoverDate(t *testing.T) {
|
||||
originDate := mustTime(t, "2026-01-01")
|
||||
cutoverDate := originDate.AddDate(0, 0, 155)
|
||||
|
||||
repo := &hppV2RepoStub{
|
||||
contextByPFK: map[uint]*commonRepo.HppV2ProjectFlockKandangContext{
|
||||
60: {
|
||||
ProjectFlockKandangID: 60,
|
||||
ProjectFlockID: 12,
|
||||
ProjectFlockCategory: "LAYING",
|
||||
KandangID: 600,
|
||||
KandangName: "Kandang G",
|
||||
LocationID: 22,
|
||||
HouseType: "close_house",
|
||||
},
|
||||
},
|
||||
pfkIDsByProject: map[uint][]uint{
|
||||
12: {60},
|
||||
},
|
||||
manualInputByProject: map[uint]*commonRepo.HppV2ManualDepreciationInputRow{
|
||||
12: {
|
||||
ID: 801,
|
||||
ProjectFlockID: 12,
|
||||
TotalCost: 1000,
|
||||
CutoverDate: cutoverDate,
|
||||
},
|
||||
},
|
||||
chickInDateByProject: map[uint]*time.Time{
|
||||
12: &originDate,
|
||||
},
|
||||
depreciationByHouse: map[string]map[int]float64{
|
||||
"close_house": {
|
||||
1: 10,
|
||||
2: 20,
|
||||
},
|
||||
},
|
||||
totalPopulationByKey: map[string]float64{
|
||||
stubKey([]uint{60}, nil): 100,
|
||||
},
|
||||
eggProductionByPFK: map[uint]struct {
|
||||
pieces float64
|
||||
kg float64
|
||||
}{
|
||||
60: {pieces: 20, kg: 10},
|
||||
},
|
||||
}
|
||||
|
||||
svc := NewHppV2Service(repo)
|
||||
result, err := svc.CalculateHppBreakdown(60, &cutoverDate)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if result.TotalPulletCost != 1000 {
|
||||
t.Fatalf("expected total pullet cost 1000, got %v", result.TotalPulletCost)
|
||||
}
|
||||
if result.TotalProductionCost != 200 {
|
||||
t.Fatalf("expected total production cost 200, got %v", result.TotalProductionCost)
|
||||
}
|
||||
|
||||
componentTotals := map[string]float64{}
|
||||
for _, component := range result.Components {
|
||||
componentTotals[component.Code] = component.Total
|
||||
}
|
||||
if componentTotals[hppV2ComponentManualPulletCost] != 1000 {
|
||||
t.Fatalf("expected manual pullet cost 1000, got %v", componentTotals[hppV2ComponentManualPulletCost])
|
||||
}
|
||||
if componentTotals[hppV2ComponentDepreciation] != 200 {
|
||||
t.Fatalf("expected depreciation 200, got %v", componentTotals[hppV2ComponentDepreciation])
|
||||
}
|
||||
|
||||
var depreciation *HppV2Component
|
||||
for i := range result.Components {
|
||||
if result.Components[i].Code == hppV2ComponentDepreciation {
|
||||
depreciation = &result.Components[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if depreciation == nil || len(depreciation.Parts) != 1 {
|
||||
t.Fatalf("expected one depreciation part, got %+v", depreciation)
|
||||
}
|
||||
if depreciation.Parts[0].Details["schedule_day"] != 2 {
|
||||
t.Fatalf("expected schedule day 2, got %+v", depreciation.Parts[0].Details)
|
||||
}
|
||||
if depreciation.Parts[0].Details["start_schedule_day"] != 2 {
|
||||
t.Fatalf("expected start schedule day 2, got %+v", depreciation.Parts[0].Details)
|
||||
}
|
||||
if result.Hpp.Estimation.HargaKg != 20 {
|
||||
t.Fatalf("expected estimation harga/kg 20, got %v", result.Hpp.Estimation.HargaKg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user