package repositories import ( "testing" "time" ) // The terminal holds a unique index on barcode, so this rule decides whether a // catalogue import succeeds at all. Measured against live data when it was // written: 6,245 products, 93 distinct SKUs, and "1" used by 5,794 of them. func TestPosBarcodeFallsBackToProductIdWhenTheSkuIsNotScannable(t *testing.T) { cases := []struct { name string productID int sku string want string }{ {"the SKU almost every product shares", 844, "1", "844"}, {"blank SKU", 845, "", "845"}, {"whitespace only", 846, " ", "846"}, {"too short to be a barcode", 847, "1234567", "847"}, {"too long to be a barcode", 848, "123456789012345", "848"}, {"not digits", 849, "SKU-ABC-123", "849"}, {"digits with a space", 850, "1234 5678", "850"}, // Real scannable codes are used as-is, so the day the catalogue carries // them scanning starts working with no code change. {"EAN-8", 851, "12345678", "12345678"}, {"UPC-A", 852, "012345678905", "012345678905"}, {"EAN-13", 853, "8901030865278", "8901030865278"}, {"padded EAN-13", 854, " 8901030865278 ", "8901030865278"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := posBarcode(c.productID, c.sku); got != c.want { t.Errorf("posBarcode(%d, %q) = %q, want %q", c.productID, c.sku, got, c.want) } }) } } func TestPosBarcodesAreUniqueAcrossACatalogueOfSharedSkus(t *testing.T) { // The failure this exists to prevent: a whole catalogue collapsing onto one // barcode and the import being rejected by the terminal's unique index. seen := make(map[string]int) for id := 844; id < 844+500; id++ { barcode := posBarcode(id, "1") if first, clash := seen[barcode]; clash { t.Fatalf("products %d and %d both produced barcode %q", first, id, barcode) } seen[barcode] = id } } func TestRoundStockQtyNeverUnderDeducts(t *testing.T) { // productstocks.quantity is an integer column and a counter sells 1.5 kg of // onions. Rounding up keeps recorded stock at or below what is on the shelf; // truncating would let the shop oversell a little more with every sale. cases := []struct { quantity float64 want int }{ {1, 1}, {1.5, 2}, {0.25, 1}, {2.0, 2}, {2.01, 3}, {0, 1}, {-1, 1}, } for _, c := range cases { if got := roundStockQty(c.quantity); got != c.want { t.Errorf("roundStockQty(%g) = %d, want %d", c.quantity, got, c.want) } } } func TestLegacyOrderQtyIsUnchanged(t *testing.T) { // App orders have always truncated, and that behaviour is deliberately // preserved rather than corrected — changing it would silently alter stock // deduction for every order already flowing through createOrderTx. cases := []struct { quantity float64 want int }{ {1, 1}, {1.5, 1}, {0.5, 1}, {3.9, 3}, {0, 1}, } for _, c := range cases { if got := legacyOrderQty(c.quantity); got != c.want { t.Errorf("legacyOrderQty(%g) = %d, want %d", c.quantity, got, c.want) } } } // A catalogue revision is the terminal's memory of when it last pulled. If it // does not survive a round trip, every pull silently becomes a full snapshot — // or worse, a filtered result gets labelled as one and the shop's shelf empties. func TestPosRevisionRoundTrips(t *testing.T) { at := time.Date(2026, 8, 3, 12, 30, 45, 0, time.UTC) revision := posRevisionFor(1135, at) if revision != "loc1135-20260803T123045Z" { t.Fatalf("revision = %q, want loc1135-20260803T123045Z", revision) } got := posRevisionCutoff(1135, revision) if !got.Equal(at) { t.Errorf("cutoff = %v, want %v", got, at) } } func TestAnUnusableRevisionFallsBackToAFullSnapshot(t *testing.T) { // A zero cutoff means "send everything", and the caller turns that into // is_delta:false. Falling back the other way — answering an unreadable // revision with a change set — would leave a terminal permanently missing // every change it had not already seen, with nothing to show for it. cases := []struct { name string location int revision string }{ {"empty", 1135, ""}, {"whitespace", 1135, " "}, {"no prefix", 1135, "20260803T123045Z"}, {"malformed timestamp", 1135, "loc1135-not-a-time"}, {"truncated timestamp", 1135, "loc1135-20260803"}, {"another outlet's revision", 1135, "loc1097-20260803T123045Z"}, {"prefix collision", 113, "loc1135-20260803T123045Z"}, {"garbage", 1135, "../../etc/passwd"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := posRevisionCutoff(c.location, c.revision); !got.IsZero() { t.Errorf("cutoff = %v, want zero (full snapshot) for %q", got, c.revision) } }) } } func TestAnOutletCannotReplayAnotherOutletsRevision(t *testing.T) { // loc1135 and loc113 share a textual prefix. Matching loosely would let one // shop's cutoff silently scope another shop's delta. at := time.Date(2026, 8, 3, 12, 30, 45, 0, time.UTC) revision := posRevisionFor(1135, at) if got := posRevisionCutoff(1135, revision); got.IsZero() { t.Error("the issuing outlet could not read back its own revision") } for _, other := range []int{113, 11350, 1097, 1} { if got := posRevisionCutoff(other, revision); !got.IsZero() { t.Errorf("outlet %d accepted outlet 1135's revision (cutoff %v)", other, got) } } }