# Client-facing Research API — Curl / Postman

For the React client frontend hosted on a separate server.

**Controller:** `ClientResearchController` (categories reuse `ResearchCategoryController::index`)
**Middleware:** `api`, `restrict.client.origin`, `throttle:60,1`
**No JWT required** — these routes do not use `jwt.auth` / `check.token` / `dynamic.permission`, so **no `route_apis` rows are needed**.
**No caching** — every request reads live data.

Base URL (XAMPP local):

```text
http://localhost/fintraBackend/fi_adm/public/api
```

---

## Setup before the client can call these

### 1. Allow the client origin

`restrict.client.origin` checks the `Origin` header (falling back to `Referer`) against a comma-separated allowlist. Add the React host to `.env`:

```env
TODAYS_DATA_ALLOWED_ORIGINS=https://client.fintra.com.bd,http://localhost:3000
```

Use scheme + host + port, with no trailing slash. Then clear the config cache:

```bash
php artisan config:clear
```

> **Important:** if `TODAYS_DATA_ALLOWED_ORIGINS` is empty, the middleware lets **every** request through. Set it explicitly in production.

### 2. CORS

`config/cors.php` currently ships `'allowed_origins' => ['*']` for local development, with the production block commented out. For production, uncomment it and include the client host, otherwise the browser blocks the cross-origin request before it ever reaches the middleware.

### 3. What this does and does not protect

Origin restriction is a **browser-enforced** control, not authentication. It stops another website from reading this API in a browser, but `curl` or Postman can send any `Origin` header, so it does not prove the caller is a logged-in investor. The PDFs also live on the `public` disk, so `storage/research_reports/*.pdf` URLs are fetchable by anyone holding the link regardless of how the JSON API is gated. If per-investor gating is needed later, add a middleware that verifies a JWT minted by the client auth server to the same route group; the controller and resource would not change.

---

## 1. Featured reports (hero slider)

Latest featured reports across all categories. Returns 3 by default.

```bash
curl --location --request GET "http://localhost/fintraBackend/fi_adm/public/api/client-research/featured-reports" \
  --header "Accept: application/json" \
  --header "Origin: http://localhost:3000"
```

Custom count (clamped to 1-10):

```bash
curl --location --request GET "http://localhost/fintraBackend/fi_adm/public/api/client-research/featured-reports?limit=5" \
  --header "Accept: application/json" \
  --header "Origin: http://localhost:3000"
```

### Response (200)

```json
{
  "status": true,
  "count": 3,
  "data": [
    {
      "id": 7,
      "report_name": "BATBC_Q4_2026_2026-09-11",
      "sub_heading": "Updated listing criteria for the SME Board following BSEC's 2025 directive.",
      "report_type": null,
      "category": { "id": 1, "name": "Company Analysis" },
      "uploaded_at": "11 Sep 2026, 10:45 PM",
      "uploaded_by": { "id": 5, "name": "Saad Arnob", "avatar_url": null },
      "pdf_previews": {
        "english_pdf_url": "http://localhost/fintraBackend/fi_adm/public/storage/research_reports/abc123.pdf",
        "bangla_pdf_url": "http://localhost/fintraBackend/fi_adm/public/storage/research_reports/def456.pdf"
      },
      "tags": ["#equity", "#market", "#outlook"],
      "featured_image_url": "http://localhost/fintraBackend/fi_adm/public/storage/research_reports/hero789.jpg",
      "is_featured": "Featured"
    }
  ]
}
```

Because this endpoint filters on `is_featured = 1`, `is_featured` is always `"Featured"` here — it never comes back `null`.

---

## 2. Research history (card grid)

Same behaviour as the back-office `research-history-index`, with `featured_image_url` and `is_featured` added to every item.

```bash
curl --location --request GET "http://localhost/fintraBackend/fi_adm/public/api/client-research/research-history" \
  --header "Accept: application/json" \
  --header "Origin: http://localhost:3000"
```

Filter by category tab (a `research_categories.id`; `all` or omitting it returns everything):

```bash
curl --location --request GET "http://localhost/fintraBackend/fi_adm/public/api/client-research/research-history?category=1" \
  --header "Accept: application/json" \
  --header "Origin: http://localhost:3000"
```

Keyword search across `report_title`, `sub_heading`, report type name and tag names:

```bash
curl --location --request GET "http://localhost/fintraBackend/fi_adm/public/api/client-research/research-history?search=budget" \
  --header "Accept: application/json" \
  --header "Origin: http://localhost:3000"
```

Six per page to match the client card grid:

```bash
curl --location --request GET "http://localhost/fintraBackend/fi_adm/public/api/client-research/research-history?per_page=6&page=2" \
  --header "Accept: application/json" \
  --header "Origin: http://localhost:3000"
```

### Query parameters

