package services import ( "encoding/json" "testing" "nearle/repositories" ) /* What the app needs to turn a scan into an order. The scan screen is the start of a checkout: a customer photographs a packet and the next thing they do is add it to a basket. Every field an order line carries has to be on the option itself — reaching back up the response for the store, or splitting "500 g" to recover the unit, is the app doing work the server already did. */ func TestAScannedOptionCarriesEverythingAnOrderLineNeeds(t *testing.T) { row := repositories.StoreOptionRow{ Tenantid: 1147, Locationid: 1170, Productid: 7101, Productname: "Balaji Wafers Simply Salted 135g", Productimage: "https://example.test/wafers.jpg", Unitvalue: "135", Productunit: "g", Price: 35, Productcost: 29, Categoryid: 2, Subcategoryid: 21, Stock: 4, } opt := optionFromRow(row, nil) if opt.Tenantid != 1147 || opt.Locationid != 1170 { t.Errorf("store keys missing: tenant=%d location=%d", opt.Tenantid, opt.Locationid) } if opt.Locationid == 0 { t.Error("locationid is 0 — the app cannot place an order against no outlet") } if opt.Unitvalue != "135" || opt.Productunit != "g" { t.Errorf("unit parts missing: %q %q", opt.Unitvalue, opt.Productunit) } if opt.Size != "135 g" { // The printable label stays: the app has always shown it. t.Errorf("size label changed: %q", opt.Size) } if opt.Categoryid != 2 || opt.Subcategoryid != 21 { t.Errorf("category ids missing: %d %d", opt.Categoryid, opt.Subcategoryid) } if opt.Productimage != "https://example.test/wafers.jpg" || opt.Image != opt.Productimage { t.Errorf("image not under both names: %q %q", opt.Image, opt.Productimage) } } func TestPriceAndCostAreNotTheSameField(t *testing.T) { // The money one. `price` is what the customer pays; `productcost` is what // the shop paid. Billing against the cost would sell at cost, so they must // never be conflated. opt := optionFromRow(repositories.StoreOptionRow{ Productid: 1, Price: 35, Productcost: 29, }, nil) if opt.Price != 35 { t.Errorf("price = %v, want 35", opt.Price) } if opt.Productcost != 29 { t.Errorf("productcost = %v, want 29", opt.Productcost) } if opt.Price == opt.Productcost { t.Error("price and cost collapsed into one number") } } func TestTheWireNamesTheAppAskedFor(t *testing.T) { opt := optionFromRow(repositories.StoreOptionRow{ Tenantid: 1147, Locationid: 1170, Productid: 7101, Productname: "x", Productimage: "y", Unitvalue: "135", Productunit: "g", Price: 35, Productcost: 29, Categoryid: 2, Subcategoryid: 21, }, nil) encoded, err := json.Marshal(opt) if err != nil { t.Fatalf("marshal: %v", err) } var out map[string]any if err := json.Unmarshal(encoded, &out); err != nil { t.Fatalf("unmarshal: %v", err) } for _, key := range []string{ "productid", "tenantid", "locationid", "productcost", "price", "productname", "productimage", "unitvalue", "productunit", "categoryid", "subcategoryid", } { if _, ok := out[key]; !ok { t.Errorf("missing %q: %s", key, encoded) } } } func TestASizeRowKeepsItsOwnStoreAndPrice(t *testing.T) { // A variant hanging under a matched product is a separate sellable row with // its own price and its own outlet. It is what the customer actually taps // when they pick 1 kg instead of 500 g. opt := optionFromRow(repositories.StoreOptionRow{ Tenantid: 1147, Locationid: 1170, Productid: 7102, Parentid: 7101, Variantname: "1 kg", Unitvalue: "1", Productunit: "kg", Price: 240, Productcost: 200, }, nil) if !opt.IsVariant || opt.MatchedBy != "variant-of:7101" { t.Fatalf("not recognised as a size: %+v", opt) } if opt.Tenantid != 1147 || opt.Locationid != 1170 || opt.Price != 240 { t.Errorf("a size row lost its store or price: %+v", opt) } if opt.Unitvalue != "1" || opt.Productunit != "kg" { t.Errorf("a size row lost its unit: %q %q", opt.Unitvalue, opt.Productunit) } }