282 lines
12 KiB
Go
282 lines
12 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 = &Projection{Expected: 2_090_000, BookOnce: 2_000_000, StepMax: 69_828}
|
||
// ⚠ ON THE CUT, NOT ON THE PRICE — the store keeps the two apart on purpose, and the type makes
|
||
// that the only way to say it (see Projection).
|
||
in.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)
|
||
}
|
||
}
|
||
|
||
// ⛔ THE HONEST CHARACTER COUNT SURVIVES A PRICE THIS BUILD COULD NOT READ, and it did not until an
|
||
// audit asked. `source_chars` was written only when the whole projection passed its witness — so a
|
||
// manifest that named the text perfectly and failed on a MONEY key cost the screen its character
|
||
// count too, and «Знаков» fell back to the intake's approximation, which for an EPUB counts the runes
|
||
// of a ZIP archive. The two facts are independent: one is about the text, the other about money.
|
||
//
|
||
// ⚠ Since Projection was cut down to money alone, writing the count off the price is no longer
|
||
// EXPRESSIBLE — the field it would be read from does not exist. What this pin still catches is the
|
||
// other half of the same mistake: gating the write on the price's arrival.
|
||
//
|
||
// Mutation caught: writing a null `source_chars` whenever `in.Price` is nil.
|
||
func TestTheCharacterCountSurvivesAPriceThisBuildCouldNotRead(t *testing.T) {
|
||
s, ctx := testDB(t)
|
||
book := readingBook(t, s, ctx, "u1")
|
||
in := priced()
|
||
in.Price = nil // the money did not arrive…
|
||
in.SourceChars = 3000 // …and the engine's count of the text did (priced() sets it; said again here
|
||
// because this test is ABOUT that figure and a reader should not have to go look)
|
||
if err := s.SaveStructure(ctx, book, in); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, _, err := s.GetBook(ctx, "u1", book)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got.SourceChars == nil || *got.SourceChars != 3000 {
|
||
t.Fatalf("the engine's rune count was discarded with the price: %v", got.SourceChars)
|
||
}
|
||
// …and the book is still refused for SALE, which is the other half: the two facts are independent
|
||
// in both directions.
|
||
order, err := s.ReadBookForOrder(ctx, "u1", book)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if order.Priced {
|
||
t.Error("a book whose price did not arrive reads as priced")
|
||
}
|
||
}
|
||
|
||
// ⛔ WORK DELIVERED BEYOND THE ORDER'S BOUNDARY MUST NOT CANCEL WORK STILL OWED INSIDE IT. The
|
||
// allowance handed to the engine is «ordered minus delivered», and counting the delivered book-WIDE
|
||
// while counting the ordered only up to the boundary makes the two describe different sets: a run
|
||
// ordered through chapter 2 whose chapters 1 and 3 came back delivered fell to its floor of one unit
|
||
// and half-delivered what was bought. It costs no money — settlement is by fact — and it breaks «you
|
||
// bought N», which is the promise this pack exists to make true.
|
||
//
|
||
// Mutation caught: `deliveredWithinOrder` counting the book instead of the order.
|
||
func TestDeliveryBeyondTheOrderDoesNotCancelWorkOwedInsideIt(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)
|
||
}
|
||
// The order runs through chapter ONE (two units). Chapter TWO — beyond it — comes back delivered.
|
||
var c1 string
|
||
if err := s.pool.QueryRow(ctx,
|
||
`select id from chapters where book_id = $1 and number = 1`, book).Scan(&c1); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := s.pool.Exec(ctx, `
|
||
update books set ordered_at = now(), ordered_through_chapter_id = $2,
|
||
ordered_through_chapter_number = 1 where id = $1`, book, c1); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := s.pool.Exec(ctx, `
|
||
insert into unit_resolutions (book_id, chapter, unit, wave, shipped, flagged, at)
|
||
values ($1, 2, 0, 'edit', true, false, now())`, book); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var ordered, delivered int
|
||
var resolved bool
|
||
if err := s.pool.QueryRow(ctx, `
|
||
select `+orderedUnits+`, `+orderResolved+`, `+deliveredWithinOrder+`
|
||
from books b where b.id = $1`, book).Scan(&ordered, &resolved, &delivered); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !resolved || ordered != 2 {
|
||
t.Fatalf("the order through chapter one resolves to %d units (resolved %v), want 2", ordered, resolved)
|
||
}
|
||
if delivered != 0 {
|
||
t.Errorf("a chapter delivered BEYOND the order counted as %d units of it: the allowance would "+
|
||
"fall to its floor and half-deliver what was bought", delivered)
|
||
}
|
||
}
|