# api-design.md

## Routing model

Custom router (`app/Http/Router.php`) — no framework. Pattern-matched routes with `:id`-style path params, dispatched to controller class + method.

```
HTTP request → public/index.php (front controller)
            → autoloader (custom PSR-4)
            → Router::dispatch()
            → AuthMiddleware (if route needs auth)
            → Controller method
            → Repository / Service
            → MySQL
            → JSON or rendered PHP view
```

---

## Public endpoints (rendered HTML)

| Method | Path | Controller | Purpose |
|---|---|---|---|
| GET  | `/login`               | AuthController.showLogin     | Login form |
| POST | `/login`               | AuthController.login         | Submit credentials |
| GET  | `/logout`              | AuthController.logout        | Destroy session |
| GET  | `/` or `/dashboard`    | (inline)                     | Dashboard landing |
| GET  | `/clients`             | ClientController.index       | Client list |
| GET  | `/clients/create`      | ClientController.create      | Create form (UK GDPR consent required) |
| POST | `/clients/create`      | ClientController.create      | Persist client |
| GET  | `/clients/:id`         | ClientController.show        | Client detail |

## Scenario routes

| Method | Path | Controller | Purpose |
|---|---|---|---|
| GET  | `/clients/:client_id/scenarios/create`  | ScenarioController.create | New scenario form |
| POST | `/clients/:client_id/scenarios/create`  | ScenarioController.create | Persist scenario + assets |
| GET  | `/scenarios/:id`                        | ScenarioController.show   | Scenario detail + run button |
| GET  | `/scenarios/:id/edit`                   | ScenarioController.edit   | Edit form |
| POST | `/scenarios/:id/edit`                   | ScenarioController.edit   | Persist updates |
| GET  | `/scenarios/:id/strategy` *(Phase 2)*   | StrategyController.compare| Strategy comparator |

## Simulation API

| Method | Path | Controller | Returns |
|---|---|---|---|
| POST | `/simulate/:id`        | SimulationController.run       | **JSON** payload (summary, charts, run_id) |
| GET  | `/runs/:id/pdf`        | SimulationController.exportPdf | HTML report (PDF via mPDF or client-side) |
| GET  | `/runs/:id/csv`        | SimulationController.exportCsv | CSV stream |

### Sample `/simulate/:id` response

```json
{
  "run_id": 42,
  "summary": {
    "success_rate": 78.4,
    "median_years_left": 23,
    "worst_depletion_year": 18,
    "verdict": "Moderate probability — consider reducing withdrawals...",
    "withdrawal_amount": 28295.42,
    "inflation_rate": 0.025,
    "estimated_tax": 3295.42,
    "net_withdrawal": 25000.00,
    "state_pension_income": 11502.40,
    "tax_year": 2025
  },
  "charts": {
    "portfolio_paths": {
      "labels": [1, 2, ..., 25],
      "datasets": [
        { "label": "Best case (P90)", "data": [...], "borderColor": "#27AE60" },
        { "label": "Median (P50)",    "data": [...], "borderColor": "#2980B9", "borderWidth": 3 },
        { "label": "Worst case (P10)","data": [...], "borderColor": "#E74C3C" }
      ]
    },
    "confidence_bands": [
      { "year": 1, "p10": ..., "p25": ..., "p50": ..., "p75": ..., "p90": ... }
    ]
  },
  "meta": {
    "scenario_name": "SIPP + State Pension retirement 2026",
    "risk_profile": "moderate",
    "client_age": 60,
    "life_expectancy": 87,
    "generated_at": "2026-05-27T10:00:00+01:00"
  }
}
```

**Note:** `state_pension_income` is the annual State Pension income already subtracted from the
target before portfolio drawdown. It appears in the response for display in the PDF report.

---

## Fund Search API

| Method | Path | Controller | Returns |
|---|---|---|---|
| GET | `/api/funds/search?q=...`           | FundController.search       | JSON array of matching funds |
| GET | `/api/funds/:id/performance`        | FundController.performance  | JSON of annual returns |

### Sample search result

```json
[
  {
    "id": 17,
    "isin": "IE00B3RBWM25",
    "sedol": "B3RBWM2",
    "name": "Vanguard FTSE All-World UCITS ETF",
    "fund_manager": "Vanguard",
    "asisa_category": "IA Global",
    "fund_type": "etf",
    "ter": 0.0022,
    "risk_rating": "high",
    "is_verified": true
  }
]
```

