feat(F-144): completed feature
This commit is contained in:
@@ -1,40 +1,39 @@
|
||||
# F-143 — Acceptance
|
||||
# F-144 — Criterios de aceptación
|
||||
|
||||
### AC1 — contrato de filtros
|
||||
- `GET /reporting/filters/schema` (admin) → 200 → `{ filterSchema, comparison, dataAvailability, permissions }`.
|
||||
- `comparison.modes` incluye `none`, `previous_equal`, `previous_calendar`.
|
||||
- `comparison.rangeBounds === 'inclusive_start_exclusive_end'`.
|
||||
- `dataAvailability` refleja el baseline F-142 (grossSales/discounts/tax/unitsSold/orders/customers `available`; netSales/margin/paymentMethod/refunds/shipping `unavailable`).
|
||||
- `filterSchema.filters` incluye `storeId` (repeatable, uuid), `compare`, `channel`, `groupBy`, `page`, `pageSize`.
|
||||
## AC1 — store_id multi-tienda (snapshot, no rewrite)
|
||||
- `orders_orders.store_id` es `uuid NOT NULL DEFAULT '00000000-0000-0000-0000-000000000001'::uuid`.
|
||||
- FK `orders_orders_store_id_fkey → pos_stores(id)` existe y es `VALID`.
|
||||
- Índice `orders_orders_store_id_created_at_idx ON orders_orders(store_id, created_at)` existe.
|
||||
- Filas existentes heredan el default store (no table rewrite de datos).
|
||||
- Un `INSERT INTO orders_orders DEFAULT VALUES` persiste `store_id = DEFAULT_STORE_ID`.
|
||||
|
||||
### AC2 — RBAC (backend-authority)
|
||||
- `customer` (role customer) → 403 en `/reporting/filters/schema` y `/reporting/filters/validate`.
|
||||
- `admin` y `editor` → 200 en `/reporting/filters/schema` (tienen `REPORTING_VIEW`).
|
||||
- `admin`/`editor`/`pos_manager`/`pos_cashier` → 200 en `/reporting/filters/validate` (tienen `REPORTING_SALES`).
|
||||
- `customer` NO aparece en `REPORTING_ROLE_PERMISSIONS` con permisos.
|
||||
## AC2 — shipping_cents separado
|
||||
- `orders_orders.shipping_cents` es `integer NOT NULL DEFAULT 0`.
|
||||
- Filas existentes mantienen su `total_cents` (no se altera; shipping_cents = 0).
|
||||
- El `INSERT ... DEFAULT VALUES` registra `shipping_cents = 0`.
|
||||
|
||||
### AC3 — parseo + rango `[from,to)`
|
||||
- `GET /reporting/filters/validate?from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z&compare=previous_equal` → 200 → `filters.range.from/to` normalizados; `comparison.range.from` < `filters.range.from` < `filters.range.to`; `comparison.range.to` === `filters.range.from`.
|
||||
- `pageSize` y `page` vienen por defecto (1 y 50) cuando no se pasan.
|
||||
## AC3 — snapshots de línea (cost_at_sale_cents, vat_rate)
|
||||
- `orders_items.cost_at_sale_cents` es `bigint`, nullable (NULL para filas históricas → margen `unavailable`).
|
||||
- `orders_items.vat_rate` es `text`, nullable (snapshot del tipo IVA aplicado; NULL → IVA-por-tipo `unavailable`).
|
||||
- No se reescribe la historia: columnas nuevas no tocan datos existentes.
|
||||
|
||||
### AC4 — validación
|
||||
- `from > to` → 400 (`VALIDATION_ERROR` / 400).
|
||||
- `?storeId=<single uuid>` parsea a array de 1 elemento; `?storeId=a&storeId=b` a array de 2.
|
||||
- UUID inválido → 400.
|
||||
## AC4 — migración idempotente y reversible
|
||||
- `up()` es no-op si se re-ejecuta (DDL `IF NOT EXISTS` / `DO $$` guards).
|
||||
- `down()` elimina índice, constraint y columnas nuevas.
|
||||
|
||||
### AC5 — comparison modes (unit)
|
||||
- `comparisonRange(range, 'none')` === `null`.
|
||||
- `previous_equal`: `to_prev === from_actual`, `from_prev === from_actual - duration`.
|
||||
- `previous_calendar`: ventana alineada a UTC, `to_prev <= from_actual`.
|
||||
## AC5 — itest de snapshots (DB real)
|
||||
- `reporting-snapshots.itest.ts` recrea la DB, migra (aplica 048), y verifica
|
||||
columnas, nullabilidad, defaults, FK (contype='f') e índice vía
|
||||
`information_schema`/`pg_constraint`/`pg_indexes`, más un insert
|
||||
`DEFAULT VALUES` con backfill de `store_id`/`shipping_cents`.
|
||||
|
||||
### AC6 — granularidad de permisos
|
||||
- `REPORTING_FINANCIAL` concedido solo a `admin` (editor/pos_manager/pos_cashier → 403).
|
||||
- `requireReportingPermission` lanza AppError(403) para roles sin el permiso.
|
||||
## AC6 — verificación de gates
|
||||
- `npx tsc --noEmit` → 0 errores.
|
||||
- `TEST_DATABASE_URL=... npx vitest run` → itest F-144 3/3 verde + sin regresiones
|
||||
(orders/catalog/checkout/sales/etc.) 0 fallos.
|
||||
- `node scripts/check-module-boundaries.mjs src` → 0 violaciones nuevas.
|
||||
|
||||
### AC7 — tests unitarios (sin DB)
|
||||
- `parseReportingFilters`: defaults, repeatable uuid arrays, from>to rechazado.
|
||||
- `comparisonRange`: 3 modos.
|
||||
- Matriz de permisos role→perms.
|
||||
- Tests: ≥8 unit + ≥6 route. `tsc --noEmit` 0 errores; `npm test` sin regresiones; `lint:boundaries` sin violaciones nuevas.
|
||||
|
||||
> `lint:boundaries` (scripts/check-module-boundaries.mjs) — reporting NO aparece todavía en la lista de módulos existentes; confirma que reporting importa solo `shared/*`/`zod`.
|
||||
## AC7 — sin regresión en flujos existentes
|
||||
- El seed y el checkout siguen funcionando: `orders_orders` `INSERT` sin
|
||||
`store_id` explícito sigue válido (column DEFAULT).
|
||||
- `orders.itest.ts`, `catalog.itest.ts`, `checkout-flow.itest.ts` siguen verdes.
|
||||
|
||||
Reference in New Issue
Block a user