mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
adjust repo hpp v2
This commit is contained in:
@@ -266,6 +266,26 @@ func (r *HppV2RepositoryImpl) GetRecordingStockRoutingAdjustmentCostByProjectFlo
|
|||||||
utils.FlagVitamin,
|
utils.FlagVitamin,
|
||||||
utils.FlagKimia,
|
utils.FlagKimia,
|
||||||
}
|
}
|
||||||
|
transferExistsCondition := `
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM laying_transfer_targets AS ltt
|
||||||
|
JOIN laying_transfers AS lt ON lt.id = ltt.laying_transfer_id
|
||||||
|
WHERE ltt.deleted_at IS NULL
|
||||||
|
AND lt.deleted_at IS NULL
|
||||||
|
AND lt.executed_at IS NOT NULL
|
||||||
|
AND ltt.target_project_flock_kandang_id = r.project_flock_kandangs_id
|
||||||
|
AND COALESCE(DATE(lt.effective_move_date), DATE(lt.economic_cutoff_date), DATE(lt.transfer_date)) <= DATE(?)
|
||||||
|
AND (
|
||||||
|
SELECT a.action
|
||||||
|
FROM approvals a
|
||||||
|
WHERE a.approvable_type = ?
|
||||||
|
AND a.approvable_id = lt.id
|
||||||
|
ORDER BY a.id DESC
|
||||||
|
LIMIT 1
|
||||||
|
) = ?
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
var total float64
|
var total float64
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
@@ -284,7 +304,19 @@ func (r *HppV2RepositoryImpl) GetRecordingStockRoutingAdjustmentCostByProjectFlo
|
|||||||
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
||||||
Where("pfk_rec.project_flock_id = ?", projectFlockID).
|
Where("pfk_rec.project_flock_id = ?", projectFlockID).
|
||||||
Where("DATE(r.record_datetime) <= DATE(?)", periodDate).
|
Where("DATE(r.record_datetime) <= DATE(?)", periodDate).
|
||||||
Where("(rs.project_flock_kandang_id IS NULL OR rs.project_flock_kandang_id <> r.project_flock_kandangs_id)").
|
Where(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"((%s) AND rs.project_flock_kandang_id IS NOT NULL AND rs.project_flock_kandang_id <> r.project_flock_kandangs_id) OR (NOT (%s) AND rs.project_flock_kandang_id IS NULL)",
|
||||||
|
transferExistsCondition,
|
||||||
|
transferExistsCondition,
|
||||||
|
),
|
||||||
|
periodDate,
|
||||||
|
string(utils.ApprovalWorkflowTransferToLaying),
|
||||||
|
entity.ApprovalActionApproved,
|
||||||
|
periodDate,
|
||||||
|
string(utils.ApprovalWorkflowTransferToLaying),
|
||||||
|
entity.ApprovalActionApproved,
|
||||||
|
).
|
||||||
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags).
|
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags).
|
||||||
Scan(&total).Error
|
Scan(&total).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -98,33 +99,95 @@ func TestHppV2RepositoryGetEggTerjualProratesHistoricalFarmSalesFromAdjustments(
|
|||||||
|
|
||||||
func TestHppV2RepositoryGetRecordingStockRoutingAdjustmentCostByProjectFlockID(t *testing.T) {
|
func TestHppV2RepositoryGetRecordingStockRoutingAdjustmentCostByProjectFlockID(t *testing.T) {
|
||||||
db := setupHppV2RepositoryTestDB(t)
|
db := setupHppV2RepositoryTestDB(t)
|
||||||
|
approvalType := utils.ApprovalWorkflowTransferToLaying.String()
|
||||||
|
|
||||||
mustExecHppV2(t, db,
|
mustExecHppV2(t, db,
|
||||||
`INSERT INTO project_flock_kandangs (id, kandang_id, project_flock_id) VALUES (101, 1, 1), (201, 2, 2)`,
|
`INSERT INTO project_flock_kandangs (id, kandang_id, project_flock_id) VALUES
|
||||||
|
(101, 1, 1),
|
||||||
|
(102, 2, 1),
|
||||||
|
(103, 3, 1),
|
||||||
|
(104, 4, 1),
|
||||||
|
(105, 5, 1),
|
||||||
|
(201, 6, 2)`,
|
||||||
`INSERT INTO recordings (id, project_flock_kandangs_id, record_datetime, deleted_at) VALUES
|
`INSERT INTO recordings (id, project_flock_kandangs_id, record_datetime, deleted_at) VALUES
|
||||||
(1, 101, '2026-04-10 08:00:00', NULL),
|
(1, 101, '2026-04-10 08:00:00', NULL),
|
||||||
(2, 101, '2026-04-11 08:00:00', NULL),
|
(2, 101, '2026-04-10 08:05:00', NULL),
|
||||||
(3, 101, '2026-04-12 08:00:00', NULL)`,
|
(3, 101, '2026-04-10 08:10:00', NULL),
|
||||||
|
(4, 102, '2026-04-10 08:15:00', NULL),
|
||||||
|
(5, 102, '2026-04-10 08:20:00', NULL),
|
||||||
|
(6, 103, '2026-04-12 08:00:00', NULL),
|
||||||
|
(7, 103, '2026-04-12 08:05:00', NULL),
|
||||||
|
(8, 104, '2026-04-12 08:10:00', NULL),
|
||||||
|
(9, 104, '2026-04-12 08:15:00', NULL),
|
||||||
|
(10, 105, '2026-04-12 08:20:00', NULL),
|
||||||
|
(11, 105, '2026-04-12 08:25:00', NULL)`,
|
||||||
`INSERT INTO product_warehouses (id, warehouse_id, product_id, project_flock_kandang_id) VALUES
|
`INSERT INTO product_warehouses (id, warehouse_id, product_id, project_flock_kandang_id) VALUES
|
||||||
(501, 201, 10, NULL),
|
(501, 201, 10, NULL),
|
||||||
(502, 201, 11, NULL),
|
(502, 201, 10, NULL),
|
||||||
(503, 201, 12, NULL)`,
|
(503, 201, 10, NULL),
|
||||||
|
(504, 201, 10, NULL),
|
||||||
|
(505, 201, 10, NULL),
|
||||||
|
(506, 201, 10, NULL),
|
||||||
|
(507, 201, 10, NULL),
|
||||||
|
(508, 201, 10, NULL),
|
||||||
|
(509, 201, 10, NULL),
|
||||||
|
(510, 201, 10, NULL),
|
||||||
|
(511, 201, 10, NULL)`,
|
||||||
`INSERT INTO flags (id, flagable_type, flagable_id, name) VALUES
|
`INSERT INTO flags (id, flagable_type, flagable_id, name) VALUES
|
||||||
(10, 'products', 10, 'PAKAN'),
|
(10, 'products', 10, 'PAKAN')`,
|
||||||
(11, 'products', 11, 'OVK'),
|
|
||||||
(12, 'products', 12, 'PAKAN')`,
|
|
||||||
`INSERT INTO recording_stocks (id, recording_id, product_warehouse_id, project_flock_kandang_id) VALUES
|
`INSERT INTO recording_stocks (id, recording_id, product_warehouse_id, project_flock_kandang_id) VALUES
|
||||||
(101, 1, 501, NULL),
|
(101, 1, 501, NULL),
|
||||||
(102, 2, 502, 201),
|
(102, 2, 502, 201),
|
||||||
(103, 3, 503, 101)`,
|
(103, 3, 503, 101),
|
||||||
|
(104, 4, 504, NULL),
|
||||||
|
(105, 5, 505, 201),
|
||||||
|
(106, 6, 506, NULL),
|
||||||
|
(107, 7, 507, 201),
|
||||||
|
(108, 8, 508, NULL),
|
||||||
|
(109, 9, 509, 201),
|
||||||
|
(110, 10, 510, NULL),
|
||||||
|
(111, 11, 511, 201)`,
|
||||||
`INSERT INTO purchase_items (id, product_id, price) VALUES
|
`INSERT INTO purchase_items (id, product_id, price) VALUES
|
||||||
(601, 10, 100),
|
(601, 10, 100),
|
||||||
(602, 11, 200),
|
(602, 10, 110),
|
||||||
(603, 12, 300)`,
|
(603, 10, 120),
|
||||||
|
(604, 10, 130),
|
||||||
|
(605, 10, 140),
|
||||||
|
(606, 10, 150),
|
||||||
|
(607, 10, 160),
|
||||||
|
(608, 10, 170),
|
||||||
|
(609, 10, 180),
|
||||||
|
(610, 10, 190),
|
||||||
|
(611, 10, 200)`,
|
||||||
`INSERT INTO stock_allocations (id, usable_type, usable_id, stockable_type, stockable_id, status, allocation_purpose, qty) VALUES
|
`INSERT INTO stock_allocations (id, usable_type, usable_id, stockable_type, stockable_id, status, allocation_purpose, qty) VALUES
|
||||||
(9001, 'RECORDING_STOCK', 101, 'PURCHASE_ITEMS', 601, 'ACTIVE', 'CONSUME', 2),
|
(9001, 'RECORDING_STOCK', 101, 'PURCHASE_ITEMS', 601, 'ACTIVE', 'CONSUME', 2),
|
||||||
(9002, 'RECORDING_STOCK', 102, 'PURCHASE_ITEMS', 602, 'ACTIVE', 'CONSUME', 1.5),
|
(9002, 'RECORDING_STOCK', 102, 'PURCHASE_ITEMS', 602, 'ACTIVE', 'CONSUME', 1),
|
||||||
(9003, 'RECORDING_STOCK', 103, 'PURCHASE_ITEMS', 603, 'ACTIVE', 'CONSUME', 1)`,
|
(9003, 'RECORDING_STOCK', 103, 'PURCHASE_ITEMS', 603, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9004, 'RECORDING_STOCK', 104, 'PURCHASE_ITEMS', 604, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9005, 'RECORDING_STOCK', 105, 'PURCHASE_ITEMS', 605, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9006, 'RECORDING_STOCK', 106, 'PURCHASE_ITEMS', 606, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9007, 'RECORDING_STOCK', 107, 'PURCHASE_ITEMS', 607, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9008, 'RECORDING_STOCK', 108, 'PURCHASE_ITEMS', 608, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9009, 'RECORDING_STOCK', 109, 'PURCHASE_ITEMS', 609, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9010, 'RECORDING_STOCK', 110, 'PURCHASE_ITEMS', 610, 'ACTIVE', 'CONSUME', 1),
|
||||||
|
(9011, 'RECORDING_STOCK', 111, 'PURCHASE_ITEMS', 611, 'ACTIVE', 'CONSUME', 1)`,
|
||||||
|
`INSERT INTO laying_transfers (id, transfer_date, effective_move_date, economic_cutoff_date, executed_at, deleted_at) VALUES
|
||||||
|
(1001, '2026-04-04', '2026-04-05', NULL, '2026-04-05 00:00:00', NULL),
|
||||||
|
(1002, '2026-05-01', '2026-05-01', NULL, '2026-05-01 00:00:00', NULL),
|
||||||
|
(1003, '2026-04-03', '2026-04-05', NULL, '2026-04-05 00:00:00', NULL),
|
||||||
|
(1004, '2026-04-03', '2026-04-05', NULL, NULL, NULL)`,
|
||||||
|
`INSERT INTO laying_transfer_targets (id, laying_transfer_id, target_project_flock_kandang_id, deleted_at) VALUES
|
||||||
|
(2001, 1001, 101, NULL),
|
||||||
|
(2002, 1002, 103, NULL),
|
||||||
|
(2003, 1003, 104, NULL),
|
||||||
|
(2004, 1004, 105, NULL)`,
|
||||||
|
fmt.Sprintf(`INSERT INTO approvals (id, approvable_type, approvable_id, action) VALUES
|
||||||
|
(3001, '%s', 1001, 'APPROVED'),
|
||||||
|
(3002, '%s', 1002, 'APPROVED'),
|
||||||
|
(3003, '%s', 1003, 'APPROVED'),
|
||||||
|
(3004, '%s', 1003, 'REJECTED'),
|
||||||
|
(3005, '%s', 1004, 'APPROVED')`,
|
||||||
|
approvalType, approvalType, approvalType, approvalType, approvalType),
|
||||||
)
|
)
|
||||||
|
|
||||||
repo := &HppV2RepositoryImpl{db: db}
|
repo := &HppV2RepositoryImpl{db: db}
|
||||||
@@ -134,14 +197,14 @@ func TestHppV2RepositoryGetRecordingStockRoutingAdjustmentCostByProjectFlockID(t
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error, got %v", err)
|
t.Fatalf("expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
assertFloatEquals(t, total, 500)
|
assertFloatEquals(t, total, 750)
|
||||||
|
|
||||||
earlyPeriod := mustJakartaTime(t, "2026-04-10 23:59:59")
|
earlyPeriod := mustJakartaTime(t, "2026-04-10 23:59:59")
|
||||||
earlyTotal, err := repo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID(context.Background(), 1, earlyPeriod)
|
earlyTotal, err := repo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID(context.Background(), 1, earlyPeriod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error, got %v", err)
|
t.Fatalf("expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
assertFloatEquals(t, earlyTotal, 200)
|
assertFloatEquals(t, earlyTotal, 240)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB {
|
func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB {
|
||||||
@@ -246,6 +309,26 @@ func setupHppV2RepositoryTestDB(t *testing.T) *gorm.DB {
|
|||||||
flagable_id INTEGER NULL,
|
flagable_id INTEGER NULL,
|
||||||
name TEXT NULL
|
name TEXT NULL
|
||||||
)`,
|
)`,
|
||||||
|
`CREATE TABLE laying_transfers (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
transfer_date DATETIME NULL,
|
||||||
|
effective_move_date DATETIME NULL,
|
||||||
|
economic_cutoff_date DATETIME NULL,
|
||||||
|
executed_at DATETIME NULL,
|
||||||
|
deleted_at DATETIME NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE laying_transfer_targets (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
laying_transfer_id INTEGER NULL,
|
||||||
|
target_project_flock_kandang_id INTEGER NULL,
|
||||||
|
deleted_at DATETIME NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE approvals (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
approvable_type TEXT NULL,
|
||||||
|
approvable_id INTEGER NULL,
|
||||||
|
action TEXT NULL
|
||||||
|
)`,
|
||||||
)
|
)
|
||||||
|
|
||||||
return db
|
return db
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
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"
|
dto "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
||||||
repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
|
repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
||||||
@@ -21,12 +22,17 @@ import (
|
|||||||
|
|
||||||
type expenseDepreciationRepoMock struct {
|
type expenseDepreciationRepoMock struct {
|
||||||
repportRepo.ExpenseDepreciationRepository
|
repportRepo.ExpenseDepreciationRepository
|
||||||
manualInputs []repportRepo.FarmDepreciationManualInputRow
|
manualInputs []repportRepo.FarmDepreciationManualInputRow
|
||||||
|
candidateRows []repportRepo.FarmDepreciationCandidateRow
|
||||||
|
snapshots []entity.FarmDepreciationSnapshot
|
||||||
|
|
||||||
upsertedRow *entity.FarmDepreciationManualInput
|
upsertedRow *entity.FarmDepreciationManualInput
|
||||||
deleteCalled bool
|
deleteCalled bool
|
||||||
deleteDate time.Time
|
deleteDate time.Time
|
||||||
deleteFarmIDs []uint
|
deleteFarmIDs []uint
|
||||||
|
upsertSnapshotCalls int
|
||||||
|
upsertedSnapshots []entity.FarmDepreciationSnapshot
|
||||||
|
getSnapshotsCalls int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *expenseDepreciationRepoMock) DB() *gorm.DB {
|
func (m *expenseDepreciationRepoMock) DB() *gorm.DB {
|
||||||
@@ -46,6 +52,37 @@ func (m *expenseDepreciationRepoMock) UpsertManualInput(_ context.Context, row *
|
|||||||
return nil
|
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 {
|
func (m *expenseDepreciationRepoMock) DeleteSnapshotsFromDate(_ context.Context, fromDate time.Time, farmIDs []uint) error {
|
||||||
m.deleteCalled = true
|
m.deleteCalled = true
|
||||||
m.deleteDate = fromDate
|
m.deleteDate = fromDate
|
||||||
@@ -57,6 +94,15 @@ func (m *expenseDepreciationRepoMock) GetLatestManualInputsByFarms(_ context.Con
|
|||||||
return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil
|
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 {
|
type hppCostRepoMock struct {
|
||||||
commonRepo.HppCostRepository
|
commonRepo.HppCostRepository
|
||||||
kandangIDsByFarm map[uint][]uint
|
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) {
|
func TestUpsertExpenseDepreciationManualInput_InvalidatesSnapshotsFromCutoverDate(t *testing.T) {
|
||||||
repo := &expenseDepreciationRepoMock{
|
repo := &expenseDepreciationRepoMock{
|
||||||
manualInputs: []repportRepo.FarmDepreciationManualInputRow{
|
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 {
|
func decodeDepreciationComponents(t *testing.T, raw []byte) depreciationFarmComponents {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var out depreciationFarmComponents
|
var out depreciationFarmComponents
|
||||||
|
|||||||
@@ -231,25 +231,9 @@ func (s *repportService) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDe
|
|||||||
farmNameByID[row.ProjectFlockID] = row.FarmName
|
farmNameByID[row.ProjectFlockID] = row.FarmName
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshots, err := s.ExpenseDepreciationRepo.GetSnapshotsByPeriodAndFarmIDs(ctx.Context(), periodDate, farmIDs)
|
snapshotByFarmID := make(map[uint]entity.FarmDepreciationSnapshot)
|
||||||
if err != nil {
|
if params.ForceRecompute {
|
||||||
return nil, nil, err
|
computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, farmIDs, farmNameByID)
|
||||||
}
|
|
||||||
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 {
|
if computeErr != nil {
|
||||||
return nil, nil, computeErr
|
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 {
|
if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
snapshotByFarmID = make(map[uint]entity.FarmDepreciationSnapshot, len(computedSnapshots))
|
||||||
for _, row := range computedSnapshots {
|
for _, row := range computedSnapshots {
|
||||||
snapshotByFarmID[row.ProjectFlockId] = row
|
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))
|
rows := make([]dto.ExpenseDepreciationRowDTO, 0, len(candidateRows))
|
||||||
@@ -2717,6 +2734,7 @@ func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validat
|
|||||||
rawLocation := ctx.Query("location_id", "")
|
rawLocation := ctx.Query("location_id", "")
|
||||||
rawProjectFlock := ctx.Query("project_flock_id", "")
|
rawProjectFlock := ctx.Query("project_flock_id", "")
|
||||||
period := strings.TrimSpace(ctx.Query("period", ""))
|
period := strings.TrimSpace(ctx.Query("period", ""))
|
||||||
|
forceRecompute := ctx.QueryBool("force_recompute", false)
|
||||||
|
|
||||||
areaIDs, err := parseCommaSeparatedInt64s(rawArea)
|
areaIDs, err := parseCommaSeparatedInt64s(rawArea)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2766,6 +2784,7 @@ func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validat
|
|||||||
Page: page,
|
Page: page,
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
Period: period,
|
Period: period,
|
||||||
|
ForceRecompute: forceRecompute,
|
||||||
ProjectFlockIDs: projectFlockIDs,
|
ProjectFlockIDs: projectFlockIDs,
|
||||||
AreaIDs: areaIDs,
|
AreaIDs: areaIDs,
|
||||||
LocationIDs: locationIDs,
|
LocationIDs: locationIDs,
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ type ExpenseDepreciationQuery struct {
|
|||||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"`
|
||||||
Period string `query:"period" validate:"required,datetime=2006-01-02"`
|
Period string `query:"period" validate:"required,datetime=2006-01-02"`
|
||||||
|
ForceRecompute bool `query:"force_recompute"`
|
||||||
ProjectFlockIDs []int64 `query:"-"`
|
ProjectFlockIDs []int64 `query:"-"`
|
||||||
AreaIDs []int64 `query:"-"`
|
AreaIDs []int64 `query:"-"`
|
||||||
LocationIDs []int64 `query:"-"`
|
LocationIDs []int64 `query:"-"`
|
||||||
|
|||||||
Reference in New Issue
Block a user