pricing
This commit is contained in:
@@ -402,6 +402,10 @@ func (ctl *ProductController) GetAllProducts(c *fiber.Ctx) error {
|
|||||||
if err == nil && isAppRequest(c) {
|
if err == nil && isAppRequest(c) {
|
||||||
details = services.InStockOnlyGrouped(details)
|
details = services.InStockOnlyGrouped(details)
|
||||||
|
|
||||||
|
// And nothing a shopper cannot be charged for. An unpriced product is
|
||||||
|
// unfinished, not free, and ₹0 on a phone reads as the latter.
|
||||||
|
details = services.PricedOnly(details)
|
||||||
|
|
||||||
// One product, three sizes — not three products.
|
// One product, three sizes — not three products.
|
||||||
//
|
//
|
||||||
// A shop stocking Aachi Baby Fryums in 100g, 500g and 1kg has three
|
// A shop stocking Aachi Baby Fryums in 100g, 500g and 1kg has three
|
||||||
|
|||||||
@@ -117,3 +117,43 @@ func WithoutVariantChildren(groups []models.Tenantproducts, isChild map[int]bool
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PricedOnly drops the products a shopper cannot be charged for.
|
||||||
|
//
|
||||||
|
// The companion to InStockOnlyGrouped, and the same argument: being in a shop's
|
||||||
|
// catalogue is not the same as being on sale. A product with no price is not
|
||||||
|
// free, it is unfinished — but ₹0 on a phone reads as free, and a shopper who
|
||||||
|
// taps it either expects to pay nothing or decides the shop is broken.
|
||||||
|
//
|
||||||
|
// ── Where the ₹0 products come from ─────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Import used to write no price at all, and 159 of the 1,572 catalogue rows
|
||||||
|
// still carry no price for it to copy — loose produce has none by design, and
|
||||||
|
// some scraped rows never had one. So an import lands at ₹0, the merchant adds
|
||||||
|
// stock, and the product goes on sale at nothing. Measured 2026-09-03: Kmart's
|
||||||
|
// entire customer-facing catalogue was five products, every one at ₹0.
|
||||||
|
//
|
||||||
|
// The merchant is not left guessing: the console lists these as "unpriced" with
|
||||||
|
// a Set price button, and pricing one puts it on sale immediately.
|
||||||
|
//
|
||||||
|
// Deliberately NOT applied to the console, for the same reason as the stock
|
||||||
|
// filter — an unpriced product is precisely what a merchant needs to see in
|
||||||
|
// order to fix it.
|
||||||
|
func PricedOnly(groups []models.Tenantproducts) []models.Tenantproducts {
|
||||||
|
out := make([]models.Tenantproducts, 0, len(groups))
|
||||||
|
for _, group := range groups {
|
||||||
|
kept := make([]models.Products, 0, len(group.Products))
|
||||||
|
for _, product := range group.Products {
|
||||||
|
// `Price` is the effective figure the same query computed — the
|
||||||
|
// outlet's own price, falling back to the master. Filtering on the
|
||||||
|
// number that is DISPLAYED means the list and the label cannot
|
||||||
|
// disagree.
|
||||||
|
if product.Price > 0 {
|
||||||
|
kept = append(kept, product)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group.Products = kept
|
||||||
|
out = append(out, group)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -715,3 +715,55 @@ func TestFilteringChildrenDoesNotDisturbTheCallersSlice(t *testing.T) {
|
|||||||
t.Errorf("the caller's slice was modified")
|
t.Errorf("the caller's slice was modified")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
A product with no price is unfinished, not free.
|
||||||
|
|
||||||
|
Import writes whatever the catalogue carries, and 159 of the 1,572 catalogue
|
||||||
|
rows carry no price — loose produce has none by design, some scraped rows never
|
||||||
|
had one. So a product can reach a shelf at ₹0 and go on sale at nothing.
|
||||||
|
|
||||||
|
Measured 2026-09-03: Kmart's entire customer-facing catalogue was five products,
|
||||||
|
every one at ₹0, all with stock.
|
||||||
|
*/
|
||||||
|
|
||||||
|
func TestAnUnpricedProductIsNotOfferedToShoppers(t *testing.T) {
|
||||||
|
groups := []models.Tenantproducts{{Products: []models.Products{
|
||||||
|
{Productid: 1, Price: 45},
|
||||||
|
{Productid: 2, Price: 0},
|
||||||
|
{Productid: 3, Price: 12.5},
|
||||||
|
}}}
|
||||||
|
got := PricedOnly(groups)
|
||||||
|
if len(got[0].Products) != 2 {
|
||||||
|
t.Fatalf("expected the two priced products, got %v", got[0].Products)
|
||||||
|
}
|
||||||
|
for _, product := range got[0].Products {
|
||||||
|
if product.Price <= 0 {
|
||||||
|
t.Errorf("product %d has no price and was still offered", product.Productid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestANegativePriceIsNotAPrice(t *testing.T) {
|
||||||
|
got := PricedOnly([]models.Tenantproducts{{Products: []models.Products{{Productid: 1, Price: -5}}}})
|
||||||
|
if len(got[0].Products) != 0 {
|
||||||
|
t.Error("a negative price was treated as sellable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAShopWithNothingPricedStillExists(t *testing.T) {
|
||||||
|
// The shop is open; it has nothing ready to sell. Dropping the group would
|
||||||
|
// say the shop is not there, which is a different and wrong statement.
|
||||||
|
got := PricedOnly([]models.Tenantproducts{{Products: []models.Products{{Productid: 1, Price: 0}}}})
|
||||||
|
if len(got) != 1 || len(got[0].Products) != 0 {
|
||||||
|
t.Errorf("expected the shop to survive with nothing on offer, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPricingFilterDoesNotDisturbTheCallersSlice(t *testing.T) {
|
||||||
|
original := []models.Tenantproducts{{Products: []models.Products{{Productid: 1, Price: 10}, {Productid: 2, Price: 0}}}}
|
||||||
|
_ = PricedOnly(original)
|
||||||
|
if len(original[0].Products) != 2 {
|
||||||
|
t.Error("the caller's slice was modified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user