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()) } }