---

## Admin endpoints (gated by `funds.manage`)

| Method | Path | Controller | Purpose |
|---|---|---|---|
| GET  | `/admin/funds`                       | AdminFundController.index             | Paginated fund list |
| GET  | `/admin/funds/runs`                  | AdminFundController.runs              | Ingest run history |
| GET  | `/admin/funds/import-performance`    | AdminFundController.importPerformance | Upload form |
| POST | `/admin/funds/import-performance`    | AdminFundController.importPerformance | CSV upload handler |
| GET  | `/admin/funds/:id/edit`              | AdminFundController.edit              | Fund edit form |
| POST | `/admin/funds/:id/edit`              | AdminFundController.edit              | Persist fund |
| GET  | `/admin/funds/:id/performance`       | AdminFundController.performance       | Per-fund performance |
| POST | `/admin/funds/:id/performance`       | AdminFundController.performance       | Persist performance |

---

## Billing endpoints (Stripe)

| Method | Path | Controller | Purpose |
|---|---|---|---|
| GET  | `/billing/plans`     | BillingController.plans     | Plan selection (prices in GBP) |
| POST | `/billing/checkout`  | BillingController.checkout  | Start Stripe Checkout session |
| POST | `/billing/notify`    | BillingController.notify    | Stripe webhook handler (verify `Stripe-Signature`) |
| GET  | `/billing/success`   | BillingController.success   | Post-payment landing |
| GET  | `/billing/cancel`    | (inline)                    | Cancelled landing |

---

## Data Flow (canonical example)

```
1. User clicks "Run Monte Carlo" on /scenarios/42
2. JS: POST /simulate/42, body: runs=500
3. Router → SimulationController::run
4. AuthMiddleware enforces simulation.run permission
5. UsageQuota::canRunSimulation() check
6. ScenarioRepository::find(42) loads scenario + assets (tenant-scoped)
7. SimulationEngine::default(500, $taxCalc, $taxYear)->run($scenario)
   — StatePensionAsset::annualIncome() subtracted from target each year
   — SippAsset draws from taxable 75% fraction; 25% PCLS modelled at first draw
8. CreditService::deduct() if Free tier
9. SimulationRunRepository::save() persists the result
10. AuditLog::record('simulation.run', ...)
11. EventDispatcher::dispatch(new SimulationCompleted(...))
12. DashboardReport::generate() shapes JSON (GBP amounts, en-GB locale)
13. echo json_encode($report)
14. JS renderResults(data) updates the dashboard
```

---

## Admin Portal endpoints (superadmin — `AdminAuthMiddleware`)

### Auth
| Method | Path | Controller | Purpose |
|---|---|---|---|
| GET  | `/admin/login`  | AdminAuthController.showLogin | Admin login form |
| POST | `/admin/login`  | AdminAuthController.login     | Submit credentials |
| GET  | `/admin/logout` | AdminAuthController.logout    | Destroy admin session |
| GET  | `/admin`        | AdminDashboardController.index | Dashboard |

### Users (global)
| Method | Path | Controller | Purpose |
|---|---|---|---|
| GET  | `/admin/users`              | AdminUserController.index  | All users across all tenants |
| GET  | `/admin/users/create`       | AdminUserController.create | Create form |
| POST | `/admin/users/create`       | AdminUserController.create | Persist user |
| GET  | `/admin/users/:id/edit`     | AdminUserController.edit   | Edit form |
| POST | `/admin/users/:id/edit`     | AdminUserController.edit   | Persist update |
| POST | `/admin/users/:id/delete`   | AdminUserController.delete | Hard delete (superadmin protected) |

### Tenants
| Method | Path | Controller | Purpose |
|---|---|---|---|
| GET  | `/admin/tenants`                        | AdminTenantController.index      | Tenant list |
| GET  | `/admin/tenants/create`                 | AdminTenantController.create     | Create form |
| POST | `/admin/tenants/create`                 | AdminTenantController.create     | Persist tenant |
| GET  | `/admin/tenants/:id`                    | AdminTenantController.show       | Tenant detail + users |
| GET  | `/admin/tenants/:id/edit`               | AdminTenantController.edit       | Edit form |
| POST | `/admin/tenants/:id/edit`               | AdminTenantController.edit       | Persist update |
| POST | `/admin/tenants/:id/status`             | AdminTenantController.updateStatus | Toggle status |
| POST | `/admin/tenants/:id/users`              | AdminTenantController.addUser    | Add user to tenant |
| GET  | `/admin/tenants/:id/users/:uid/edit`    | AdminTenantController.editUser   | Edit tenant user |
| POST | `/admin/tenants/:id/users/:uid/edit`    | AdminTenantController.editUser   | Persist update |
| POST | `/admin/tenants/:id/users/:uid/delete`  | AdminTenantController.deleteUser | Remove user from tenant |
| POST | `/admin/tenants/:id/delete`             | AdminTenantController.delete     | Delete tenant + cascade (name-confirmation guard) |

