textmachine/platform/internal/pgstore/price_test.go

282 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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