Files
lti-api/internal/modules/repports/services/repport.expense_depreciation_test.go
T
2026-04-19 17:27:42 +07:00

446 lines
13 KiB
Go

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)
}
}