196 lines
8 KiB
Go
196 lines
8 KiB
Go
package pgstore
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"textmachine/platform/internal/ingest"
|
||
"textmachine/platform/internal/money"
|
||
)
|
||
|
||
// priced is twoChapters with a projection on it: a $2.00 book-level bound (the shipped arm's flat
|
||
// figure), two chapters of unequal price, and one editor reservation.
|
||
func priced() Structure {
|
||
in := twoChapters("k1")
|
||
in.Structure = ingest.StructureDetected
|
||
in.Price = &ingest.BookPrice{
|
||
ExpectedUSD: 2_090_000, BookOnceUSD: 2_000_000, StepMaxUSD: 69_828, SourceChars: 3000,
|
||
}
|
||
// ⚠ The price lives on the UNIT and only there: a chapter's cost is the sum of the units still to
|
||
// deliver, so a chapter-level copy would be a second carrier free to drift from it.
|
||
in.Chapters[0].Units[0].Expected, in.Chapters[0].Units[0].SourceChars = 40_000, 1200
|
||
in.Chapters[0].Units[1].Expected, in.Chapters[0].Units[1].SourceChars = 20_000, 800
|
||
in.Chapters[1].Units[0].Expected, in.Chapters[1].Units[0].SourceChars = 30_000, 1000
|
||
return in
|
||
}
|
||
|
||
// The projection lands WITH the cut it was derived from, and it is read back as an order is priced:
|
||
// the book's three figures, and the chapters still to be delivered with their own bills.
|
||
//
|
||
// ⚠ The per-chapter figure is PRO-RATED by what is left, and the division truncates — so the error
|
||
// is downward, which is the ratified direction (D39.206: a hold that is short costs one top-up, a
|
||
// long one freezes credit the buyer cannot spend elsewhere).
|
||
func TestTheProjectionLandsWithTheCutAndPricesWhatIsLeft(t *testing.T) {
|
||
s, ctx := testDB(t)
|
||
book := readingBook(t, s, ctx, "u1")
|
||
if err := s.SaveStructure(ctx, book, priced()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, err := s.ReadBookForOrder(ctx, "u1", book)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !got.Priced {
|
||
t.Fatal("a book that carries a whole projection reads as unpriced")
|
||
}
|
||
if got.Expected != 2_090_000 || got.BookOnce != 2_000_000 || got.StepMax != 69_828 {
|
||
t.Errorf("the book's figures: %+v", got)
|
||
}
|
||
if got.Structure != ingest.StructureDetected {
|
||
t.Errorf("structure %q", got.Structure)
|
||
}
|
||
if len(got.Remaining) != 2 {
|
||
t.Fatalf("%d chapters remaining, want 2", len(got.Remaining))
|
||
}
|
||
if got.Remaining[0].Expected != 60_000 || got.Remaining[0].Units != 2 ||
|
||
got.Remaining[0].SourceChars != 2000 {
|
||
t.Errorf("chapter 1: %+v", got.Remaining[0])
|
||
}
|
||
if got.Remaining[0].ID == "" || got.Remaining[0].Number != 1 {
|
||
t.Errorf("chapter 1 carries no identity to anchor an order on: %+v", got.Remaining[0])
|
||
}
|
||
|
||
// ⛔ ONE OF CHAPTER ONE'S TWO UNITS COMES BACK DELIVERED — THE CHEAP ONE — and the chapter's
|
||
// remainder is then the price of the unit that is LEFT, not half the chapter's total.
|
||
//
|
||
// The two units are $0.04 and $0.02, so the two arithmetics differ by half a cent here and by far
|
||
// more on a real book: measured on a chapter holding a 100-rune unit at $0.001 beside a
|
||
// 9900-rune one at $0.099, dividing by the COUNT quotes $0.050 against a real remainder of
|
||
// $0.099 — a `covers_all` the engine then stops halfway through — and, with the expensive unit
|
||
// delivered instead, quotes $0.050 against a real $0.001 and refuses a purchase the buyer can
|
||
// afford. The engine prices per UNIT and this platform stores it; the count was never the
|
||
// question.
|
||
//
|
||
// Written as a RESOLUTION ROW and not as a counter, because the rows are what the query reads and
|
||
// the counters are derived from them: a fixture that moved only the counter would be describing a
|
||
// state the materializer cannot produce.
|
||
deliver := func(chapter, unit int) {
|
||
t.Helper()
|
||
if _, err := s.pool.Exec(ctx, `
|
||
insert into unit_resolutions (book_id, chapter, unit, wave, shipped, flagged, at)
|
||
values ($1, $2, $3, 'edit', true, false, now())`, book, chapter, unit); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
deliver(1, 0) // the $0.04 unit of chapter one
|
||
got, err = s.ReadBookForOrder(ctx, "u1", book)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(got.Remaining) != 2 || got.Remaining[0].Units != 1 {
|
||
t.Fatalf("a half-delivered chapter: %+v", got.Remaining)
|
||
}
|
||
if got.Remaining[0].Expected != 20_000 {
|
||
t.Errorf("the remainder of chapter one is priced at %s, want the %s of the unit that is left "+
|
||
"(half the chapter would be %s, and that is the arithmetic this test exists to refuse)",
|
||
got.Remaining[0].Expected.USD(), money.MicroUSD(20_000).USD(), money.MicroUSD(30_000).USD())
|
||
}
|
||
// The TEXT that is left follows the same rule: what the screen says is «still to translate», not
|
||
// the whole chapter.
|
||
if got.Remaining[0].SourceChars != 800 {
|
||
t.Errorf("the remainder of chapter one is %d runes, want the 800 of the unit that is left",
|
||
got.Remaining[0].SourceChars)
|
||
}
|
||
|
||
// A chapter fully delivered leaves the order entirely: it is not for sale twice.
|
||
deliver(1, 4)
|
||
got, err = s.ReadBookForOrder(ctx, "u1", book)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(got.Remaining) != 1 || got.Remaining[0].Number != 2 {
|
||
t.Errorf("a delivered chapter is still on offer: %+v", got.Remaining)
|
||
}
|
||
}
|
||
|
||
// A cut with NO projection lands as not priced, and nothing is invented in its place. The book is
|
||
// still a book — its tree, its text and its reading surface are unaffected — it simply cannot be
|
||
// SOLD until the engine has priced it.
|
||
func TestACutWithoutAProjectionLandsAsUnpricedAndNotAsFree(t *testing.T) {
|
||
s, ctx := testDB(t)
|
||
book := readingBook(t, s, ctx, "u1")
|
||
if err := s.SaveStructure(ctx, book, twoChapters("k1")); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, err := s.ReadBookForOrder(ctx, "u1", book)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got.Priced {
|
||
t.Fatalf("a cut with no projection reads as priced: %+v", got)
|
||
}
|
||
if got.Expected != 0 || got.StepMax != 0 {
|
||
t.Errorf("figures were invented: %+v", got)
|
||
}
|
||
// The tree landed all the same.
|
||
page, err := s.ListChapters(ctx, "u1", book, 0, "")
|
||
if err != nil || len(page.Chapters) != 2 {
|
||
t.Fatalf("the tree did not land beside the missing price: %+v (%v)", page, err)
|
||
}
|
||
}
|
||
|
||
// ⛔ THE PROJECTION FOLLOWS THE CUT AND NEVER OUTLIVES IT. A price written a moment apart from the
|
||
// tree is a price for a cut that may already be gone, so a re-cut that this build could not price
|
||
// must leave NOTHING behind rather than yesterday's figures beside today's chapters: the stale pair
|
||
// would sell a book that no longer exists, at a price nobody quoted.
|
||
func TestARecutWithoutAPriceDoesNotLeaveTheOldOneStanding(t *testing.T) {
|
||
s, ctx := testDB(t)
|
||
book := readingBook(t, s, ctx, "u1")
|
||
if err := s.SaveStructure(ctx, book, priced()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
recut := twoChapters("k2") // a different cut, and this time the engine could not price it
|
||
if err := s.SaveStructure(ctx, book, recut); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, err := s.ReadBookForOrder(ctx, "u1", book)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got.Priced {
|
||
t.Fatalf("the previous cut's price survived a re-cut: %+v", got)
|
||
}
|
||
for _, c := range got.Remaining {
|
||
if c.Expected != 0 {
|
||
t.Errorf("chapter %d kept the previous cut's bill %s", c.Number, c.Expected.USD())
|
||
}
|
||
}
|
||
}
|
||
|
||
// The honest character count reaches the library row, and the flag beside it says which number it
|
||
// is. ⚠ `null` keeps its own meaning — «the book is still arriving» — because re-using it for «we do
|
||
// not trust this number» would destroy a meaning the contract already promises (ratified D39.201
|
||
// §5б: a flag BESIDE the number, never a changed meaning for null).
|
||
func TestTheEnginesCharacterCountReachesTheLibraryRowBesideItsAccuracy(t *testing.T) {
|
||
s, ctx := testDB(t)
|
||
book := readingBook(t, s, ctx, "u1")
|
||
before, _, err := s.GetBook(ctx, "u1", book)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if before.SourceChars != nil {
|
||
t.Fatalf("a book with no manifest read carries an engine count: %v", *before.SourceChars)
|
||
}
|
||
if err := s.SaveStructure(ctx, book, priced()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
after, _, err := s.GetBook(ctx, "u1", book)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if after.SourceChars == nil || *after.SourceChars != 3000 {
|
||
t.Fatalf("the engine's rune count did not land: %v", after.SourceChars)
|
||
}
|
||
if after.Structure != ingest.StructureDetected {
|
||
t.Errorf("the cut's provenance did not land: %q", after.Structure)
|
||
}
|
||
}
|