mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
adjust common hpp v2
This commit is contained in:
@@ -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 {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.ExpenseDepreciationRepo.DeleteSnapshotsFromDate(
|
||||
ctx.Context(),
|
||||
cutoverDate,
|
||||
[]uint{row.ProjectFlockId},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &dto.ExpenseDepreciationManualInputRowDTO{
|
||||
ID: int64(row.Id),
|
||||
@@ -456,6 +463,11 @@ type depreciationKandangComponent struct {
|
||||
TransferQty float64 `json:"transfer_qty"`
|
||||
PulletCostDayN float64 `json:"pullet_cost_day_n"`
|
||||
DepreciationValue float64 `json:"depreciation_value"`
|
||||
DepreciationSource string `json:"depreciation_source,omitempty"`
|
||||
ManualInputID *uint `json:"manual_input_id,omitempty"`
|
||||
CutoverDate string `json:"cutover_date,omitempty"`
|
||||
OriginDate string `json:"origin_date,omitempty"`
|
||||
StartScheduleDay *int `json:"start_schedule_day,omitempty"`
|
||||
}
|
||||
|
||||
type depreciationFarmComponents struct {
|
||||
@@ -469,124 +481,98 @@ func (s *repportService) computeExpenseDepreciationSnapshots(
|
||||
farmIDs []uint,
|
||||
farmNameByID map[uint]string,
|
||||
) ([]entity.FarmDepreciationSnapshot, error) {
|
||||
_ = farmNameByID
|
||||
|
||||
if len(farmIDs) == 0 {
|
||||
return []entity.FarmDepreciationSnapshot{}, nil
|
||||
}
|
||||
|
||||
inputRows, err := s.ExpenseDepreciationRepo.GetLatestTransferInputsByFarms(ctx, periodDate, farmIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if s.HppCostRepo == nil {
|
||||
return nil, errors.New("hpp cost repository is not configured")
|
||||
}
|
||||
|
||||
groupedByFarm := make(map[uint][]repportRepo.FarmDepreciationLatestTransferRow, len(farmIDs))
|
||||
houseTypeSet := make(map[string]struct{})
|
||||
maxDay := 0
|
||||
|
||||
for _, row := range inputRows {
|
||||
groupedByFarm[row.ProjectFlockID] = append(groupedByFarm[row.ProjectFlockID], row)
|
||||
dayN := approvalService.DepreciationScheduleDay(row.TransferDate, periodDate, valueOrEmptyString(row.HouseType))
|
||||
if dayN > maxDay {
|
||||
maxDay = dayN
|
||||
}
|
||||
houseType := approvalService.NormalizeDepreciationHouseType(valueOrEmptyString(row.HouseType))
|
||||
if houseType != "" {
|
||||
houseTypeSet[houseType] = struct{}{}
|
||||
}
|
||||
if s.HppV2Svc == nil {
|
||||
return nil, errors.New("hpp v2 service is not configured")
|
||||
}
|
||||
|
||||
houseTypes := make([]string, 0, len(houseTypeSet))
|
||||
for houseType := range houseTypeSet {
|
||||
houseTypes = append(houseTypes, houseType)
|
||||
}
|
||||
sort.Strings(houseTypes)
|
||||
|
||||
percentByHouseType, err := s.ExpenseDepreciationRepo.GetDepreciationPercents(ctx, houseTypes, maxDay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type sourceCostCacheItem struct {
|
||||
totalDepCost float64
|
||||
}
|
||||
sourceCostCache := make(map[string]sourceCostCacheItem)
|
||||
sourcePopulationCache := make(map[uint]float64)
|
||||
|
||||
result := make([]entity.FarmDepreciationSnapshot, 0, len(farmIDs))
|
||||
for _, farmID := range farmIDs {
|
||||
farmRows := groupedByFarm[farmID]
|
||||
kandangIDs, err := s.HppCostRepo.GetProjectFlockKandangIDs(ctx, farmID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
components := depreciationFarmComponents{
|
||||
KandangCount: len(farmRows),
|
||||
Kandang: make([]depreciationKandangComponent, 0, len(farmRows)),
|
||||
Kandang: make([]depreciationKandangComponent, 0, len(kandangIDs)),
|
||||
}
|
||||
|
||||
totalDepreciationValue := 0.0
|
||||
totalPulletCostDayN := 0.0
|
||||
for _, row := range farmRows {
|
||||
dayN := approvalService.DepreciationScheduleDay(row.TransferDate, periodDate, valueOrEmptyString(row.HouseType))
|
||||
houseType := approvalService.NormalizeDepreciationHouseType(valueOrEmptyString(row.HouseType))
|
||||
for _, kandangID := range kandangIDs {
|
||||
breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &periodDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if breakdown == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
transferDateKey := row.TransferDate.Format("2006-01-02")
|
||||
cacheKey := fmt.Sprintf("%d|%s", row.SourceProjectFlockID, transferDateKey)
|
||||
cached, exists := sourceCostCache[cacheKey]
|
||||
if !exists {
|
||||
endOfDay := row.TransferDate.Add(24 * time.Hour)
|
||||
sourceDepCost, calcErr := s.HppSvc.GetTotalDepresiasiFlockGrowing(row.SourceProjectFlockID, &endOfDay)
|
||||
if calcErr != nil {
|
||||
return nil, calcErr
|
||||
depreciationComponent := hppV2FindDepreciationComponent(breakdown)
|
||||
if depreciationComponent == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, part := range depreciationComponent.Parts {
|
||||
if part.Total <= 0 {
|
||||
continue
|
||||
}
|
||||
cached = sourceCostCacheItem{totalDepCost: sourceDepCost}
|
||||
sourceCostCache[cacheKey] = cached
|
||||
}
|
||||
|
||||
sourcePopulation, popExists := sourcePopulationCache[row.SourceProjectFlockID]
|
||||
if !popExists {
|
||||
if s.HppCostRepo == nil {
|
||||
sourcePopulation = 0
|
||||
} else {
|
||||
kandangIDs, idsErr := s.HppCostRepo.GetProjectFlockKandangIDs(ctx, row.SourceProjectFlockID)
|
||||
if idsErr != nil {
|
||||
return nil, idsErr
|
||||
}
|
||||
population, popErr := s.HppCostRepo.GetTotalPopulation(ctx, kandangIDs)
|
||||
if popErr != nil {
|
||||
return nil, popErr
|
||||
}
|
||||
sourcePopulation = population
|
||||
houseType := approvalService.NormalizeDepreciationHouseType(breakdown.HouseType)
|
||||
component := depreciationKandangComponent{
|
||||
ProjectFlockKandangID: breakdown.ProjectFlockKandangID,
|
||||
KandangID: breakdown.KandangID,
|
||||
KandangName: breakdown.KandangName,
|
||||
SourceProjectFlockID: hppV2DetailUint(part.Details, "source_project_flock_id"),
|
||||
HouseType: houseType,
|
||||
DayN: hppV2DetailInt(part.Details, "schedule_day"),
|
||||
DepreciationPercent: hppV2DetailFloat(part.Details, "depreciation_percent"),
|
||||
PulletCostDayN: hppV2DetailFloat(part.Details, "pullet_cost_day_n"),
|
||||
DepreciationValue: part.Total,
|
||||
DepreciationSource: part.Code,
|
||||
OriginDate: hppV2DetailString(part.Details, "origin_date"),
|
||||
}
|
||||
sourcePopulationCache[row.SourceProjectFlockID] = sourcePopulation
|
||||
|
||||
if component.HouseType == "" {
|
||||
component.HouseType = approvalService.NormalizeDepreciationHouseType(hppV2DetailString(part.Details, "house_type"))
|
||||
}
|
||||
|
||||
if ref := hppV2FindReference(part.References, "laying_transfer"); ref != nil {
|
||||
component.TransferID = ref.ID
|
||||
component.TransferDate = ref.Date
|
||||
component.TransferQty = ref.Qty
|
||||
}
|
||||
|
||||
if part.Code == "manual_cutover" {
|
||||
if startDay := hppV2DetailInt(part.Details, "start_schedule_day"); startDay > 0 {
|
||||
component.StartScheduleDay = &startDay
|
||||
}
|
||||
component.CutoverDate = hppV2DetailString(part.Details, "cutover_date")
|
||||
if manualID := hppV2DetailUint(part.Details, "manual_input_id"); manualID > 0 {
|
||||
component.ManualInputID = &manualID
|
||||
}
|
||||
if component.ManualInputID == nil {
|
||||
if ref := hppV2FindReference(part.References, "farm_depreciation_manual_input"); ref != nil && ref.ID > 0 {
|
||||
manualID := ref.ID
|
||||
component.ManualInputID = &manualID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalPulletCostDayN += component.PulletCostDayN
|
||||
totalDepreciationValue += component.DepreciationValue
|
||||
components.Kandang = append(components.Kandang, component)
|
||||
}
|
||||
|
||||
initialPulletCost := 0.0
|
||||
if sourcePopulation > 0 {
|
||||
initialPulletCost = (cached.totalDepCost * row.TransferQty) / sourcePopulation
|
||||
}
|
||||
|
||||
pulletCostDayN, depreciationValue, depreciationPercent := approvalService.CalculateDepreciationAtDayN(
|
||||
initialPulletCost,
|
||||
dayN,
|
||||
houseType,
|
||||
percentByHouseType,
|
||||
)
|
||||
|
||||
totalPulletCostDayN += pulletCostDayN
|
||||
totalDepreciationValue += depreciationValue
|
||||
|
||||
components.Kandang = append(components.Kandang, depreciationKandangComponent{
|
||||
ProjectFlockKandangID: row.ProjectFlockKandangID,
|
||||
KandangID: row.KandangID,
|
||||
KandangName: row.KandangName,
|
||||
TransferID: row.TransferID,
|
||||
TransferDate: row.TransferDate.Format("2006-01-02"),
|
||||
SourceProjectFlockID: row.SourceProjectFlockID,
|
||||
HouseType: houseType,
|
||||
DayN: dayN,
|
||||
DepreciationPercent: depreciationPercent,
|
||||
TransferQty: row.TransferQty,
|
||||
PulletCostDayN: pulletCostDayN,
|
||||
DepreciationValue: depreciationValue,
|
||||
})
|
||||
}
|
||||
|
||||
components.KandangCount = len(components.Kandang)
|
||||
effectivePercent := approvalService.CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN)
|
||||
|
||||
componentsJSON, marshalErr := json.Marshal(components)
|
||||
@@ -607,6 +593,106 @@ func (s *repportService) computeExpenseDepreciationSnapshots(
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func hppV2FindDepreciationComponent(breakdown *approvalService.HppV2Breakdown) *approvalService.HppV2Component {
|
||||
if breakdown == nil {
|
||||
return nil
|
||||
}
|
||||
for idx := range breakdown.Components {
|
||||
if breakdown.Components[idx].Code == "DEPRECIATION" {
|
||||
return &breakdown.Components[idx]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hppV2FindReference(references []approvalService.HppV2Reference, refType string) *approvalService.HppV2Reference {
|
||||
if refType == "" {
|
||||
return nil
|
||||
}
|
||||
for idx := range references {
|
||||
if references[idx].Type == refType {
|
||||
return &references[idx]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hppV2DetailFloat(details map[string]any, key string) float64 {
|
||||
if details == nil || key == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
raw, exists := details[key]
|
||||
if !exists || raw == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
switch value := raw.(type) {
|
||||
case float64:
|
||||
return value
|
||||
case float32:
|
||||
return float64(value)
|
||||
case int:
|
||||
return float64(value)
|
||||
case int8:
|
||||
return float64(value)
|
||||
case int16:
|
||||
return float64(value)
|
||||
case int32:
|
||||
return float64(value)
|
||||
case int64:
|
||||
return float64(value)
|
||||
case uint:
|
||||
return float64(value)
|
||||
case uint8:
|
||||
return float64(value)
|
||||
case uint16:
|
||||
return float64(value)
|
||||
case uint32:
|
||||
return float64(value)
|
||||
case uint64:
|
||||
return float64(value)
|
||||
case string:
|
||||
parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return parsed
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func hppV2DetailInt(details map[string]any, key string) int {
|
||||
return int(math.Round(hppV2DetailFloat(details, key)))
|
||||
}
|
||||
|
||||
func hppV2DetailUint(details map[string]any, key string) uint {
|
||||
value := hppV2DetailInt(details, key)
|
||||
if value < 0 {
|
||||
return 0
|
||||
}
|
||||
return uint(value)
|
||||
}
|
||||
|
||||
func hppV2DetailString(details map[string]any, key string) string {
|
||||
if details == nil || key == "" {
|
||||
return ""
|
||||
}
|
||||
raw, exists := details[key]
|
||||
if !exists || raw == nil {
|
||||
return ""
|
||||
}
|
||||
switch value := raw.(type) {
|
||||
case string:
|
||||
return value
|
||||
case time.Time:
|
||||
return value.Format("2006-01-02")
|
||||
default:
|
||||
return fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func parseSnapshotComponents(raw []byte) any {
|
||||
if len(raw) == 0 {
|
||||
return map[string]any{}
|
||||
@@ -2280,13 +2366,15 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
||||
}
|
||||
if hppCost != nil {
|
||||
eggPiecesFloatRemaining = hppCost.Estimation.Butir - hppCost.Real.Butir
|
||||
eggHpp = hppCost.Estimation.HargaKg
|
||||
// eggHpp = hppCost.Estimation.HargaKg
|
||||
eggHpp = hppCost.Real.HargaKg
|
||||
eggTotalPiecesFloat = hppCost.Estimation.Butir
|
||||
eggWeightFloat = hppCost.Estimation.Kg
|
||||
if eggTotalPiecesFloat > 0 {
|
||||
avgWeight = eggWeightFloat / eggTotalPiecesFloat
|
||||
}
|
||||
eggRemainingWeightFloatRemaining = avgWeight * eggPiecesFloatRemaining
|
||||
// eggRemainingWeightFloatRemaining = avgWeight * eggPiecesFloatRemaining
|
||||
eggRemainingWeightFloatRemaining = hppCost.Estimation.Kg - hppCost.Real.Kg
|
||||
}
|
||||
}
|
||||
if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {
|
||||
|
||||
Reference in New Issue
Block a user