# F-143 — API contract documentation

## Resumen del cambio (user-facing)
New backoffice **Reporting** module delivering the shared filter contract + RBAC foundation (F-142 §3/§6/§9). No report data is read here (F-144+ owns report data); `GET /reporting/filters/schema` only introspects the contract. Two new endpoints, both backoffice-only (backoffice_session or storefront `mdv_session` via `combinedAuth`):

## Nuevas rutas
| Method | Path | Auth (authenticate) | Permission | Body / Query | 200 response |
|--------|------|---------------------|------------|--------------|--------------|
| GET | `/reporting/filters/schema` | `combinedAuth` (authenticated) | `REPORTING_VIEW` | n/a | `{ filterSchema, comparison, dataAvailability, permissions }` |
| GET | `/reporting/filters/validate` | `combinedAuth` (authenticated) | `REPORTING_SALES` | `?from&to&compare?…` (see below) | `{ ok, filters, comparison: { range } }` |

## RBAC
Permissions are role-based (no permission table yet — F-142 §9 "compatibilidad inicial"):
- `admin` — all reporting permissions (incl. `REPORTING_FINANCIAL`, `REPORTING_EXPORT`).
- `editor` — VIEW + SALES + PRODUCTS + CUSTOMERS + INVENTORY + DISCOUNTS + REFUNDS + TAXES.
- `pos_manager` — VIEW + SALES + PAYMENTS + CASH.
- `pos_cashier` — VIEW + SALES.
- `customer` — none (403 on both routes).

A future `backoffice_permissions` migration keeps the `requireReportingPermission(user, perm)` call-site signature.

## Filter schema (shared contract)
Query params parsed by `reportingFiltersSchema` via `parseJson` (→ 400 `VALIDATION_ERROR` on bad input):

- `from` *(string, required)* — ISO 8601 datetime, **inclusive** start.
- `to` *(string, required)* — ISO 8601 datetime, **exclusive** end. `from < to` enforced (400 on inversion).
- `compare` — enum `none | previous_equal | previous_calendar` (default `none`).
- `channel` — enum `all | ecommerce | pos | admin` (default `all`).
- `storeId`, `terminalId`, `cashierId`, `paymentMethodId`, `productId`, `categoryId`, `brandId` — repeatable UUID (single value or repeated).
- `customerId` — single UUID (optional; walk-in POS has none).
- `state` — repeatable string (e.g. `PAID,SHIPPED`).
- `groupBy` — enum `day|week|month|hour|store|channel|terminal|cashier|payment`.
- `page` *(int, default 1)*, `pageSize` *(int, 1..200, default 50)*.
- `sort` — string (e.g. `-revenue`).

### Comparison range (`comparisonRange`, `[from,to)`)
- `none` → `comparison.range = null`.
- `previous_equal` → exact-duration mirror; prior window ends exactly at `range.from`.
- `previous_calendar` → UTC-aligned prior window of `ceil(span_days)` days.

## `dataAvailability` (metadata only — F-142 §4 baseline)
Never converted to 0. Currently:
- `available`: `grossSales`, `discounts`, `tax`, `unitsSold`, `orders`, `customers`.
- `unavailable`: `netSales`, `margin`, `paymentMethod`, `refunds`, `shipping`.

## Errores
- `401` — unauthenticated (`authenticate` rejects).
- `403` — authenticated but lacks the route permission.
- `400` — malformed query (zod → `VALIDATION_ERROR`), incluyendo `from >= to` e UUIDs inválidos.

## Implementación
- `project/src/modules/reporting/` (domain/application/api + tests + index).
- `project/src/app/build-app.ts` — `registerReportingRoutes` registered inside `if (deps.pool)` with `combinedAuth`.
No migrations, no DB schema or data-table changes in this ticket.
