# NAN-477 PeriodSelector visibility probe — 2026-05-21

## Step 1 — Backend endpoints

### Projections endpoint
- Path: `GET /stocks/{ticker}/detail/projections`
- Handler: `backend/services/main/app/routers/_market/stock_analyst.py:362-511`
- Response model: `ProjectionsResponse` at `stock_analyst.py:300-310`
  - `projections: List[QuarterlyProjection]` — quarterly data (`stock_analyst.py:304-305`)
  - `annual_projections: List[AnnualConsensusProjection]` — annual data (`stock_analyst.py:307-309`)
  - Both periods present in this endpoint's response

### Analyst Forecasts endpoint (what mobile actually calls)
- Path: `GET /market/stocks/{ticker}/detail/analyst-forecasts`
- Handler: `backend/services/main/app/routers/_market/stock_analyst.py:65-359`
- Response model: `AnalystForecastsResponse` at `stock_analyst.py:193-214`
  - `annual_projections: List[AnnualProjection]` only — **no quarterly field** (`stock_analyst.py:209-210`)

### Key model types
- `AnnualProjection` — `stock_analyst.py:146-171` — `year`, `eps`, `revenue`, etc.
- `QuarterlyProjection` — `stock_analyst.py:261-276` — `fiscal_year`, `fiscal_period`, `date`
- Data source: FMP `/analyst-estimates` API via `backend/services/main/app/repositories/fmp/analyst.py:175-183`; quarterly can be empty when ticker has no analyst quarterly coverage

---

## Step 2 — API reachability

- Base URL found: `https://pure-heuristic-488414-s6.as.r.appspot.com` (from `.env.local`)
- Auth credentials not available in worktree — live curl not attempted
- Proceeding with static code trace (Step 3)

---

## Step 3 — Backend quarterly gate

- Service: `backend/services/main/app/repositories/fmp/analyst.py:175-183`
- Data source: FMP external API; `period` parameter selects annual vs quarterly
- Quarterly gate: FMP returns an empty list when a ticker has no analyst quarterly coverage — no explicit mobile-facing gate; data absence drives it
- The `/analyst-forecasts` endpoint never requests quarterly from FMP regardless — it only fetches and returns `annual_projections`

---

## Step 4 — Mobile: ProjectionMetricCard

- Component: `src/features/stock-detail/presentation/components/analyst-forecasts/ProjectionMetricCard.tsx`
- `availablePeriods` derivation at line 218:
  ```
  ALL_PERIOD_OPTIONS.filter((opt) => projections.some((p) => p.periodType === opt.id))
  ```
  Filters to period IDs that have at least one matching entry in the `projections` array.
- `ALL_PERIOD_OPTIONS` (line 23-26): `[{ id: 'annual' }, { id: 'quarterly' }]`
- Selector hidden when `options.length <= 1` at `PeriodDropdown` line 67: `if (options.length <= 1) return null`

### Data chain
- Hook: feeds `projections: readonly Projection[]` prop to the card
- DTO: `src/features/stock-detail/data/dto/analystForecastsDto.ts:64-89` — only defines `annual_projections`; `period_type` defaults to `'annual'` at `dto:67`
- Mapper: `src/features/stock-detail/data/mappers/analystForecastsMapper.ts:138-152` — maps only `dto.annual_projections`, all marked `periodType: 'annual'`; mapper line 172:
  ```
  projections: sortByYearAsc(dto.annual_projections.map(mapProjection))
  ```
- Domain model: `src/features/stock-detail/business/models/AnalystForecasts.ts:75-100` — `Projection.periodType: ProjectionPeriodType` (`'annual' | 'quarterly'`)

### Behavior when annual-only
`availablePeriods` → `[{ id: 'annual', label: 'Annually' }]` — length 1 → selector returns `null`. Correct per conditional.

---

## Step 5 — Conclusion

**The selector being hidden is correct given current data, but the root issue is an endpoint mismatch — not a mobile-side filtering bug.**

The mobile app calls `/market/stocks/{ticker}/detail/analyst-forecasts`, which the backend intentionally returns as annual-only (`AnalystForecastsResponse` has no quarterly field). The DTO and mapper faithfully reflect that — every projection gets `periodType: 'annual'`, `availablePeriods` has length 1, and the selector hides itself correctly.

The separate `/stocks/{ticker}/detail/projections` endpoint does return both annual AND quarterly (`ProjectionsResponse` has both lists), but mobile never calls it.

**This is an integration gap, not a mobile UI bug.** The fix decision is architectural:

- Option A: Wire mobile to also call `/stocks/{ticker}/detail/projections` and merge quarterly into the `projections` array alongside annual — selector becomes visible when quarterly data exists for the ticker.
- Option B: Extend `/market/stocks/{ticker}/detail/analyst-forecasts` backend response to include quarterly projections alongside annual.

Either option requires a backend or DTO change; `ProjectionMetricCard.tsx` and the `periodLabels.length > 1` conditional are correct as-is and need no change.
