# 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: ```json "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. ```sql 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: ```sh 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.