62 lines
2.9 KiB
Go
62 lines
2.9 KiB
Go
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)
|
|||
|
|
}
|