feat(F-146): completed feature
This commit is contained in:
109
work/artifacts/F-146/architect.md
Normal file
109
work/artifacts/F-146/architect.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# F-146 — Architect
|
||||
|
||||
## Feature
|
||||
Reporting: service summary and sales API.
|
||||
|
||||
## Background
|
||||
F-143 set up the RBAC and filter contracts. F-144 added store_id/shipping_cents snapshots. F-145 added payment lines. F-146 wires up the ReportingService that reads the actual data using those columns.
|
||||
|
||||
## Objetivo
|
||||
Implementar ReportingService con métodos `summary()` y `sales()` que:
|
||||
- Usan el CTE `filtered_orders` del architecture doc §7
|
||||
- Aplican todos los filtros del schema de F-143
|
||||
- Devuelven métricas con `dataAvailability` correcto
|
||||
- Soportan `groupBy`, `pagination` y `compare`
|
||||
|
||||
## Diseño
|
||||
|
||||
### ReportingService (application layer)
|
||||
|
||||
```typescript
|
||||
// src/modules/reporting/application/reporting-service.ts
|
||||
|
||||
interface ReportingServiceDeps {
|
||||
pool: pg.Pool;
|
||||
}
|
||||
|
||||
class ReportingService {
|
||||
constructor(private deps: ReportingServiceDeps) {}
|
||||
|
||||
async summary(filters: ReportingFilters): Promise<SummaryResponse>
|
||||
async sales(filters: ReportingFilters): Promise<SalesResponse>
|
||||
}
|
||||
```
|
||||
|
||||
### SQL base (CTE de §7 arquitectura)
|
||||
|
||||
```sql
|
||||
WITH filtered_orders AS (
|
||||
SELECT o.*
|
||||
FROM orders_orders o
|
||||
WHERE o.created_at >= $1 -- from (UTC)
|
||||
AND o.created_at < $2 -- to (UTC)
|
||||
AND o.state IN ('PAID','PROCESSING','SHIPPED','DELIVERED','COMPLETED')
|
||||
AND ($3::text IS NULL OR o.source = $3) -- channel
|
||||
AND (cardinality($4::uuid[]) = 0 OR o.store_id = ANY($4::uuid[]))
|
||||
AND (cardinality($5::uuid[]) = 0 OR o.terminal_id = ANY($5::uuid[]))
|
||||
AND (cardinality($6::text[]) = 0 OR o.state = ANY($6::text[]))
|
||||
),
|
||||
filtered_items AS (
|
||||
SELECT i.*, o.source, o.store_id, o.terminal_id
|
||||
FROM orders_items i
|
||||
JOIN filtered_orders o ON o.id = i.order_id
|
||||
)
|
||||
SELECT
|
||||
COUNT(DISTINCT o.id)::int AS orders,
|
||||
COUNT(DISTINCT o.user_id) FILTER (WHERE o.user_id IS NOT NULL)::int AS customers,
|
||||
COALESCE(SUM(i.unit_price_cents * i.quantity), 0)::bigint AS gross_sales_cents,
|
||||
COALESCE(SUM(i.discount_cents), 0)::bigint AS discounts_cents,
|
||||
COALESCE(SUM(i.tax_cents), 0)::bigint AS tax_cents,
|
||||
COALESCE(SUM(i.quantity), 0)::int AS units_sold
|
||||
FROM filtered_orders o
|
||||
LEFT JOIN filtered_items i ON i.order_id = o.id
|
||||
```
|
||||
|
||||
### groupBy extension (sales endpoint)
|
||||
|
||||
```sql
|
||||
-- Para groupBy=day (agrupación por día UTC)
|
||||
DATE_TRUNC('day', o.created_at) AS period
|
||||
```
|
||||
|
||||
### dataAvailability (según modelo actual)
|
||||
|
||||
```typescript
|
||||
const dataAvailability: DataAvailability = {
|
||||
grossSales: 'available',
|
||||
netSales: 'unavailable', // sin shipping_cents aún (F-144 existe, service aún no lo usa)
|
||||
discounts: 'available',
|
||||
tax: 'available',
|
||||
unitsSold: 'available',
|
||||
orders: 'available',
|
||||
customers: 'available',
|
||||
margin: 'unavailable', // sin cost_at_sale en orders_items
|
||||
paymentMethod: 'unavailable', // sin JOIN con reporting_payment_lines aún
|
||||
refunds: 'unavailable', // necesita state=REFUNDED/PARTIALLY_REFUNDED en filtro
|
||||
shipping: 'available', // shipping_cents en orders_orders (F-144)
|
||||
};
|
||||
```
|
||||
|
||||
### Rutas
|
||||
|
||||
- `GET /reporting/summary?from=&to=&channel=&storeId=&...` → SummaryResponse
|
||||
- `GET /reporting/sales?from=&to=&groupBy=day&page=1&...` → SalesResponse (con pagination)
|
||||
|
||||
### Caching
|
||||
|
||||
F-146 NO implementa caché (postergado a F-148 post-dashboard). La respuesta incluye `updatedAt: new Date().toISOString()` y `cache: { hit: false, maxAgeSeconds: 0 }` como placeholder.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
AC1: GET /reporting/summary devuelve métricas agregadas (orders, customers, grossSales, discounts, tax, units) con rango filtrado.
|
||||
AC2: GET /reporting/sales devuelve filas agrupadas por día/semana/mes/hora/store/channel.
|
||||
AC3: Filtros storeId/terminalId/state/channel se aplican correctamente.
|
||||
AC4: Paginación funciona (page/pageSize).
|
||||
AC5: dataAvailability refleja correctamente qué métricas son calculables.
|
||||
AC6: Compare previous_equal devuelve rango previo del mismo tamaño.
|
||||
AC7: Errores de validación (fechas invertidas, IDs inválidos) devuelven 400.
|
||||
AC8: Permisos RBAC aplicados (REPORTING_VIEW para summary, REPORTING_SALES para sales).
|
||||
AC9: tsc 0, itest pasa, verify.sh verde.
|
||||
Reference in New Issue
Block a user