health score
This commit is contained in:
@@ -50,10 +50,17 @@ type ProductService interface {
|
||||
type productService struct {
|
||||
repo repositories.ProductRepository
|
||||
catalogueService CatalogueService
|
||||
// May be nil. A deployment without the catalogue-intelligence service still
|
||||
// serves every product screen, without a nutrition panel on it.
|
||||
nutrition NutritionService
|
||||
}
|
||||
|
||||
func NewProductService(repo repositories.ProductRepository, catalogueService CatalogueService) ProductService {
|
||||
return &productService{repo: repo, catalogueService: catalogueService}
|
||||
func NewProductService(
|
||||
repo repositories.ProductRepository,
|
||||
catalogueService CatalogueService,
|
||||
nutrition NutritionService,
|
||||
) ProductService {
|
||||
return &productService{repo: repo, catalogueService: catalogueService, nutrition: nutrition}
|
||||
}
|
||||
|
||||
func (s *productService) GetProductSubCategory(categoryID, tenantID int) ([]models.ProductSubCategory, error) {
|
||||
@@ -251,7 +258,7 @@ func (s *productService) GetProductByVariant(tenantid, variantid, locationid, pr
|
||||
// `productstock` on each row is what the app disables on. A single ungrouped
|
||||
// product comes back as a one-member group, so the app has one code path:
|
||||
// count the rows, and skip the picker when there is only one.
|
||||
decorateNutrition(result)
|
||||
decorateNutrition(s.nutrition, result)
|
||||
return result, nil
|
||||
|
||||
}
|
||||
@@ -375,19 +382,6 @@ func catalogueFactsOf(p *models.CatalogueProduct) map[string]any {
|
||||
putList("highlights", p.Highlights)
|
||||
putList("nutrients", p.Nutrients)
|
||||
|
||||
// The structured panel, snapshotted like everything else here.
|
||||
//
|
||||
// Kept BESIDE `nutrients` rather than instead of it. The lines are what the
|
||||
// console has always shown and what a re-scrape can still change; the panel
|
||||
// is what the app renders. Dropping either would break a reader that exists.
|
||||
//
|
||||
// `Nutrition` is already the resolved one — structured where the catalogue
|
||||
// has it, derived from the lines where it does not — so a product imported
|
||||
// today carries a panel whether or not the agent team has reached its brand.
|
||||
if p.Nutrition.HasValues() {
|
||||
facts["nutrition"] = p.Nutrition
|
||||
}
|
||||
|
||||
return facts
|
||||
}
|
||||
|
||||
@@ -634,61 +628,34 @@ func (s *productService) UnpublishProduct(tenantID, productID int) (int, error)
|
||||
}
|
||||
|
||||
/*
|
||||
decorateNutrition unpacks each product's nutrition panel for the app.
|
||||
decorateNutrition attaches each product's nutrition panel.
|
||||
|
||||
── Why the endpoint does this and the query does not ───────────────────────
|
||||
|
||||
The panel lives inside `cataloguefacts`, a jsonb blob the import writes to hold
|
||||
everything the catalogue knew that `products` has no column for. Reading it in
|
||||
SQL would mean a jsonb path expression inside a query that already carries four
|
||||
correlated subqueries, and it would have to be repeated in every read that ever
|
||||
wants nutrition. Unpacking it once, here, keeps the query about the shelf.
|
||||
The figures are not in Fiesta's database. They live in the catalogue-
|
||||
intelligence service, keyed by brand and image_id — the two things a tenant's
|
||||
product already carries from import — so there is nothing to join to and nothing
|
||||
for the SQL to select. See nutritionService.go.
|
||||
|
||||
── The two shapes it accepts ───────────────────────────────────────────────
|
||||
── Why a loop of network calls is acceptable here and nowhere else ─────────
|
||||
|
||||
`nutrition` is the structured panel and is used wherever it exists. `nutrients`
|
||||
is the older text[] of display lines — "Energy 350kcal" — which is what every
|
||||
product on the platform actually carries today, and it is parsed into the same
|
||||
shape rather than left unreadable by the app.
|
||||
This endpoint answers one tapped product plus its variant siblings: one row for
|
||||
most products, a handful for a size family. Never a page of sixty. Each lookup
|
||||
is cached for six hours and capped at three seconds, and a failure costs that
|
||||
product its panel and nothing else.
|
||||
|
||||
So a product shows figures if ANYTHING is known about it, and the field is
|
||||
absent only when genuinely nothing is.
|
||||
|
||||
── What it will not do ─────────────────────────────────────────────────────
|
||||
|
||||
It does not go back to the catalogue DB for a product whose snapshot is empty.
|
||||
That is a cross-database lookup on a hot mobile endpoint, per product, on a
|
||||
screen a shopper is waiting for. Products imported before `cataloguefacts`
|
||||
existed have empty snapshots — measured: all 15 catalogue-imported products on
|
||||
tenant 1147 — and the fix for those is the backfill in
|
||||
`scratch/nutritionbackfill`, run once, not a lookup paid for on every tap.
|
||||
It is deliberately NOT on the browse lists. Sixty products would be sixty
|
||||
lookups on a screen that shows no nutrition anyway.
|
||||
*/
|
||||
func decorateNutrition(products []models.Products) {
|
||||
func decorateNutrition(nutrition NutritionService, products []models.Products) {
|
||||
if nutrition == nil {
|
||||
return
|
||||
}
|
||||
for i := range products {
|
||||
products[i].Nutrition = nutritionFromFacts(products[i].Cataloguefacts)
|
||||
// One call per product, not two: the panel and the score come from the
|
||||
// same upstream record.
|
||||
found := nutrition.ForProduct(products[i].Productbrand, products[i].Imageid)
|
||||
products[i].Nutrition = found.Panel
|
||||
products[i].Healthscore = found.Health
|
||||
}
|
||||
}
|
||||
|
||||
// nutritionFromFacts reads a panel out of one product's catalogue snapshot.
|
||||
//
|
||||
// Every failure returns nil rather than an error: the product screen is worth
|
||||
// drawing without a nutrition panel, and a malformed blob on one row must not
|
||||
// fail the response for the variant group it belongs to.
|
||||
func nutritionFromFacts(raw string) *models.NutritionPanel {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var facts struct {
|
||||
Nutrition *models.NutritionPanel `json:"nutrition"`
|
||||
Nutrients []string `json:"nutrients"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(raw), &facts); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if facts.Nutrition.HasValues() {
|
||||
return facts.Nutrition
|
||||
}
|
||||
return models.NutritionFromLines(facts.Nutrients)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user