### Funds Library (platform)
| Method | Path | Controller | Purpose |
|---|---|---|---|
| GET  | `/admin/platform/funds`                 | AdminPlatformFundController.index       | Paginated fund list |
| GET  | `/admin/platform/funds/create`          | AdminPlatformFundController.create      | Create form |
| POST | `/admin/platform/funds/create`          | AdminPlatformFundController.create      | Persist + redirect to performance |
| GET  | `/admin/platform/funds/:id/performance` | AdminPlatformFundController.performance | Annual return editor |
| GET  | `/admin/platform/funds/export`          | AdminPlatformFundController.exportCsv   | Full library CSV download (SEDOL column) |
| POST | `/admin/platform/funds/import`          | AdminPlatformFundController.importCsv   | Bulk CSV upload (upsert on ISIN) |

### OpenFIGI lookup (admin API)
| Method | Path | Controller | Returns |
|---|---|---|---|
| GET | `/admin/api/figi?isin=IE00B3RBWM25` | AdminFigiController.lookup | JSON — fund metadata from OpenFIGI |
| GET | `/admin/api/figi?ticker=B3RBWM2`    | AdminFigiController.lookup | JSON — fund metadata from OpenFIGI (SEDOL lookup) |

Sample response:
```json
{
  "figi": "BBG000BVPXP2",
  "name": "VANGUARD FTSE ALL-WORLD UCITS ETF",
  "ticker": "VWRL",
  "exchCode": "LN",
  "securityType": "ETP",
  "securityType2": "ETF",
  "marketSector": "Equity"
}
```
Error responses: `503` if `OPENFIGI_API_KEY` not set; `404` if no match; `400` if no identifier supplied.

### Other admin endpoints
| Method | Path | Controller | Purpose |
|---|---|---|---|
| GET/POST | `/admin/billing/credits`     | AdminBillingController   | Manual credit top-up |
| GET/POST | `/admin/market-data`         | AdminMarketDataController | Annual UK market data (FTSE/Gilts/SONIA/CPI) |
| GET      | `/admin/market-data/export`  | AdminMarketDataController | CSV export |
| POST     | `/admin/market-data/import`  | AdminMarketDataController | CSV import |
| GET/POST | `/admin/tax-brackets`        | AdminTaxBracketController | HMRC Income Tax brackets |
| GET/POST | `/admin/fund-types`          | AdminFundTypeController   | Fund type management |
| POST     | `/admin/fund-types/:id/delete` | AdminFundTypeController | Delete (usage-guarded) |
| GET/POST | `/admin/funds/:id/edit`      | AdminFundController.edit  | Edit fund (tenant-level admin) |
| GET/POST | `/admin/funds/:id/performance` | AdminFundController.performance | Per-fund returns |

---

## Integration Points

| Integration | Purpose | Status |
|---|---|---|
| **Stripe** | Subscription billing + credit-pack purchase (GBP) | Implemented — `StripeBilling.php` stub; webhook handler present |
| **HMRC Income Tax brackets** | Tax calculation | Bracket data in DB (`uk_tax_2025.sql`), updated at each Budget |
| **UK State Pension rates** | State Pension income modelling | `state_pension_rates` table; `StatePensionAsset::fromQualifyingYears()` |
| **FTSE/BoE/ONS market data** | Annual equity/bond/cash/inflation figures | Seeded via `uk_market_data.sql` (2000–2024); CSV import for updates |
| **OpenFIGI** | Fund metadata lookup by ISIN / SEDOL | LIVE — `app/Integration/OpenFigiClient.php`; key via `OPENFIGI_API_KEY` env |
| **IA sector fund list** | Fund universe classification | CSV ingest; IA categories stored in `funds.asisa_category` column |
| **LSE-ETF / Vanguard-UK data** | Fund performance history | CSV upload via admin; `PerformanceCsvIngestor` |
