mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
729 lines
22 KiB
Go
729 lines
22 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"
|
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
|
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
|
|
candidateRows []repportRepo.FarmDepreciationCandidateRow
|
|
snapshots []entity.FarmDepreciationSnapshot
|
|
|
|
upsertedRow *entity.FarmDepreciationManualInput
|
|
deleteCalled bool
|
|
deleteDate time.Time
|
|
deleteFarmIDs []uint
|
|
upsertSnapshotCalls int
|
|
upsertedSnapshots []entity.FarmDepreciationSnapshot
|
|
getSnapshotsCalls int
|
|
}
|
|
|
|
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) GetCandidateFarms(_ context.Context, _ time.Time, _ []int64, _ []int64, _ []int64) ([]repportRepo.FarmDepreciationCandidateRow, error) {
|
|
return append([]repportRepo.FarmDepreciationCandidateRow{}, m.candidateRows...), nil
|
|
}
|
|
|
|
func (m *expenseDepreciationRepoMock) GetSnapshotsByPeriodAndFarmIDs(_ context.Context, period time.Time, farmIDs []uint) ([]entity.FarmDepreciationSnapshot, error) {
|
|
m.getSnapshotsCalls++
|
|
if len(farmIDs) == 0 {
|
|
return []entity.FarmDepreciationSnapshot{}, nil
|
|
}
|
|
allowed := make(map[uint]struct{}, len(farmIDs))
|
|
for _, farmID := range farmIDs {
|
|
allowed[farmID] = struct{}{}
|
|
}
|
|
result := make([]entity.FarmDepreciationSnapshot, 0, len(m.snapshots))
|
|
for _, row := range m.snapshots {
|
|
if _, ok := allowed[row.ProjectFlockId]; !ok {
|
|
continue
|
|
}
|
|
if row.PeriodDate.IsZero() || row.PeriodDate.Format("2006-01-02") == period.Format("2006-01-02") {
|
|
result = append(result, row)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (m *expenseDepreciationRepoMock) UpsertSnapshots(_ context.Context, rows []entity.FarmDepreciationSnapshot) error {
|
|
m.upsertSnapshotCalls++
|
|
m.upsertedSnapshots = append([]entity.FarmDepreciationSnapshot{}, rows...)
|
|
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 expenseRealizationRepoMock struct {
|
|
expenseRepo.ExpenseRealizationRepository
|
|
db *gorm.DB
|
|
}
|
|
|
|
func (m *expenseRealizationRepoMock) DB() *gorm.DB {
|
|
return m.db
|
|
}
|
|
|
|
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 TestGetExpenseDepreciation_UsesExistingSnapshotWhenForceRecomputeFalse(t *testing.T) {
|
|
periodDate := mustJakartaDate(t, "2026-06-05")
|
|
repo := &expenseDepreciationRepoMock{
|
|
candidateRows: []repportRepo.FarmDepreciationCandidateRow{
|
|
{ProjectFlockID: 1, FarmName: "Farm A"},
|
|
},
|
|
snapshots: []entity.FarmDepreciationSnapshot{
|
|
{
|
|
ProjectFlockId: 1,
|
|
PeriodDate: periodDate,
|
|
DepreciationPercentEffective: 11.1,
|
|
DepreciationValue: 111,
|
|
PulletCostDayNTotal: 1001,
|
|
Components: json.RawMessage(`{"kandang_count":0,"kandang":[]}`),
|
|
},
|
|
},
|
|
}
|
|
|
|
svc := &repportService{
|
|
Validate: validator.New(),
|
|
ExpenseDepreciationRepo: repo,
|
|
ExpenseRealizationRepo: &expenseRealizationRepoMock{},
|
|
HppCostRepo: &hppCostRepoMock{},
|
|
HppV2Svc: &hppV2ServiceMock{},
|
|
}
|
|
|
|
rows, meta, err := getExpenseDepreciationByQuery(t, svc, "page=1&limit=10&period=2026-06-05")
|
|
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 != 111 {
|
|
t.Fatalf("expected depreciation value 111, got %v", rows[0].DepreciationValue)
|
|
}
|
|
if meta == nil || meta.TotalResults != 1 {
|
|
t.Fatalf("expected meta total_results 1, got %+v", meta)
|
|
}
|
|
if repo.upsertSnapshotCalls != 0 {
|
|
t.Fatalf("expected no snapshot upsert, got %d", repo.upsertSnapshotCalls)
|
|
}
|
|
if repo.getSnapshotsCalls != 1 {
|
|
t.Fatalf("expected snapshot fetch called once, got %d", repo.getSnapshotsCalls)
|
|
}
|
|
}
|
|
|
|
func TestGetExpenseDepreciation_ForceRecomputeRebuildsAllSnapshots(t *testing.T) {
|
|
periodDate := mustJakartaDate(t, "2026-06-05")
|
|
repo := &expenseDepreciationRepoMock{
|
|
candidateRows: []repportRepo.FarmDepreciationCandidateRow{
|
|
{ProjectFlockID: 1, FarmName: "Farm A"},
|
|
},
|
|
snapshots: []entity.FarmDepreciationSnapshot{
|
|
{
|
|
ProjectFlockId: 1,
|
|
PeriodDate: periodDate,
|
|
DepreciationValue: 999,
|
|
PulletCostDayNTotal: 999,
|
|
},
|
|
},
|
|
}
|
|
|
|
svc := &repportService{
|
|
Validate: validator.New(),
|
|
ExpenseDepreciationRepo: repo,
|
|
ExpenseRealizationRepo: &expenseRealizationRepoMock{},
|
|
HppCostRepo: &hppCostRepoMock{
|
|
kandangIDsByFarm: map[uint][]uint{
|
|
1: {10},
|
|
},
|
|
},
|
|
HppV2Svc: &hppV2ServiceMock{
|
|
breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{
|
|
10: depreciationBreakdown(10, 100, "Kandang A", 100, 1000, 10),
|
|
},
|
|
},
|
|
}
|
|
|
|
rows, _, err := getExpenseDepreciationByQuery(t, svc, "page=1&limit=10&period=2026-06-05&force_recompute=true")
|
|
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 recomputed depreciation value 100, got %v", rows[0].DepreciationValue)
|
|
}
|
|
if repo.upsertSnapshotCalls != 1 {
|
|
t.Fatalf("expected snapshot upsert called once, got %d", repo.upsertSnapshotCalls)
|
|
}
|
|
if len(repo.upsertedSnapshots) != 1 || repo.upsertedSnapshots[0].ProjectFlockId != 1 {
|
|
t.Fatalf("expected upserted snapshot for farm 1, got %+v", repo.upsertedSnapshots)
|
|
}
|
|
if repo.getSnapshotsCalls != 0 {
|
|
t.Fatalf("expected no snapshot fetch in force recompute mode, got %d", repo.getSnapshotsCalls)
|
|
}
|
|
}
|
|
|
|
func TestGetExpenseDepreciation_ForceRecomputeFalseComputesOnlyMissingFarms(t *testing.T) {
|
|
periodDate := mustJakartaDate(t, "2026-06-05")
|
|
repo := &expenseDepreciationRepoMock{
|
|
candidateRows: []repportRepo.FarmDepreciationCandidateRow{
|
|
{ProjectFlockID: 1, FarmName: "Farm A"},
|
|
{ProjectFlockID: 2, FarmName: "Farm B"},
|
|
},
|
|
snapshots: []entity.FarmDepreciationSnapshot{
|
|
{
|
|
ProjectFlockId: 1,
|
|
PeriodDate: periodDate,
|
|
DepreciationPercentEffective: 11.1,
|
|
DepreciationValue: 111,
|
|
PulletCostDayNTotal: 1001,
|
|
Components: json.RawMessage(`{"kandang_count":0,"kandang":[]}`),
|
|
},
|
|
},
|
|
}
|
|
|
|
svc := &repportService{
|
|
Validate: validator.New(),
|
|
ExpenseDepreciationRepo: repo,
|
|
ExpenseRealizationRepo: &expenseRealizationRepoMock{},
|
|
HppCostRepo: &hppCostRepoMock{
|
|
kandangIDsByFarm: map[uint][]uint{
|
|
1: {10},
|
|
2: {20},
|
|
},
|
|
},
|
|
HppV2Svc: &hppV2ServiceMock{
|
|
breakdownByPFK: map[uint]*approvalService.HppV2Breakdown{
|
|
10: depreciationBreakdown(10, 100, "Kandang A", 999, 9999, 10),
|
|
20: depreciationBreakdown(20, 200, "Kandang B", 200, 2000, 10),
|
|
},
|
|
},
|
|
}
|
|
|
|
rows, _, err := getExpenseDepreciationByQuery(t, svc, "page=1&limit=10&period=2026-06-05")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("expected 2 rows, got %d", len(rows))
|
|
}
|
|
if rows[0].ProjectFlockID != 1 || rows[0].DepreciationValue != 111 {
|
|
t.Fatalf("expected farm 1 use existing snapshot value 111, got %+v", rows[0])
|
|
}
|
|
if rows[1].ProjectFlockID != 2 || rows[1].DepreciationValue != 200 {
|
|
t.Fatalf("expected farm 2 recomputed value 200, got %+v", rows[1])
|
|
}
|
|
if repo.upsertSnapshotCalls != 1 {
|
|
t.Fatalf("expected one upsert call for missing farms, got %d", repo.upsertSnapshotCalls)
|
|
}
|
|
if len(repo.upsertedSnapshots) != 1 || repo.upsertedSnapshots[0].ProjectFlockId != 2 {
|
|
t.Fatalf("expected upsert only farm 2 snapshot, got %+v", repo.upsertedSnapshots)
|
|
}
|
|
if repo.getSnapshotsCalls != 1 {
|
|
t.Fatalf("expected snapshot fetch called once, got %d", repo.getSnapshotsCalls)
|
|
}
|
|
}
|
|
|
|
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 getExpenseDepreciationByQuery(t *testing.T, svc *repportService, query string) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, error) {
|
|
t.Helper()
|
|
|
|
app := fiber.New()
|
|
var (
|
|
rows []dto.ExpenseDepreciationRowDTO
|
|
meta *dto.ExpenseDepreciationMetaDTO
|
|
)
|
|
app.Get("/", func(c *fiber.Ctx) error {
|
|
resultRows, resultMeta, err := svc.GetExpenseDepreciation(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows = resultRows
|
|
meta = resultMeta
|
|
return c.SendStatus(fiber.StatusOK)
|
|
})
|
|
|
|
target := "/"
|
|
if query != "" {
|
|
target += "?" + query
|
|
}
|
|
resp, err := app.Test(httptest.NewRequest(http.MethodGet, target, nil))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if resp.StatusCode != fiber.StatusOK {
|
|
return nil, nil, fiber.NewError(resp.StatusCode, "request failed")
|
|
}
|
|
return rows, meta, nil
|
|
}
|
|
|
|
func depreciationBreakdown(
|
|
projectFlockKandangID uint,
|
|
kandangID uint,
|
|
kandangName string,
|
|
depreciationValue float64,
|
|
pulletCostDayN float64,
|
|
depreciationPercent float64,
|
|
) *approvalService.HppV2Breakdown {
|
|
return &approvalService.HppV2Breakdown{
|
|
ProjectFlockKandangID: projectFlockKandangID,
|
|
KandangID: kandangID,
|
|
KandangName: kandangName,
|
|
HouseType: "close_house",
|
|
Components: []approvalService.HppV2Component{
|
|
{
|
|
Code: "DEPRECIATION",
|
|
Title: "Depreciation",
|
|
Total: depreciationValue,
|
|
Parts: []approvalService.HppV2ComponentPart{
|
|
{
|
|
Code: "normal_transfer",
|
|
Total: depreciationValue,
|
|
Details: map[string]any{
|
|
"schedule_day": 1,
|
|
"depreciation_percent": depreciationPercent,
|
|
"pullet_cost_day_n": pulletCostDayN,
|
|
"source_project_flock_id": 77,
|
|
"origin_date": "2026-01-01",
|
|
},
|
|
References: []approvalService.HppV2Reference{
|
|
{
|
|
Type: "laying_transfer",
|
|
ID: 701,
|
|
Date: "2026-05-20",
|
|
Qty: 100,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|