pricing
This commit is contained in:
51
services/cataloguePrice.go
Normal file
51
services/cataloguePrice.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
60
services/cataloguePrice_test.go
Normal file
60
services/cataloguePrice_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -336,6 +336,17 @@ func (s *productService) ImportCatalogueProduct(reqs []models.ImportCataloguePro
|
|||||||
// The fallback is not dead code and will not become so: products
|
// 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
|
// imported before `imageid` existed carry only the id, and a re-import is
|
||||||
// how they acquire the stable key.
|
// 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)
|
existing, err := s.repo.FindTenantProductByImageID(req.Tenantid, catalogueProduct.ImageID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -350,7 +361,7 @@ func (s *productService) ImportCatalogueProduct(reqs []models.ImportCataloguePro
|
|||||||
productID := 0
|
productID := 0
|
||||||
if existing != nil {
|
if existing != nil {
|
||||||
productID = existing.Productid
|
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
|
return err
|
||||||
}
|
}
|
||||||
// Re-importing corrects the CATEGORY too, not just the price.
|
// Re-importing corrects the CATEGORY too, not just the price.
|
||||||
@@ -400,7 +411,7 @@ func (s *productService) ImportCatalogueProduct(reqs []models.ImportCataloguePro
|
|||||||
Imageid: catalogueProduct.ImageID,
|
Imageid: catalogueProduct.ImageID,
|
||||||
Productunit: catalogueProduct.Size,
|
Productunit: catalogueProduct.Size,
|
||||||
Productcost: req.Productcost,
|
Productcost: req.Productcost,
|
||||||
Retailprice: req.Retailprice,
|
Retailprice: retail,
|
||||||
Taxpercent: req.Taxpercent,
|
Taxpercent: req.Taxpercent,
|
||||||
Approve: 1,
|
Approve: 1,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user