package processor import ( "testing" "time" "github.com/stretchr/testify/assert" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/entities" ) func TestBudgetMetrics(t *testing.T) { october := &entities.GameBudget{ Scope: constants.GameBudgetScopeGlobal, Amount: 100_000_000, PeriodStart: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC), PeriodEnd: time.Date(2026, 10, 31, 0, 0, 0, 0, time.UTC), } today := time.Date(2026, 10, 21, 0, 0, 0, 0, time.UTC) // PRD §30: Rp100M, Rp60M realized, 10 days left, burning Rp5,5M a day → Rp115M. m := budgetMetrics(october, today, budgetMetricInputs{Realized: 60_000_000, RealizedWindow: 38_500_000, WindowDays: 7, RemainingDays: 10}) assert.EqualValues(t, 40_000_000, m.Remaining) assert.EqualValues(t, 60, m.UtilizationPercent) assert.EqualValues(t, 5_500_000, m.DailyBurn) assert.EqualValues(t, 115_000_000, m.ForecastCost) assert.EqualValues(t, -15_000_000, m.ForecastRemaining) assert.EqualValues(t, 115, m.ForecastUtilizationPercent) assert.Equal(t, constants.GameBudgetCritical, m.Status, "the forecast passes the budget") assert.EqualValues(t, 70, *m.Thresholds.Warning, "the default when the budget sets none") assert.EqualValues(t, 90, *m.Thresholds.Critical) assert.Equal(t, "2026-10-21", m.AsOf) for name, tc := range map[string]struct { thresholds string realized, burn int64 want string }{ "quiet": {"", 10_000_000, 0, constants.GameBudgetHealthy}, "forecast at warning": {"", 30_000_000, 4_000_000, constants.GameBudgetWarning}, // 30 + 4 × 10 = 70% "realized at critical": {"", 90_000_000, 0, constants.GameBudgetCritical}, // 90% "spent": {"", 100_000_000, 0, constants.GameBudgetExhausted}, // 100% "over": {"", 120_000_000, 0, constants.GameBudgetExhausted}, // 120% "own thresholds": {`{"warning": 20, "critical": 50}`, 25_000_000, 0, constants.GameBudgetWarning}, "own critical only": {`{"critical": 30}`, 31_000_000, 0, constants.GameBudgetCritical}, "own warning above default": {`{"warning": 80}`, 75_000_000, 0, constants.GameBudgetHealthy}, } { b := *october b.Thresholds = entities.JSONDocument(tc.thresholds) m := budgetMetrics(&b, today, budgetMetricInputs{Realized: tc.realized, RealizedWindow: tc.burn * 7, WindowDays: 7, RemainingDays: 10}) assert.Equal(t, tc.want, m.Status, name) } // Not started yet: nothing to average, the forecast is what is realized. m = budgetMetrics(october, time.Date(2026, 9, 20, 0, 0, 0, 0, time.UTC), budgetMetricInputs{}) assert.Zero(t, m.ForecastCost) assert.Equal(t, constants.GameBudgetHealthy, m.Status) // A third of a rupiah per day is not lost: 10 over 3 days, 3 days left → 10 more. m = budgetMetrics(october, today, budgetMetricInputs{Realized: 10, RealizedWindow: 10, WindowDays: 3, RemainingDays: 3}) assert.EqualValues(t, 20, m.ForecastCost) assert.EqualValues(t, 3, m.DailyBurn) }