# Dashboard analytics API

Five read-only endpoints under `/v1/admin/analytics/*` that power the Navi
operations console. They replace the placeholder counters with real,
permission-gated reporting backed by Prisma and the live database.

All responses are JSON. All times are UTC. All money is returned as `Money`
(`{ amountMinor, currencyCode }`) using `AED` as the platform default.

## Authentication and authorization

* Every endpoint requires a Bearer token resolved by the `RbacGuard`.
* `SUPER_ADMIN` (`*` wildcard) always passes.
* `ADMIN` is admitted via `admin.report.read` (also accepted:
  `admin.audit.read`, `audit.read.all`).
* `SUPPORT_AGENT` is admitted **only** on `/admin/analytics/providers` via
  the narrow `provider_integration.health.read` permission. All other
  analytics endpoints reject support sessions.
* `PARTNER_*` roles are explicitly **not** allowed. `*.read.assigned` and
  `*.read.own` are not in the accepted permission lists for global
  analytics.

The accepted permission lists are owned by **`@navi/config`** as
`ANALYTICS_GLOBAL_READ_PERMISSIONS` and
`ANALYTICS_OPERATIONAL_READ_PERMISSIONS`. The API imports them in
`apps/api/src/modules/analytics/analytics.module.ts`; the dashboard imports
them from the same shared package for UI visibility checks. Never import
permission constants from `apps/api` into another app — apps depend on
the shared packages, not the other way around.

## Range parameters

All endpoints (except `/providers`) accept the same trailing-window query
shape, validated by `@navi/validators`:

| Field         | Type      | Notes                                                  |
| ------------- | --------- | ------------------------------------------------------ |
| `from`        | ISO 8601  | Optional. Defaults to `to - 30d`.                      |
| `to`          | ISO 8601  | Optional. Defaults to `now`.                           |
| `granularity` | `day\|week` | Optional. Defaults to `day`. Echoed back in `range`. |

Validation rules:
* `from` must be on or before `to`.
* Window cannot exceed `92` days (`ANALYTICS_MAX_RANGE_DAYS`).
* Unknown query keys are rejected (strict schema).

## Endpoints

### GET /v1/admin/analytics/overview

Top-line counters for the dashboard hero. Field reference:

| Field                          | Source                                                            |
| ------------------------------ | ----------------------------------------------------------------- |
| `metrics.activeUsers`          | `User where status = ACTIVE`                                      |
| `metrics.bookingsThisWeek`     | `Booking where createdAt >= startOfWeek(UTC)`                     |
| `metrics.publishedListings`    | `Listing where status = PUBLISHED`                                |
| `metrics.pendingPartnerReviews`| `Business where status = PENDING_REVIEW`                          |
| `metrics.paymentIntentCount`   | `PaymentIntent` count over the requested range                    |
| `metrics.paymentIntentTotal`   | `SUM(PaymentIntent.amountMinor)` over the range, as `Money`       |
| `metrics.requestedRefundCount` | `Refund where status = REQUESTED`                                 |
| `metrics.liveProviderCount`    | `ProviderIntegration where environment = PRODUCTION AND enabled AND liveReady` |
| `metrics.providerErrorCount`   | `ProviderIntegration where healthStatus IN (DOWN, DEGRADED)`      |

### GET /v1/admin/analytics/bookings

| Field         | Source                                                        |
| ------------- | ------------------------------------------------------------- |
| `byStatus`    | `groupBy Booking.status` (zero-filled across all statuses).   |
| `byCategory`  | `groupBy Booking.kind` (`STAY`, `ACTIVITY`, `TAXI_RIDE`).     |
| `byDay`       | `date_trunc('day', createdAt)` time-series within the range.  |
| `recent`      | 10 most-recent bookings (id, kind, status, total, createdAt). |

### GET /v1/admin/analytics/revenue

