# Price Currency — Frontend Guide

Every priced catalog item — **Courses**, **Training Programs**, **Consultations**, and **Books** — now returns a `currency` object next to its `price`, resolved the same way the price itself is: by the `X-Country` header, falling back to the item's admin-flagged default price.

- **Audience:** frontend developers (learner/web/mobile app)
- **Applies to:** course cards/detail, training-program cards/detail, consultant offerings, book cards/detail, and global search results for all of the above
- **Base path:** `{{base_url}}` = `/api/v1`
- **Related docs:** [courses-user.md](./courses-user.md), [training-programs-user.md](./training-programs-user.md), [consultations-user.md](./consultations-user.md), [book-pricing-update.md](./book-pricing-update.md)

Common headers on every call: `Accept: application/json`, `Content-Type: application/json`, `Accept-Language: en|ar`, optionally `X-Country: <ISO short code>` (e.g. `EG`, `SA`, case-insensitive). Responses use the standard envelope `{ "status", "message", "data" }`.

---

## 1. What changed in this update

**Before:** `price` already resolved per the caller's `X-Country` header (with a fallback to the admin-flagged default), but the currency it was priced in was never returned — clients had to hard-code a currency or infer it from the header country themselves.

**Now:** every response that carries a resolved `price` also carries the `currency` it's quoted in, as the same price row's country:

```json
{
  "price": 200,
  "currency": { "code": "SAR", "symbol": "ر.س" }
}
```

`currency` is `null` only when the item has **no price configured at all** (shouldn't happen for a published item, but treat it defensively).

### Bug fix included in this update

Consultation offerings returned from **global search** (`GET /user/search?type=consultations` and the "all sections" search) previously **ignored `X-Country` entirely** and always showed the admin-flagged default price — unlike the consultant profile page (`GET /user/consultants/{trainer}`), which already resolved the header correctly. Both now resolve `X-Country` the same way. If your search results UI assumed a fixed currency for consultations, re-check it against the consultant profile page — they can now show a different price/currency depending on the header, same as they always could there.

### What this means for the frontend

1. **Read `currency` from the response, never assume one.** Don't hard-code "SAR" or "EGP" anywhere — the same catalog item can be priced in a different currency for a different `X-Country`.
2. **Always render `price` together with `currency.symbol` (or `currency.code`)** — a bare number is ambiguous once more than one currency is in play.
3. **No breaking changes.** `currency` is a new, additive field. Existing clients that only read `price` keep working unchanged.
4. **Guard for `null`.** If `currency` is `null`, treat the item as **not currently purchasable in any country** (no price row exists) — hide the buy/enroll/book CTA rather than rendering `price: null` with no currency.

---

## 2. Response shape

`currency` is always either `null` or an object with two string fields:

| Field | Meaning |
|---|---|
| `currency.code` | ISO-ish currency code from the country's `currency_code` (e.g. `"SAR"`, `"EGP"`) |
| `currency.symbol` | Display symbol from the country's `currency_symbol` (e.g. `"ر.س"`, `"ج.م"`) |

Resolution order (identical to how `price` itself is resolved, and identical across all four entities):

1. The price row whose `country_id` matches the country from the `X-Country` header, if the header is present and the code is a known, active country.
2. Else, the item's admin-flagged **default** price row (`is_default: true`).
3. Else, the first price row on the item (defensive fallback; shouldn't happen for a properly configured item).
4. If the item has no price rows at all, both `price` and `currency` are `null`.

`X-Country` is optional and case-insensitive. An unrecognized code (not in the `countries` table) is treated the same as no header — falls through to step 2.

---

## 3. Endpoint map

| Entity | Method & path | `currency` location |
|---|---|---|
| Course card/browse | `GET /user/courses` | `data.courses[].currency` |
| Course detail | `GET /user/courses/{course}` | `data.currency` |
| Training program card/browse | `GET /user/programs` | `data.training_programs[].currency` |
| Training program detail | `GET /user/programs/{program}` | `data.currency` |
| Consultant profile (offerings) | `GET /user/consultants/{trainer}` | `data.offerings[].currency` |
| Book card/browse | `GET /client/books` | `data.books[].currency` *(already existed)* |
| Book detail | `GET /client/books/{book}` | `data.currency` *(already existed)* |
| Global search — single section | `GET /user/search?type=courses\|training_programs\|consultations\|books` | same per-item shape as above |
| Global search — all sections | `GET /user/search` | `data.results.{courses,training_programs,consultations,books}[].currency` |

`{course}`, `{program}`, `{trainer}`, `{book}` are **UUIDs**, never numeric ids.

---

## 4. Examples

### 4.1 Course browse — `GET /user/courses` with `X-Country: SA`

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "courses": [
      {
        "id": "9d3f…-course-uuid",
        "name": "Intro to Accounting",
        "price": 200,
        "currency": { "code": "SAR", "symbol": "ر.س" }
      }
    ]
  }
}
```

Same request without the header (or with an unknown code, e.g. `X-Country: US`) returns the admin default instead:

```json
{ "price": 100, "currency": { "code": "EGP", "symbol": "ج.م" } }
```

### 4.2 Training program detail — `GET /user/programs/{program}`

```json
{
  "data": {
    "id": "…-program-uuid",
    "name": "Leadership Bootcamp",
    "price": 2000,
    "currency": { "code": "SAR", "symbol": "ر.س" },
    "has_certificate": true
  }
}
```

### 4.3 Consultant profile — `GET /user/consultants/{trainer}`

```json
{
  "data": {
    "id": "…-trainer-uuid",
    "name": "Dr. Amina Youssef",
    "offerings": [
      {
        "id": "…-offering-uuid",
        "consultation_type": "Career Coaching",
        "price": 175,
        "currency": { "code": "EGP", "symbol": "ج.م" },
        "duration_minutes": 45
      }
    ]
  }
}
```

### 4.4 Global search (consultations) — `GET /user/search?type=consultations` with `X-Country: SA`

```json
{
  "data": {
    "data": [
      {
        "id": "…-offering-uuid",
        "price": 200,
        "currency": { "code": "SAR", "symbol": "ر.س" },
        "duration_minutes": 60
      }
    ]
  }
}
```

This is the endpoint that previously **ignored** `X-Country` — confirm your search-results screen now varies price/currency by header the same way the consultant profile page does.

---

## 5. Display recommendations

```ts
type Money = { price: number | null; currency: { code: string; symbol: string } | null };

