122 lines
4.7 KiB
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())
|
|
}
|
|
}
|