| Parameter | Required | Type | Notes |
|---|---|---|---|
| `category` | No | int or `all` | A `research_categories.id`. `all`, empty, or omitted skips the filter. |
| `search` | No | string | Max 255. Matches report title, sub heading, report type name, tag name. |
| `per_page` | No | int | Min 1, max 100. Defaults to `15`. |
| `page` | No | int | Standard Laravel pagination page number. |

### Response (200)

```json
{
  "status": true,
  "data": [
    {
      "id": 6,
      "report_name": "Bangladesh National Budget_2026",
      "sub_heading": "Proposed 5% tax rebate for listed manufacturing companies.",
      "report_type": { "id": 5, "name": "Budget Analysis Reports" },
      "category": { "id": 5, "name": "Macro Analysis" },
      "uploaded_at": "10 Sep 2026, 11:30 PM",
      "uploaded_by": { "id": 5, "name": "Saad Arnob", "avatar_url": null },
      "pdf_previews": {
        "english_pdf_url": "http://localhost/fintraBackend/fi_adm/public/storage/research_reports/bud111.pdf",
        "bangla_pdf_url": "http://localhost/fintraBackend/fi_adm/public/storage/research_reports/bud222.pdf"
      },
      "tags": ["#budget", "#fiscal", "#adp"],
      "featured_image_url": null,
      "is_featured": null
    }
  ],
  "category_counts": {
    "all": 7,
    "categories": [
      { "id": 1, "name": "Company Analysis", "count": 1 },
      { "id": 2, "name": "Sector Analysis", "count": 1 },
      { "id": 3, "name": "Market Updates", "count": 1 },
      { "id": 4, "name": "Daily News", "count": 1 },
      { "id": 5, "name": "Macro Analysis", "count": 2 },
      { "id": 6, "name": "Special Feature", "count": 1 }
    ]
  },
  "pagination": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 15,
    "total": 7
  }
}
```

`category_counts` honours `search` but **ignores** `category`, so every tab keeps its badge while one tab is selected. Categories with zero reports are still listed. An empty result returns `200` with `data: []` and the full `category_counts` block.

---

## 3. Categories (tab list)

Reuses the back-office `ResearchCategoryController::index`, so the client tabs are always in sync with what the back office uploads against.

```bash
curl --location --request GET "http://localhost/fintraBackend/fi_adm/public/api/client-research/categories" \
  --header "Accept: application/json" \
  --header "Origin: http://localhost:3000"
```

Optional pagination (defaults to 10 per page, sorted by name):

```bash
curl --location --request GET "http://localhost/fintraBackend/fi_adm/public/api/client-research/categories?per_page=20" \
  --header "Accept: application/json" \
  --header "Origin: http://localhost:3000"
```

### Response (200)

```json
{
  "status": true,
  "data": [
    {
      "id": 1,
      "name": "Company Analysis",
      "logo": "<svg ...></svg>",
      "description": "Company level fundamental research.",
      "created_at": "2026-09-23T16:02:00.000000Z",
      "updated_at": "2026-09-23T16:02:00.000000Z"
    }
  ],
  "pagination": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 10,
    "total": 6
  }
}
```

This endpoint returns `404` with `status: false` and `"No research categories found"` when the table is empty — that is the existing back-office behaviour, kept as-is.

---

## Error responses

### Origin not allowed (403)

```json
{
  "status": false,
  "message": "Forbidden. Origin not allowed."
}
```

### Rate limit exceeded (429)

More than 60 requests per minute from the same client.

```json
{
  "message": "Too Many Attempts."
}
```

### Validation failed (422) — research-history only

```json
{
  "status": false,
  "message": "Validation failed",
  "errors": {
    "category": ["The selected research category is invalid."],
    "per_page": ["Per page must not exceed 100."]
  }
}
```

### Server error (500)

```json
{
  "status": false,
  "message": "Failed to fetch featured reports",
  "error": "..."
}
```

---

## Postman setup

1. Method: **GET**
2. No Authorization tab needed.
3. Headers: `Accept: application/json` and `Origin: <an allowed origin>` (Postman does not send `Origin` automatically, so add it manually or the request gets a 403 once the allowlist is set).
4. Params per endpoint:

| Endpoint | Params |
|---|---|
| `client-research/featured-reports` | `limit` |
| `client-research/research-history` | `category`, `search`, `per_page`, `page` |
| `client-research/categories` | `per_page`, `page` |

---

## Fields the client mock shows that the API cannot return

* **Read time** ("14 min", "12 Min") has no column on `research_reports`. It needs either a new column or client-side estimation from `en_json`.
* **Available Languages** needs no new field — derive the flags from `pdf_previews.english_pdf_url` and `bangla_pdf_url` being non-null.
* The Economy / Industry / Ticker / Regulatory labels in the mock are design placeholders. Real tabs come from `client-research/categories`.
