Files
backend_fiesta/docs/NUTRITION_DATA.md
2026-09-29 17:27:48 +05:30

5.4 KiB

Nutrition data — the contract between the agent team, Fiesta and the app

The customer app shows a nutrition panel on the product screen. The data comes from the global catalogue, which the agent team fills. This is what each side has to produce and can rely on.


1. What the app receives

GET /live/api/v1/mob/products/getproductbyvariant?tenantid=&productid=&variantid=

Each product in details[] carries:

"nutrition": {
  "per": "100g",
  "servingsize": "30g",
  "items": [
    { "name": "Energy",        "value": 520, "unit": "kcal" },
    { "name": "Protein",       "value": 6.5, "unit": "g" },
    { "name": "Total Sugars",  "value": 22,  "unit": "g" },
    { "name": "Sodium",        "value": 310, "unit": "mg" }
  ]
}

Rules the app can build on:

  • The key is ABSENT when nothing is known. Not null, not {}. A missing key means "we do not know", never "this food has no nutrition".
  • items is never empty when nutrition is present. A panel with no rows is not sent, because an empty box on a product page reads as a claim.
  • per and servingsize are each optional and often absent — see §3. Show the basis only when it is there. Do not default it to 100g: a wrong basis makes every figure beneath it a misstatement rather than an unknown.
  • value is a number, and may be a decimal. Saturated fat is 11.5 g as often as 11 g.
  • unit is free text and may be absent. "Servings per pack 4" has a figure and no unit.
  • A row may have a name and a value of 0 with no unit. That is real label text with no number in it — "Contains permitted natural colour". Render the name and leave the figure column blank; do not print 0.
  • Every member of a variant group carries its own panel, so switching from 500 ml to 1 L does not blank the screen.

2. What the agent team fills

A nutrition column of type jsonb on each brand_* table in the catalogue database, holding exactly the object above.

ALTER TABLE brand_patanjali ADD COLUMN IF NOT EXISTS nutrition jsonb;

Notes:

  • The column is optional and Fiesta already handles its absence. Catalogue reads discover columns per table and substitute NULL for any that are missing, so brands can be filled one at a time and a table without the column keeps working. Nothing has to be co-ordinated with a deploy.
  • Spelling is servingsize, one word, lowercase. It is the shape the app was written against.
  • Write what the label says. If the pack states per 100 g, per is "100g". If it states per serving, say so. If it states neither, omit the field rather than assuming.
  • nutrition and the older nutrients may both exist on a row. Keep both — the console shows the lines, the app shows the panel, and they are different readers with different needs.

3. Where today's data comes from, and why it is thinner

Every catalogue row currently holds nutrition as nutrients, a text[] of display lines:

{"Energy 350kcal", "Protein 7g"}

Fiesta parses those into the same panel shape when no structured nutrition exists, so the app shows figures for products the agent team has not reached yet. That derived panel has no per and no servingsize — the strings never carried them — which is the visible difference between a filled brand and one still waiting.

Parsing rules, in models/nutrition.go:

line becomes
Energy 350kcal {Energy, 350, kcal}
Protein: 6.5 g {Protein, 6.5, g}
Energy - 520 kcal {Energy, 520, kcal}
Vitamin B12 1.2µg {Vitamin B12, 1.2, µg}
Servings per pack 4 {Servings per pack, 4, ""}
Contains permitted colour {Contains permitted colour, 0, ""} — kept whole
100 dropped — a figure with no label is noise

Structured always wins over derived where both exist.

4. How it reaches a shop's product

The catalogue is the source; a tenant's product is a snapshot taken at import, because the catalogue is re-scraped and rows retire. Chain:

brand_*.nutrition (agent team)
  └─ catalogue read  ─ repositories/catalogueRepository.go: nutritionOf
       └─ import     ─ services/productService.go: catalogueFactsOf
            └─ products.cataloguefacts → {"nutrition": {...}, "nutrients": [...]}
                 └─ endpoint ─ services/productService.go: decorateNutrition

getproductbyvariant does not query the catalogue database. That would be a cross-database lookup per product on a screen a shopper is waiting on. It reads the snapshot only.

5. Products imported before the snapshot existed

This is the one thing that must be done before any of it shows.

cataloguefacts is empty on every product imported before that column existed — measured on tenant 1147: 17 products, 15 catalogue-imported, 0 with facts. Those products will show no nutrition no matter what the catalogue holds.

The fix is a one-off backfill, which also restores their FSSAI licence, highlights and provider list:

go run ./scratch/cataloguefactsbackfill        # dry run — prints every change
go run ./scratch/cataloguefactsbackfill apply  # writes, then prints the undo

It touches only products with an imageid and a NULL cataloguefacts, so re-running it is a no-op rather than a second opinion. Run it after the agent team fills a brand to pick up that brand's structured panels; it is safe to run repeatedly as more brands are filled.