nutrition field

This commit is contained in:
2026-09-29 17:27:48 +05:30
parent b46902f51b
commit f9fb405974
9 changed files with 764 additions and 34 deletions

View File

@@ -251,6 +251,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)
return result, nil
}
@@ -374,6 +375,19 @@ 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
}
@@ -618,3 +632,63 @@ func (s *productService) PublishProduct(tenantID, productID int, price, taxPerce
func (s *productService) UnpublishProduct(tenantID, productID int) (int, error) {
return s.repo.UnpublishProduct(tenantID, productID)
}
/*
decorateNutrition unpacks each product's nutrition panel for the app.
── 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 two shapes it accepts ───────────────────────────────────────────────
`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.
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.
*/
func decorateNutrition(products []models.Products) {
for i := range products {
products[i].Nutrition = nutritionFromFacts(products[i].Cataloguefacts)
}
}
// 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)
}