textmachine/platform/internal/pricing/pricing_test.go

122 lines
4.7 KiB
Go

package pricing
import (
"testing"
"textmachine/platform/internal/money"
)
func model(t *testing.T) Model {
t.Helper()
m, err := New(DefaultPerChapter)
if err != nil {
t.Fatal(err)
}
return m
}
// A zero rate makes every ceiling free and every scale infinite, so it is refused at construction
// rather than defaulted quietly.
func TestARateMustBePositive(t *testing.T) {
for _, v := range []money.MicroUSD{0, -1} {
if _, err := New(v); err == nil {
t.Errorf("a rate of %d was accepted", v)
}
}
}
// The maximum is the balance AS IT IS. ⚠ This is the arithmetic D39.115 §2a corrected: a hold is a
// debit at the moment it is taken, so the balance already excludes the holds open against it, and
// subtracting a Reserved figure again halves the scale. The test states the whole sum so the mistake
// cannot come back looking reasonable.
func TestTheScaleUsesTheBalanceAsItIsAndNeverSubtractsHoldsTwice(t *testing.T) {
m := model(t) // $0.03 a chapter
// $10 granted, $1 held: the balance is $9 and Reserved is $1. The scale is computed from $9.
got := m.Scale(money.MicroUSD(9_000_000), 1000)
if want := 9_000_000 / int(DefaultPerChapter); got.Max != want {
t.Fatalf("max %d chapters, want %d — the balance is $9, not $8", got.Max, want)
}
if got.Min != 1 {
t.Errorf("min %d: zero is never selectable and one chapter explains itself", got.Min)
}
}
// Clamped by BOTH the money and what is left of the book, and the client is forbidden to clamp
// again — so whichever is smaller has to be applied here.
func TestTheScaleIsClampedByTheBookAsWellAsTheMoney(t *testing.T) {
m := model(t)
rich := m.Scale(money.MicroUSD(100_000_000), 12)
if rich.Max != 12 {
t.Errorf("a rich account on a short book: max %d, want the 12 chapters that are left", rich.Max)
}
poor := m.Scale(money.MicroUSD(90_000), 1000)
if poor.Max != 3 {
t.Errorf("$0.09 at $0.03 a chapter: max %d, want 3", poor.Max)
}
}
// Zero is legal and means "no run can start at all": the client shows the exhausted state instead of
// a scale, and default is zero ONLY here.
func TestAnExhaustedAccountGetsAZeroMaximumAndNoDefault(t *testing.T) {
m := model(t)
for _, tc := range []struct{ balance, left int }{{0, 100}, {10_000_000, 0}, {1000, 100}} {
got := m.Scale(money.MicroUSD(tc.balance), tc.left)
if got.Max != 0 || got.Default != 0 {
t.Errorf("balance %d with %d chapters left: %+v, want max and default of zero", tc.balance, tc.left, got)
}
if got.Min != 1 {
t.Errorf("min %d: the minimum is a property of the product, not of the balance", got.Min)
}
}
}
// Negative inputs are the caller's mistake and must not produce a negative scale a client would
// render as a slider running backwards.
func TestNonsenseInputsCannotProduceANegativeScale(t *testing.T) {
m := model(t)
got := m.Scale(money.MicroUSD(-5_000_000), -3)
if got.Max != 0 || got.Default != 0 {
t.Errorf("negative inputs gave %+v", got)
}
}
// The ceiling in money is what is HELD and what the engine is told to stop at — the same number by
// construction, which is what makes an overspend impossible while the platform is blind.
func TestTheCeilingInMoneyIsChaptersTimesTheRate(t *testing.T) {
m := model(t)
if got := m.Ceiling(100); got != DefaultPerChapter*100 {
t.Errorf("100 chapters = %s", got.USD())
}
if got := m.Ceiling(0); got != 0 {
t.Errorf("zero chapters = %s", got.USD())
}
if got := m.Ceiling(-5); got != 0 {
t.Errorf("a negative ceiling = %s", got.USD())
}
}
// The default sits at the top of the scale, and when the balance covers the book the top IS the
// book. Pinned because it is a PRODUCT policy the platform owns (D39.115 §4), not an accident of
// the arithmetic — if it changes, it changes deliberately.
func TestThePreselectedValueIsTheTopOfTheScale(t *testing.T) {
m := model(t)
whole := m.Scale(money.MicroUSD(100_000_000), 50)
if whole.Default != 50 || whole.Max != 50 {
t.Errorf("a book that fits: %+v, want the whole book pre-selected", whole)
}
partial := m.Scale(money.MicroUSD(300_000), 1000)
if partial.Default != partial.Max {
t.Errorf("a book that does not fit: default %d, max %d", partial.Default, partial.Max)
}
}
// The rate itself: an estimate, and one whose provenance has to survive somebody asking where it
// came from. exp08 v2 measures the standard mix at $10.94 for 500 chapters ($0.0219 each) and its
// own revision banner raises that by 15-25% ($0.0252-$0.0274); the constant is rounded UP from there.
func TestTheDefaultRateStaysInTheBandItsProvenanceGives(t *testing.T) {
const lo, hi = money.MicroUSD(25_200), money.MicroUSD(40_000)
if DefaultPerChapter < lo || DefaultPerChapter > hi {
t.Errorf("the per-chapter rate is %s, outside the band its cost model justifies (%s..%s)",
DefaultPerChapter.USD(), lo.USD(), hi.USD())
}
}