| Field                  | Source                                                                         |
| ---------------------- | ------------------------------------------------------------------------------ |
| `paymentTotalsByDay`   | Per-day `SUM(amountMinor) FILTER (WHERE status='CAPTURED')` and total.         |
| `paymentStatusBreakdown` | `groupBy PaymentIntent.status` (zero-filled).                                |
| `refunds.totalMinor`   | `SUM(Refund.amountMinor)` over the range.                                      |
| `refunds.count`        | `Refund` count over the range.                                                 |
| `refunds.byStatus`     | `groupBy Refund.status` (zero-filled).                                         |
| `commissionEstimateMinor` | `null` until a commission model is introduced. The dashboard should render a "Commission reporting pending" placeholder. |

### GET /v1/admin/analytics/finance-report

Finance operations report for the dashboard Reports page.

| Field | Source |
| --- | --- |
| `payments.*` | `PaymentIntent` aggregates, status breakdown, captured/authorized/failed totals. |
| `refunds.*` | `Refund` aggregates, requested/approved/processed totals, status breakdown. |
| `payouts.*` | `Payout` aggregates for payout periods overlapping the selected range. |
| `commission.*` | Provider integration commission-readiness counts. `estimatedMinor` remains `null` until a commission rate/ledger model exists. |
| `risk.*` | Unpaid bookings, failed payments, requested refunds, pending payouts, and production payment provider count. |

This endpoint is intentionally honest about commission maturity. It reports readiness and risk from real records, but it does not invent earned commission.

### GET /v1/admin/analytics/providers

Operational-health surface. Accepts an optional `?environment=DEMO|SANDBOX|PRODUCTION`
filter for the byEnvironment / health summaries.

| Field                | Source                                                              |
| -------------------- | ------------------------------------------------------------------- |
| `totalProviders`     | `ProviderIntegration` count (filtered if environment provided).     |
| `byMode`             | `groupBy Category.operatingMode` (zero-filled across modes).        |
| `byEnvironment`      | `groupBy ProviderIntegration.environment`.                          |
| `health.summary`     | `groupBy ProviderIntegration.healthStatus`.                         |
| `health.liveCount`   | `environment = PRODUCTION AND enabled AND liveReady`.               |
| `health.errorCount`  | `healthStatus IN (DOWN, DEGRADED)`.                                 |
| `failedSyncs`        | Up to 20 integrations with `DOWN`/`DEGRADED` health or non-null `errorMessage`, ordered by `lastHealthCheckAt DESC`. |

### GET /v1/admin/analytics/customer-behavior

Demand and conversion. Sourced from `EngagementEvent` (and `Booking` for
the conversion denominator).

| Field                 | Source                                                        |
| --------------------- | ------------------------------------------------------------- |
| `searches.total`      | `EngagementEvent.eventType = 'search.performed'`.             |
| `searches.topQueries` | `groupBy EngagementEvent.query`, top 10 by count.             |
| `listingViews`        | `EngagementEvent.eventType = 'listing.viewed'`.               |
| `saves`               | `EngagementEvent.eventType = 'saved.created'`.                |
| `bookingAttempts`     | `Booking` count over the range (proxy for booking attempts).  |
| `conversionRate`      | `bookingAttempts / max(searches + listingViews, 1)`, rounded to 4 decimal places. |
| `topCategories`       | `groupBy Booking.kind`, top 10.                               |

## Audit posture

Reading analytics does not write an `AuditLog` row by default — the
`AuditInterceptor` is only triggered by handlers tagged with `@Audited()`.
Future export endpoints (CSV/PDF for the CEO demo deck) **must** be tagged
because they are sensitive snapshots of platform health.

## Failure modes

| Status | Cause                                                  |
| ------ | ------------------------------------------------------ |
| 400    | `from > to`, range exceeds 92 days, unknown query key, malformed ISO datetime. |
| 401    | Missing or invalid Bearer token.                       |
| 403    | Authenticated session lacks any accepted permission.   |
| 500    | Unhandled service error (Prisma raw query failure, etc.). Surface as `ProblemDetails`. |
