# QA — Dashboard analytics API

Cross-functional checklist for `wave1/dashboard-analytics-api`. Walk top
to bottom before promoting to staging or wiring the dashboard against
these endpoints.

## Backend

- [ ] `pnpm --filter @navi/api typecheck` passes.
- [ ] `pnpm --filter @navi/api lint` passes.
- [ ] `pnpm --filter @navi/api test` passes, including
      `apps/api/test/analytics/analytics.spec.ts`.
- [ ] `pnpm --filter @navi/validators typecheck` passes (analytics range
      schema).
- [ ] `pnpm --filter @navi/types typecheck` passes (new analytics DTOs).
- [ ] `AnalyticsModule` is registered in `apps/api/src/app.module.ts`.
- [ ] No new Prisma migrations were introduced for this slice (analytics
      is read-only).

## Permissions matrix

For each endpoint hit `GET /v1/admin/analytics/<endpoint>` with the
following actor profiles and confirm the response code:

| Actor                          | overview | bookings | revenue | providers | customer-behavior |
| ------------------------------ | -------- | -------- | ------- | --------- | ----------------- |
| Anonymous                      | 401      | 401      | 401     | 401       | 401               |
| `REGISTERED` (no admin perms)  | 403      | 403      | 403     | 403       | 403               |
| `PARTNER_OWNER` (assigned)     | 403      | 403      | 403     | 403       | 403               |
| `SUPPORT_AGENT`                | 403      | 403      | 403     | **200**   | 403               |
| `FINANCE_MANAGER`              | 403      | 403      | 403     | 403       | 403               |
| `ADMIN`                        | 200      | 200      | 200     | 200       | 200               |
| `SUPER_ADMIN` (`*`)            | 200      | 200      | 200     | 200       | 200               |

## Validation

- [ ] `?from=...&to=...` — happy path returns 200 and echoes the range.
- [ ] `?from=2026-05-10&to=2026-05-01` — returns 400 with field-level error
      indicating `from` must precede `to`.
- [ ] `?from=2026-01-01&to=2026-05-01` (≈121 days) — returns 400 because
      it exceeds the 92-day cap.
- [ ] `?unexpected=1` — returns 400 (strict schema rejects unknown keys).
- [ ] `?from=not-a-date` — returns 400.
- [ ] Missing `from`/`to` — returns 200 using the default 30-day trailing
      window.

## Response shapes

For each endpoint, sanity-check the response with seeded data:

- [ ] `overview.metrics` includes all nine fields and money is wrapped
      as `{ amountMinor, currencyCode: "AED" }`.
- [ ] `bookings.byStatus` and `bookings.byCategory` are zero-filled across
      every enum value (no missing keys).
- [ ] `bookings.byDay` is sorted ascending by `date` and uses
      `YYYY-MM-DD`.
- [ ] `revenue.paymentTotalsByDay` reports both `capturedMinor` and
      `totalMinor` per day.
- [ ] `revenue.commissionEstimateMinor` is `null` (commission model not
      yet present).
- [ ] `providers.failedSyncs` is capped at 20 entries and ordered by
      `lastHealthCheckAt` descending.
- [ ] `customer-behavior.conversionRate` is in `[0, 1]` and is `0` when
      there are no searches or views (denominator floor of 1 prevents
      division by zero).

## Performance

- [ ] All endpoints complete in <500ms with a fresh seed (small data set).
- [ ] Indexes confirmed: `Booking @@index([userId, status])`,
      `Booking @@index([businessId, status])`, `Booking @@index([checkInAt])`,
      `EngagementEvent @@index([eventType, createdAt])`, and
      `ProviderIntegration @@index([healthStatus])`.
- [ ] No N+1 patterns introduced — `groupBy`, raw SQL, and aggregates only.

## Security

- [ ] `RbacGuard` enforces both `*` wildcard short-circuit and the
      explicit accepted-permission list.
- [ ] No PII fields (email, phone, full names) are returned by the
      analytics endpoints.
- [ ] No raw secret values or vault references appear in responses.
- [ ] Reads do not trigger `AuditInterceptor` (handlers are not tagged
      `@Audited()`).

## Dashboard integration (deferred)

The dashboard wiring lands in a follow-up branch. When it does:

- [ ] Loading states render before data arrives.
- [ ] Empty states render when buckets are zero (don't suppress the row).
- [ ] Date pickers default to the trailing 30-day window.
- [ ] Errors degrade to the placeholder hero so the rest of the dashboard
      keeps working.

## Rollback

- Revert the merge of `wave1/dashboard-analytics-api`. The change is
  additive (new module, new validators, new types, new doc files); no
  migrations to undo. The dashboard still uses the existing
  `/v1/dashboard/overview` placeholder until it is migrated.