function formatPrice({ price, currency }: Money): string {
  if (price == null || currency == null) return 'Not available';
  // Prefer currency.symbol for display; fall back to code if the symbol is missing/unsupported by the font.
  return `${price.toLocaleString()} ${currency.symbol ?? currency.code}`;
}
```

- Don't format with a locale-fixed currency formatter (e.g. `Intl.NumberFormat('en-US', { currency: 'USD' })`) — the currency is dynamic per response, not tied to the app's locale.
- If you cache catalog cards client-side, cache `price` and `currency` together and invalidate both when `X-Country` changes (e.g. the user switches their country in settings) — refetch rather than reusing a stale price with a new currency or vice versa.
- Cart/checkout screens already resolve currency independently (`CartResource`/`OrderResource`) — this update does not change those; it only adds `currency` to the catalog browse/detail/search responses above.

---

## 6. Edge cases

| Situation | Behaviour |
|---|---|
| No `X-Country` header sent | Resolves to the admin-flagged default price/currency |
| `X-Country` set to an unknown/inactive code | Same as no header — falls back to default |
| `X-Country` matches a country with no price row for this item | Falls back to default, **not** `null` |
| Item has zero price rows configured | `price: null`, `currency: null` — hide purchase CTA |
| Consultation search vs. consultant profile for the same offering | Now return the **same** price/currency for the same `X-Country` (previously could differ) |

---

## 7. QA checklist

- [ ] Course/program/book/consultation cards show the right symbol for the `X-Country` sent, and switch when the header changes.
- [ ] Omitting `X-Country` (or sending an unknown code) shows the admin default price *with* its own currency, not a mismatched one.
- [ ] Global search results for consultations match the consultant profile page's price/currency for the same `X-Country`.
- [ ] An item with no price rows renders as unavailable, not `NaN <symbol>` or `null null`.
- [ ] No screen hard-codes a currency symbol/code for these entities.

---

## 8. Backend reference

| Concern | Location |
|---|---|
| Shared price/currency subquery helper | `app/Support/ResolvedPrice.php` |
| Course resolution | `app/Services/Client/CourseService.php`, `app/Http/Resources/User/Course/CourseCardResource.php` |
| Training program resolution | `app/Services/Client/TrainingProgramService.php`, `app/Http/Resources/Client/TrainingProgram/TrainingProgramResource.php` |
| Consultation resolution | `app/Http/Resources/Client/Consultation/ConsultantOfferingResource.php`, `app/Http/Resources/Client/Consultation/ConsultantDetailsResource.php` |
| Book resolution (unchanged, reference implementation) | `app/Services/Books/BookPricingService.php`, `app/Http/Resources/Client/Book/BookCardResource.php` |
| Global search | `app/Services/Client/GlobalSearchService.php` |
| Header resolution rule | `app/Support/PricingCountry.php` |
| Tests | `tests/Feature/Education/LearnerCourseApiTest.php`, `tests/Feature/User/TrainingProgramCrudTest.php`, `tests/Feature/Consultation/Client/ConsultationBrowsingTest.php` |
