# "My Needs" endpoint — integration guide

How the customer portal loads the logged-in patient's nutrient needs. This is the
contract the Flutter app uses today; replicate the same call + client-side transform
in React Native.

> **Heads up:** there is no `GET /needs` endpoint. The needs screen is derived
> **client-side** from the user's **recipe history**. The server returns raw recipe
> snapshots; the app picks the newest one and reshapes it. You must replicate that
> transform — see [Client-side transform](#client-side-transform).

## Endpoint

```
GET {userMetadataBaseUrl}/api/v1/recipe
Authorization: Bearer <patient access token>
Accept: application/json
```

- **Service:** UserMetadata (UMD).
- **No path/query params.** The result is scoped to the caller via the JWT `sub` —
  you always get *your own* recipes, never another user's.
- **Returns:** a JSON **array** of recipe snapshots (the full history), newest not
  guaranteed first — sort client-side.

### Base URL per environment

| Env     | `userMetadataBaseUrl`                                   |
|---------|---------------------------------------------------------|
| dev     | `https://ng-novoservices-dev-wa-umd.azurewebsites.net`  |
| staging | `https://ng-novoservices-stag-wa-umd.azurewebsites.net` |
| prod    | `https://ng-novoservices-prod-wa-umd.azurewebsites.net` |

> Real customer data lives on **prod** (read-only). dev/staging are mostly empty,
> so an empty `[]` there is expected, not a bug.

### Status codes

| Code  | Meaning |
|-------|---------|
| `200` | OK — array of recipes (may be empty `[]` → user has no recipes yet). |
| `403` | Patient's email is **not confirmed**. The app surfaces this as a distinct "confirm your email" state, not a generic error. |
| `401` | Token missing/expired/invalid — **or** the token's audience doesn't include `user-metadata-service` (see Auth). |

## Auth

OAuth2 / OIDC **authorization code + PKCE**, public client (no secret — never embed one).

- **Client id:** `genius-app`
- **Scopes:** `openid profile email novodaily_shop customer-portal`
  - `novodaily_shop` is what grants access to the patient `userdata` endpoints.
- **Authority (IdentityServer) per env:**
  - dev: `https://ng-novoservices-dev-wa-is.azurewebsites.net`
  - staging: `https://ng-novoservices-stag-wa-is.azurewebsites.net`
  - prod: `https://ng-novoservices-prod-wa-is.azurewebsites.net`
- **Redirect URIs registered for `genius-app`:** native
  `com.novogenia.geniusapp://oauth/callback`; web `http://localhost:5180/redirect.html`.
  Register your own RN redirect URI in IdentityManagement before testing.

> **Audience gotcha:** a valid patient token still **401s** a backend until that
> service's audience is added to the customer-portal scope in Identity Server. If
> `/api/v1/recipe` 401s with a token that otherwise works, check the token's `aud`
> claim includes `user-metadata-service`.

## Raw response shape

Array of recipe snapshots. **Keys are case-insensitive** on the wire — the backend
has shipped both camelCase and PascalCase, so normalize keys before reading them.

```jsonc
[
  {
    "date": "2026-05-21T00:00:00Z",        // ISO 8601; sort key for "latest"
    "ingredients": [
      {
        "name": "Vitamin D",                // nutrient key — also the catalog lookup key
        "dose": 2000,                        // number
        "need": "High Need",                 // enum-ish string, see mapping below
        "arrows": "UP",                      // trend string, see mapping below
        "surveyDone": true,                  // questionnaire data contributed?
        "bloodDone": false,                  // blood marker data contributed?
        "dnaDone": true                      // DNA data contributed?
      }
      // ... more ingredients
    ]
  }
  // ... more recipe snapshots (history)
]
```

### TypeScript types

```ts
type RecipeDTO = {
  date: string;                 // ISO 8601
  ingredients: IngredientDTO[];
};

type IngredientDTO = {
  name: string;
  dose: number;
  need: string;                 // see needLevel mapping
  arrows: string;               // see trend mapping
  surveyDone: boolean;
  bloodDone: boolean;
  dnaDone: boolean;
};
```

### Enum mappings (parse defensively)

Both `need` and `arrows` arrive as free-ish strings. The backend has sent both
`[EnumMember]` display strings ("High Need", "UP UP") and raw C# enum names
("HighNeed", "UPUP"). Normalize (lowercase, strip non-alphanumerics) before matching.

**`need` → display level** (normalize, then match):

| Normalized input        | Level        | Notes                          |
|-------------------------|--------------|--------------------------------|
| `avoid`                 | `noNeed`     | red                            |
| `lowneed`               | `lowNeed`    | blue                           |
| `good`, `youregood`     | `normalNeed` | green                          |
| `need`, `highneed`      | `highNeed`   | orange                         |
| anything else / unknown | `none`       | **hidden** — drop from the UI  |

**`arrows` → trend** (normalize, then `startsWith`):

| Normalized starts with | Trend    |
|------------------------|----------|
| `up`                   | `up`     |
| `down`                 | `down`   |
| otherwise              | `stable` |

## Client-side transform

The screen is **not** the raw response. Replicate these steps:

1. **Pick the latest recipe.** Sort the array by `date` descending; take the first.
   Treat a missing/unparseable `date` as the epoch (sorts last). Empty array → empty
   needs state.
2. **Map enums.** For each ingredient, convert `need` → level and `arrows` → trend
   per the tables above.
3. **Drop invisible nutrients.** Discard any ingredient whose level is `none`.
4. **Enrich from the local nutrient catalog.** The API does **not** return display
   name, unit, category, image, or descriptive copy. Look those up by ingredient
   `name` in a client-side catalog (in Flutter: `data/catalog/nutrient_catalog.dart`).
   The catalog supplies: localized `name`, `unit`, `category`, `imageAsset`, and a
   `details` blob. Carry `dose`, the mapped `level`/`trend`, and the three
   `surveyDone`/`bloodDone`/`dnaDone` flags through from the ingredient.

Result per nutrient: `{ name, dose, unit, level, trend, category, image, details,
surveyDone, bloodDone, dnaDone }`.

> Porting note: bring the catalog over too, or stand up an equivalent lookup. Without
> it you only have `name` + `dose` + flags and can't render units, categories, or
> images.

## Minimal React Native fetch

```ts
const NEEDS_LEVEL: Record<string, string> = {
  avoid: "noNeed",
  lowneed: "lowNeed",
  good: "normalNeed",
  youregood: "normalNeed",
  need: "highNeed",
  highneed: "highNeed",
};

const norm = (s: string | undefined) =>
  (s ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");

function mapLevel(raw?: string): string {
  return NEEDS_LEVEL[norm(raw)] ?? "none";
}

function mapTrend(raw?: string): "up" | "down" | "stable" {
  const n = norm(raw);
  if (n.startsWith("up")) return "up";
  if (n.startsWith("down")) return "down";
  return "stable";
}

// case-insensitive key read (wire sends camelCase OR PascalCase)
function ci(obj: Record<string, any>, key: string) {
  const lower = key.toLowerCase();
  const hit = Object.keys(obj).find((k) => k.toLowerCase() === lower);
  return hit ? obj[hit] : undefined;
}

async function fetchNeeds(baseUrl: string, accessToken: string) {
  const res = await fetch(`${baseUrl}/api/v1/recipe`, {
    headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
  });

  if (res.status === 403) throw new Error("EMAIL_NOT_CONFIRMED");
  if (!res.ok) throw new Error(`recipe fetch failed: ${res.status}`);

  const history: any[] = await res.json();
  if (!history.length) return { date: null, nutrients: [] };

  // latest by date
  const latest = [...history].sort((a, b) =>
    String(ci(b, "date") ?? "").localeCompare(String(ci(a, "date") ?? "")),
  )[0];

  const nutrients = ((ci(latest, "ingredients") as any[]) ?? [])
    .map((i) => ({
      name: ci(i, "name") ?? "",
      dose: Number(ci(i, "dose") ?? 0),
      level: mapLevel(ci(i, "need")),
      trend: mapTrend(ci(i, "arrows")),
      surveyDone: ci(i, "surveyDone") === true,
      bloodDone: ci(i, "bloodDone") === true,
      dnaDone: ci(i, "dnaDone") === true,
    }))
    .filter((n) => n.level !== "none"); // drop hidden
    // then: enrich each with catalog.lookup(n.name) for unit/category/image/details

  return { date: ci(latest, "date") ?? null, nutrients };
}
```

## Source references (Flutter app)

| Concern            | File |
|--------------------|------|
| HTTP call          | `lib/data/sources/microservice/microservice_needs_repository.dart` |
| Base URL + scopes  | `lib/core/config/app_config.dart` |
| Latest-pick + parse| `lib/data/models/recipe.dart` |
| Enum mappings      | `lib/data/models/need_level.dart`, `lib/data/models/trend.dart` |
| Aggregate + enrich | `lib/data/models/need_nutrient.dart` |
| Catalog lookup     | `lib/data/catalog/nutrient_catalog.dart` |
