e2e changes

This commit is contained in:
2026-09-16 17:09:04 +05:30
parent c06b029cb2
commit 2f1501883b
18 changed files with 1169 additions and 50 deletions

View File

@@ -4,9 +4,11 @@ import (
"encoding/json"
"fmt"
"log"
"strings"
"time"
"nearle/models"
"nearle/repositories"
"time"
)
type ProductService interface {
@@ -22,7 +24,7 @@ type ProductService interface {
RemoveProductVariant(tenantid, variantid int) error
VariantChildIDs(tenantid int) (map[int]bool, error)
CreateProductStock(stocks []models.Productstock) error
CreateProduct(product models.Products) error
CreateProduct(product models.Products) (models.Products, error)
UpdateProduct(product models.Products) error
DeleteProduct(productID int) error
GetStockStatement(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Productstockstatement, error)
@@ -169,8 +171,27 @@ func (s *productService) UpdateProductStatus(productIDs []int, status string) er
return s.repo.UpdateProductStatus(productIDs, status)
}
func (s *productService) CreateProduct(product models.Products) error {
return s.repo.CreateProduct(product)
// CreateProduct stores one product and hands it back with its id filled in.
//
// It used to return only an error, and the id was lost on the way out: the
// repository took the struct by value, GORM wrote the generated productid onto
// that copy, and the copy was discarded — so the endpoint answered
// `productid: 0` for a row that certainly had one.
//
// The caller needs it. A product is not sellable until it has been priced at an
// outlet and stocked there, and both of those calls are keyed on productid, so
// every importer had to create, re-read the tenant's whole catalogue, and match
// its own rows back by SKU to carry on — which is also why creating two
// products with the same SKU quietly attached the second one's stock to the
// first.
func (s *productService) CreateProduct(product models.Products) (models.Products, error) {
id, err := s.repo.CreateProductReturningID(product)
if err != nil {
return models.Products{}, err
}
product.Productid = id
return product, nil
}
func (s *productService) UpdateProduct(product models.Products) error {
@@ -308,6 +329,54 @@ func (s *productService) DeleteProductLocation(tenantid, locationid, productid i
return s.repo.DeleteProductLocation(tenantid, locationid, productid)
}
// catalogueFactsOf collects the catalogue fields the product table has no
// column for, so an import keeps them instead of leaving them behind.
//
// Only what the catalogue actually stated: an empty field is omitted rather
// than written as `""` or `[]`, so a reader can tell "the catalogue did not say"
// from "the catalogue said none". The drawer prints a row per fact and an empty
// string would print an empty row.
//
// The keys are the catalogue's own wire names. They are what the console
// already reads off a live catalogue row, so the same rendering works against
// either source without a translation layer in between.
func catalogueFactsOf(p *models.CatalogueProduct) map[string]any {
facts := map[string]any{}
if p == nil {
return facts
}
put := func(key, value string) {
if v := strings.TrimSpace(value); v != "" {
facts[key] = v
}
}
putList := func(key string, values []string) {
kept := make([]string, 0, len(values))
for _, v := range values {
if t := strings.TrimSpace(v); t != "" {
kept = append(kept, t)
}
}
if len(kept) > 0 {
facts[key] = kept
}
}
put("title", p.Title)
put("category", p.Category)
put("variant_key", p.VariantKey)
put("sku_source", p.SKUSource)
put("price_range", p.PriceRange)
put("fssai_license", p.FSSAILicense)
put("search_query", p.SearchQuery)
putList("providers", p.Providers)
putList("highlights", p.Highlights)
putList("nutrients", p.Nutrients)
return facts
}
// ImportCatalogueProduct bridges a global catalogue product (CatalogueDB) into
// a tenant's own store catalogue: it snapshots the catalogue product into the
// tenant's `products` table on first import (keyed on brand+catalogueid so
@@ -417,6 +486,27 @@ func (s *productService) ImportCatalogueProduct(reqs []models.ImportCataloguePro
Taxpercent: req.Taxpercent,
Approve: 1,
}
// Everything the snapshot has no column for, kept as the catalogue
// stated it.
//
// Ten of the catalogue's eighteen fields used to stop here. Two of
// them SHOULD — `category` is remapped to the platform's own
// categoryid, and `price_range` is replaced by the price the shop
// sets — but they are kept anyway, because what other retailers
// charge is the most useful thing on the drawer when somebody is
// deciding what to charge, and the catalogue's own category is how
// a mis-filed product gets noticed.
//
// Encoding failure is swallowed, like the images below: the product
// is worth creating without its facts, and refusing an import over
// a nutrition line would be the wrong trade.
if encoded, err := json.Marshal(catalogueFactsOf(catalogueProduct)); err == nil {
snapshot.Cataloguefacts = string(encoded)
} else {
log.Printf("import: could not encode catalogue facts for %s/%d: %v",
req.Brand, req.Catalogueid, err)
}
if len(catalogueProduct.Images) > 0 {
// The first stays where every reader already looks for it.
snapshot.Productimage = catalogueProduct.Images[0]