mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
adjust repo hpp v2
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
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"
|
||||
@@ -21,12 +22,17 @@ import (
|
||||
|
||||
type expenseDepreciationRepoMock struct {
|
||||
repportRepo.ExpenseDepreciationRepository
|
||||
manualInputs []repportRepo.FarmDepreciationManualInputRow
|
||||
manualInputs []repportRepo.FarmDepreciationManualInputRow
|
||||
candidateRows []repportRepo.FarmDepreciationCandidateRow
|
||||
snapshots []entity.FarmDepreciationSnapshot
|
||||
|
||||
upsertedRow *entity.FarmDepreciationManualInput
|
||||
deleteCalled bool
|
||||
deleteDate time.Time
|
||||
deleteFarmIDs []uint
|
||||
upsertedRow *entity.FarmDepreciationManualInput
|
||||
deleteCalled bool
|
||||
deleteDate time.Time
|
||||
deleteFarmIDs []uint
|
||||
upsertSnapshotCalls int
|
||||
upsertedSnapshots []entity.FarmDepreciationSnapshot
|
||||
getSnapshotsCalls int
|
||||
}
|
||||
|
||||
func (m *expenseDepreciationRepoMock) DB() *gorm.DB {
|
||||
@@ -46,6 +52,37 @@ func (m *expenseDepreciationRepoMock) UpsertManualInput(_ context.Context, row *
|
||||
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
|
||||
@@ -57,6 +94,15 @@ func (m *expenseDepreciationRepoMock) GetLatestManualInputsByFarms(_ context.Con
|
||||
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
|
||||
@@ -352,6 +398,167 @@ func TestComputeExpenseDepreciationSnapshots_ZeroWhenDepreciationMissing(t *test
|
||||
}
|
||||
}
|
||||
|
||||
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{
|
||||
@@ -411,6 +618,82 @@ func TestUpsertExpenseDepreciationManualInput_InvalidatesSnapshotsFromCutoverDat
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -231,25 +231,9 @@ func (s *repportService) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDe
|
||||
farmNameByID[row.ProjectFlockID] = row.FarmName
|
||||
}
|
||||
|
||||
snapshots, err := s.ExpenseDepreciationRepo.GetSnapshotsByPeriodAndFarmIDs(ctx.Context(), periodDate, farmIDs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
snapshotByFarmID := make(map[uint]entity.FarmDepreciationSnapshot, len(snapshots))
|
||||
for _, row := range snapshots {
|
||||
snapshotByFarmID[row.ProjectFlockId] = row
|
||||
}
|
||||
|
||||
missingFarmIDs := make([]uint, 0)
|
||||
for _, farmID := range farmIDs {
|
||||
if _, exists := snapshotByFarmID[farmID]; exists {
|
||||
continue
|
||||
}
|
||||
missingFarmIDs = append(missingFarmIDs, farmID)
|
||||
}
|
||||
|
||||
if len(missingFarmIDs) > 0 {
|
||||
computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, missingFarmIDs, farmNameByID)
|
||||
snapshotByFarmID := make(map[uint]entity.FarmDepreciationSnapshot)
|
||||
if params.ForceRecompute {
|
||||
computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, farmIDs, farmNameByID)
|
||||
if computeErr != nil {
|
||||
return nil, nil, computeErr
|
||||
}
|
||||
@@ -257,10 +241,43 @@ func (s *repportService) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDe
|
||||
if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
snapshotByFarmID = make(map[uint]entity.FarmDepreciationSnapshot, len(computedSnapshots))
|
||||
for _, row := range computedSnapshots {
|
||||
snapshotByFarmID[row.ProjectFlockId] = row
|
||||
}
|
||||
}
|
||||
} else {
|
||||
snapshots, err := s.ExpenseDepreciationRepo.GetSnapshotsByPeriodAndFarmIDs(ctx.Context(), periodDate, farmIDs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
snapshotByFarmID = make(map[uint]entity.FarmDepreciationSnapshot, len(snapshots))
|
||||
for _, row := range snapshots {
|
||||
snapshotByFarmID[row.ProjectFlockId] = row
|
||||
}
|
||||
|
||||
missingFarmIDs := make([]uint, 0)
|
||||
for _, farmID := range farmIDs {
|
||||
if _, exists := snapshotByFarmID[farmID]; exists {
|
||||
continue
|
||||
}
|
||||
missingFarmIDs = append(missingFarmIDs, farmID)
|
||||
}
|
||||
|
||||
if len(missingFarmIDs) > 0 {
|
||||
computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, missingFarmIDs, farmNameByID)
|
||||
if computeErr != nil {
|
||||
return nil, nil, computeErr
|
||||
}
|
||||
if len(computedSnapshots) > 0 {
|
||||
if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, row := range computedSnapshots {
|
||||
snapshotByFarmID[row.ProjectFlockId] = row
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rows := make([]dto.ExpenseDepreciationRowDTO, 0, len(candidateRows))
|
||||
@@ -2717,6 +2734,7 @@ func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validat
|
||||
rawLocation := ctx.Query("location_id", "")
|
||||
rawProjectFlock := ctx.Query("project_flock_id", "")
|
||||
period := strings.TrimSpace(ctx.Query("period", ""))
|
||||
forceRecompute := ctx.QueryBool("force_recompute", false)
|
||||
|
||||
areaIDs, err := parseCommaSeparatedInt64s(rawArea)
|
||||
if err != nil {
|
||||
@@ -2766,6 +2784,7 @@ func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validat
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
Period: period,
|
||||
ForceRecompute: forceRecompute,
|
||||
ProjectFlockIDs: projectFlockIDs,
|
||||
AreaIDs: areaIDs,
|
||||
LocationIDs: locationIDs,
|
||||
|
||||
@@ -84,6 +84,7 @@ type ExpenseDepreciationQuery struct {
|
||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"`
|
||||
Period string `query:"period" validate:"required,datetime=2006-01-02"`
|
||||
ForceRecompute bool `query:"force_recompute"`
|
||||
ProjectFlockIDs []int64 `query:"-"`
|
||||
AreaIDs []int64 `query:"-"`
|
||||
LocationIDs []int64 `query:"-"`
|
||||
|
||||
Reference in New Issue
Block a user