diff --git a/services/cataloguePrice.go b/services/cataloguePrice.go new file mode 100644 index 0000000..a2a3ec4 --- /dev/null +++ b/services/cataloguePrice.go @@ -0,0 +1,51 @@ +package services + +import ( + "regexp" + "strconv" + "strings" +) + +// The catalogue quotes a RANGE, as a string: "₹9-11", "₹75-95", sometimes a +// single "₹22". It is what a shopper would expect to pay across the shops the +// catalogue was built from, not a price any particular shop has set. +var cataloguePriceFigure = regexp.MustCompile(`[0-9]+(?:\.[0-9]+)?`) + +// SuggestedPriceFrom reads a starting price out of a catalogue price range. +// +// ── Why import needs this at all ──────────────────────────────────────────── +// +// Import deliberately wrote no price: "import is not pricing", the merchant sets +// it. That is right about who decides and wrong about what a shopper sees in the +// meantime — the product goes live at ₹0, which is not "unpriced" on a phone, it +// reads as free. Products uploaded by sheet carry their price and show it; the +// same product imported from the catalogue showed nothing. +// +// ── Which end of the range ────────────────────────────────────────────────── +// +// The LOW end. It is a suggestion the merchant will overwrite, and the two ways +// to be wrong are not equal: too low is a margin the shop notices and corrects, +// too high is a shopper who quietly decides the shop is expensive. The low end +// is also the number a catalogue range is usually anchored on. +// +// Returns 0 when there is no figure to read — 159 of the 672 catalogue rows +// measured on 2026-09-03 carry no range at all, and inventing a price for those +// would be worse than showing none. +func SuggestedPriceFrom(priceRange string) float64 { + found := cataloguePriceFigure.FindAllString(strings.TrimSpace(priceRange), -1) + if len(found) == 0 { + return 0 + } + + low := 0.0 + for _, raw := range found { + value, err := strconv.ParseFloat(raw, 64) + if err != nil || value <= 0 { + continue + } + if low == 0 || value < low { + low = value + } + } + return low +} diff --git a/services/cataloguePrice_test.go b/services/cataloguePrice_test.go new file mode 100644 index 0000000..8f5de91 --- /dev/null +++ b/services/cataloguePrice_test.go @@ -0,0 +1,60 @@ +package services + +import "testing" + +/* +Import used to write no price at all — "import is not pricing, the merchant sets +it". Right about who decides, wrong about what a shopper sees in between: the +product went live at ₹0, which on a phone reads as free rather than as unpriced. +A product uploaded by sheet carried its price and showed it; the same product +imported from the catalogue showed nothing. + +The catalogue quotes a range as a string, so it has to be read rather than cast. +*/ + +func TestARangeGivesItsLowEnd(t *testing.T) { + // Too low is a margin the shop notices and corrects; too high is a shopper + // quietly deciding the shop is expensive. + if got := SuggestedPriceFrom("₹9-11"); got != 9 { + t.Errorf("₹9-11 -> %v, want 9", got) + } + if got := SuggestedPriceFrom("₹75-95"); got != 75 { + t.Errorf("₹75-95 -> %v, want 75", got) + } +} + +func TestASinglePriceIsReadAsItself(t *testing.T) { + if got := SuggestedPriceFrom("₹22"); got != 22 { + t.Errorf("₹22 -> %v, want 22", got) + } +} + +func TestPaiseSurvive(t *testing.T) { + if got := SuggestedPriceFrom("₹12.50 - ₹15.75"); got != 12.5 { + t.Errorf("got %v, want 12.5", got) + } +} + +func TestOrderInsideTheRangeDoesNotMatter(t *testing.T) { + // A range written backwards still has a low end. + if got := SuggestedPriceFrom("₹95-75"); got != 75 { + t.Errorf("got %v, want 75", got) + } +} + +func TestNoPriceMeansNoPrice(t *testing.T) { + // 159 of 672 catalogue rows carry no range. Inventing a figure for those + // would be worse than showing none — the merchant would never know to look. + for _, input := range []string{"", " ", "₹", "price on request", "N/A"} { + if got := SuggestedPriceFrom(input); got != 0 { + t.Errorf("%q -> %v, want 0", input, got) + } + } +} + +func TestAZeroInTheRangeIsNotAPrice(t *testing.T) { + // "₹0-50" means the low end is unknown, not that the product is free. + if got := SuggestedPriceFrom("₹0-50"); got != 50 { + t.Errorf("got %v, want 50", got) + } +} diff --git a/services/productService.go b/services/productService.go index af1b5a9..5ceb264 100644 --- a/services/productService.go +++ b/services/productService.go @@ -336,6 +336,17 @@ func (s *productService) ImportCatalogueProduct(reqs []models.ImportCataloguePro // The fallback is not dead code and will not become so: products // imported before `imageid` existed carry only the id, and a re-import is // how they acquire the stable key. + // The price the product goes live at. + // + // The caller wins whenever it sends one — a merchant who typed a price + // is never overwritten by a catalogue suggestion. Only when it sends + // nothing does the catalogue’s own range fill the gap, so an imported + // product shows a price in the app instead of ₹0, which reads as free. + retail := req.Retailprice + if retail <= 0 { + retail = SuggestedPriceFrom(catalogueProduct.PriceRange) + } + existing, err := s.repo.FindTenantProductByImageID(req.Tenantid, catalogueProduct.ImageID) if err != nil { return err @@ -350,7 +361,7 @@ func (s *productService) ImportCatalogueProduct(reqs []models.ImportCataloguePro productID := 0 if existing != nil { productID = existing.Productid - if err := s.repo.UpdateProductPricing(productID, req.Retailprice, req.Productcost, req.Taxpercent); err != nil { + if err := s.repo.UpdateProductPricing(productID, retail, req.Productcost, req.Taxpercent); err != nil { return err } // Re-importing corrects the CATEGORY too, not just the price. @@ -400,7 +411,7 @@ func (s *productService) ImportCatalogueProduct(reqs []models.ImportCataloguePro Imageid: catalogueProduct.ImageID, Productunit: catalogueProduct.Size, Productcost: req.Productcost, - Retailprice: req.Retailprice, + Retailprice: retail, Taxpercent: req.Taxpercent, Approve: 1, }