diff --git a/backlog/features.json b/backlog/features.json index e241467..0fa4683 100644 --- a/backlog/features.json +++ b/backlog/features.json @@ -6675,6 +6675,23 @@ "close": true }, "completed_at": "2026-08-22T15:35:43Z" + }, + { + "id": "F-157", + "type": "fix", + "title": "Admin Reporting sections live on Reporting page", + "description": "Keep only Reporting in main sidebar; clicking it shows Dashboard, Sales and Products section list inside Reporting page like Settings.", + "priority": "high", + "risk": "low", + "status": "done", + "created_at": "2026-08-22", + "gates": { + "reviewer": true, + "security": true, + "qa": true, + "close": true + }, + "completed_at": "2026-08-22T15:42:35Z" } ] } diff --git a/project/apps/admin/src/app/(dashboard)/layout.tsx b/project/apps/admin/src/app/(dashboard)/layout.tsx index a77d2ee..64192ae 100644 --- a/project/apps/admin/src/app/(dashboard)/layout.tsx +++ b/project/apps/admin/src/app/(dashboard)/layout.tsx @@ -1,13 +1,13 @@ 'use client'; import { useEffect } from 'react'; -import { useRouter } from 'next/navigation'; +import { usePathname, useRouter } from 'next/navigation'; import Link from 'next/link'; import { AuthProvider, useAuth } from '@/features/auth/components/AuthProvider'; -import { topNavItems, subNavItems, type NavItem } from '@/lib/permissions'; +import { visibleNavItems, type NavItem } from '@/lib/permissions'; import type { Role } from '@/types'; function NavItemRow({ item }: { item: NavItem }) { - const pathname = window?.location?.pathname ?? ''; + const pathname = usePathname(); const active = item.href === '/' ? pathname === '/' : pathname.startsWith(item.href); @@ -35,7 +35,7 @@ function NavItemRow({ item }: { item: NavItem }) { } function Sidebar({ role, email }: { role: Role; email: string }) { - const topItems = topNavItems(role); + const items = visibleNavItems(role); return (
@@ -50,22 +50,9 @@ function Sidebar({ role, email }: { role: Role; email: string }) { {/* Nav */} {/* User footer */} diff --git a/project/apps/admin/src/app/(dashboard)/reporting/dashboard/page.tsx b/project/apps/admin/src/app/(dashboard)/reporting/dashboard/page.tsx index 398517d..5d7cc1a 100644 --- a/project/apps/admin/src/app/(dashboard)/reporting/dashboard/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/reporting/dashboard/page.tsx @@ -295,12 +295,9 @@ function DashboardContent() { }; return ( -
+
{/* Header */}
-
- ← Reporting -

Dashboard de ventas

Tendencias, canales y rendimiento por tienda/terminal diff --git a/project/apps/admin/src/app/(dashboard)/reporting/layout.tsx b/project/apps/admin/src/app/(dashboard)/reporting/layout.tsx new file mode 100644 index 0000000..e7fbedf --- /dev/null +++ b/project/apps/admin/src/app/(dashboard)/reporting/layout.tsx @@ -0,0 +1,46 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +const SECTIONS = [ + { href: '/reporting/dashboard', label: 'Dashboard', icon: '📊' }, + { href: '/reporting/sales', label: 'Ventas', icon: '🧾' }, + { href: '/reporting/products', label: 'Productos', icon: '📦' }, +] as const; + +export default function ReportingLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + + return ( +

+
+

Reporting

+

Informes de ventas y rendimiento del negocio.

+
+ +
+ + +
{children}
+
+
+ ); +} diff --git a/project/apps/admin/src/app/(dashboard)/reporting/page.tsx b/project/apps/admin/src/app/(dashboard)/reporting/page.tsx index 6a53629..5988778 100644 --- a/project/apps/admin/src/app/(dashboard)/reporting/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/reporting/page.tsx @@ -1,299 +1,5 @@ -'use client'; - -/** - * F-147 — Reporting shell (main page). - * - * Layout: header + filter bar + KPI grid + data table. - * Filters are persisted in URL searchParams. - */ - -import { Suspense, useCallback, useEffect, useState } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { DateRangePicker, DATE_PRESETS } from '@/components/reporting/DateRangePicker'; -import { KpiCard } from '@/components/reporting/KpiCard'; -import { reportingClient, type Availability, type ReportingChannel, type SummaryResponse } from '@/lib/reporting-client'; - -function formatCents(cents: number) { - return `€${(cents / 100).toFixed(2)}`; -} - -function metricAvailability( - data: SummaryResponse | null, - metric: keyof SummaryResponse['dataAvailability'], -): Availability { - return data?.dataAvailability?.[metric] ?? 'unavailable'; -} - -function buildDefaultRange() { - const preset = DATE_PRESETS[1]; // "Últimos 30 días" - return preset.getValue(); -} - -interface FilterState { - from: string; - to: string; - channel: ReportingChannel; - compare: 'none' | 'previous_equal' | 'previous_calendar'; -} - -function ReportingContent() { - const router = useRouter(); - const searchParams = useSearchParams(); - - // Initialize filters from URL or defaults - const getInitialFilters = (): FilterState => { - const from = searchParams.get('from') ?? buildDefaultRange().from; - const to = searchParams.get('to') ?? buildDefaultRange().to; - const channel = (searchParams.get('channel') ?? 'all') as ReportingChannel; - const compare = (searchParams.get('compare') ?? 'none') as FilterState['compare']; - return { from, to, channel, compare }; - }; - - const [filters, setFilters] = useState(getInitialFilters); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - - // Sync filters to URL - const updateSearchParams = useCallback( - (newFilters: FilterState) => { - const params = new URLSearchParams(); - params.set('from', newFilters.from); - params.set('to', newFilters.to); - if (newFilters.channel !== 'all') params.set('channel', newFilters.channel); - if (newFilters.compare !== 'none') params.set('compare', newFilters.compare); - router.replace(`/reporting?${params.toString()}`, { scroll: false }); - }, - [router], - ); - - const handleFiltersChange = useCallback( - (newFilters: FilterState) => { - setFilters(newFilters); - updateSearchParams(newFilters); - }, - [updateSearchParams], - ); - - const loadData = useCallback(async () => { - setLoading(true); - setError(''); - try { - const result = await reportingClient.fetchSummary({ - from: filters.from, - to: filters.to, - channel: filters.channel, - compare: filters.compare, - }); - setData(result); - } catch (err) { - setError(err instanceof Error ? err.message : 'Error al cargar datos'); - } finally { - setLoading(false); - } - }, [filters]); - - useEffect(() => { loadData(); }, [loadData]); - - const da = data?.dataAvailability; - - return ( -
- {/* Header */} -
-

Reporting

-

- {data ? `Actualizado: ${new Date(data.updatedAt).toLocaleString('es-ES')}` : 'Cargando...'} -

-
- - {/* Filter bar */} -
-
- {/* Channel */} -
- - -
- - {/* Compare */} -
- - -
- - {/* Refresh */} - -
- - {/* Date range */} -
-

Rango de fechas

- handleFiltersChange({ ...filters, from, to })} - /> -
-
- - {/* KPI grid */} - {loading ? ( -
- {[...Array(4)].map((_, i) => ( -
-
-
-
- ))} -
- ) : error ? ( -
-

{error}

- -
- ) : !data ? ( -
-

📊

-

Cargando datos...

-
- ) : ( - <> - {/* Comparison banner */} - {data.comparison && ( -
- Comparación activa:{' '} - Comparando con el período {filters.compare === 'previous_equal' ? 'anterior (misma duración)' : 'mes anterior (calendario)'}. -
- )} - -
- - - - - - - - -
- - {/* Data availability legend */} -
-

- Estado de métricas -

-
- {Object.entries(data.dataAvailability).map(([key, value]) => ( -
- {key.replace(/([A-Z])/g, ' $1').trim()}: - - {value === 'available' ? '✓' : '✗'} {value === 'available' ? 'Disponible' : 'No disponible'} - -
- ))} -
-
- - )} -
- ); -} +import { redirect } from 'next/navigation'; export default function ReportingPage() { - return ( - -
-
-
-
- {[...Array(4)].map((_, i) => ( -
- ))} -
-
-
- }> - - - ); + redirect('/reporting/dashboard'); } diff --git a/project/apps/admin/src/app/(dashboard)/reporting/products/page.tsx b/project/apps/admin/src/app/(dashboard)/reporting/products/page.tsx index d6731e2..b03baeb 100644 --- a/project/apps/admin/src/app/(dashboard)/reporting/products/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/reporting/products/page.tsx @@ -55,7 +55,7 @@ function ProductsContent() { from: filters.from, to: filters.to, channel: filters.channel, - groupBy: filters.sort === 'revenue' ? 'revenue' : undefined, + sort: filters.sort, page: filters.page, pageSize: filters.pageSize, }); @@ -76,12 +76,9 @@ function ProductsContent() { }; return ( -
+
{/* Header */}
-

Productos

Ranking de productos más vendidos diff --git a/project/apps/admin/src/app/(dashboard)/reporting/sales/page.tsx b/project/apps/admin/src/app/(dashboard)/reporting/sales/page.tsx index 5681cef..a228e0f 100644 --- a/project/apps/admin/src/app/(dashboard)/reporting/sales/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/reporting/sales/page.tsx @@ -114,12 +114,9 @@ function SalesContent() { useEffect(() => { loadData(); }, [loadData]); return ( -

+
{/* Header */}
-

Ventas agrupadas

{data ? `${data.pagination.totalRows} filas · Actualizado: ${new Date(data.updatedAt).toLocaleString('es-ES')}` : 'Cargando...'} diff --git a/project/apps/admin/src/lib/permissions.ts b/project/apps/admin/src/lib/permissions.ts index 1ce59b5..d3941ed 100644 --- a/project/apps/admin/src/lib/permissions.ts +++ b/project/apps/admin/src/lib/permissions.ts @@ -37,15 +37,11 @@ export interface NavItem { icon: string; permission: Permission; badge?: number; - parentHref?: string; } export const NAV_ITEMS: NavItem[] = [ { href: '/', label: 'Dashboard', icon: '📊', permission: 'dashboard' }, { href: '/reporting', label: 'Reporting', icon: '📈', permission: 'reporting.read' }, - { href: '/reporting/dashboard', label: 'Dashboard', icon: '📊', permission: 'reporting.read', parentHref: '/reporting' }, - { href: '/reporting/sales', label: 'Ventas', icon: '🧾', permission: 'reporting.read', parentHref: '/reporting' }, - { href: '/reporting/products', label: 'Productos', icon: '📦', permission: 'reporting.read', parentHref: '/reporting' }, { href: '/products', label: 'Productos', icon: '📦', permission: 'products.read' }, { href: '/orders', label: 'Pedidos', icon: '🧾', permission: 'orders.read' }, { href: '/payments', label: 'Pagos', icon: '💳', permission: 'orders.read' }, @@ -64,10 +60,6 @@ export const NAV_ITEMS: NavItem[] = [ { href: '/settings', label: 'Ajustes', icon: '⚙️', permission: 'dashboard' }, ]; -export function topNavItems(role: Role): NavItem[] { - return NAV_ITEMS.filter((item) => !item.parentHref && can(role, item.permission)); -} - -export function subNavItems(parentHref: string, role: Role): NavItem[] { - return NAV_ITEMS.filter((item) => item.parentHref === parentHref && can(role, item.permission)); +export function visibleNavItems(role: Role): NavItem[] { + return NAV_ITEMS.filter((item) => can(role, item.permission)); } diff --git a/project/apps/admin/src/lib/reporting-client.ts b/project/apps/admin/src/lib/reporting-client.ts index 8ff453b..cc51277 100644 --- a/project/apps/admin/src/lib/reporting-client.ts +++ b/project/apps/admin/src/lib/reporting-client.ts @@ -25,6 +25,7 @@ export interface ReportingFilters { terminalId?: string | string[]; state?: string | string[]; groupBy?: GroupBy; + sort?: 'units' | 'revenue'; page?: number; pageSize?: number; } @@ -138,6 +139,7 @@ function filtersToQueryString(filters: ReportingFilters): string { if (filters.compare && filters.compare !== 'none') params.set('compare', filters.compare); if (filters.channel && filters.channel !== 'all') params.set('channel', filters.channel); if (filters.groupBy) params.set('groupBy', filters.groupBy); + if (filters.sort) params.set('sort', filters.sort); if (filters.page) params.set('page', String(filters.page)); if (filters.pageSize) params.set('pageSize', String(filters.pageSize)); if (filters.storeId) { diff --git a/work/artifacts/F-157/architect.md b/work/artifacts/F-157/architect.md new file mode 100644 index 0000000..c3b118f --- /dev/null +++ b/work/artifacts/F-157/architect.md @@ -0,0 +1,15 @@ +# F-157 — Diseño + +## Decisión +Usar un layout anidado de Next.js en `app/(dashboard)/reporting/layout.tsx` para alojar navegación local persistente, siguiendo el patrón visual de las pestañas internas de Ajustes. + +## Cambios +1. `permissions.ts`: el modelo del sidebar vuelve a ser plano; solo existe la entrada principal `/reporting`. +2. Dashboard layout: renderiza únicamente entradas del menú principal, sin subitems. +3. Reporting nested layout: lista local Dashboard, Ventas y Productos con estado activo según `usePathname`. +4. `/reporting`: redirige a `/reporting/dashboard`, que actúa como apartado inicial. + +## Riesgos +- Evitar que `/reporting` se marque distinto: el sidebar usa `pathname.startsWith('/reporting')`. +- Evitar duplicar paddings: el layout local solo añade estructura flex y deja a cada página su contenido. +- Mantener RBAC existente; no se introducen rutas ni permisos nuevos. diff --git a/work/artifacts/F-157/documenter.md b/work/artifacts/F-157/documenter.md new file mode 100644 index 0000000..3370216 --- /dev/null +++ b/work/artifacts/F-157/documenter.md @@ -0,0 +1,7 @@ +# F-157 — Documentación + +Cambio de comportamiento visible: + +- El menú principal contiene únicamente **Reporting**. +- Al abrir Reporting se selecciona **Dashboard** por defecto. +- **Dashboard**, **Ventas** y **Productos** son apartados internos del módulo y se muestran en la navegación local de Reporting, igual que los apartados de Ajustes. diff --git a/work/artifacts/F-157/implementer.md b/work/artifacts/F-157/implementer.md new file mode 100644 index 0000000..5401961 --- /dev/null +++ b/work/artifacts/F-157/implementer.md @@ -0,0 +1,23 @@ +# F-157 — Implementer + +## Implementación +- Eliminados Dashboard, Ventas y Productos de `NAV_ITEMS`; el menú principal conserva una única entrada Reporting. +- Simplificado el sidebar para renderizar solo `visibleNavItems`. +- Añadido `reporting/layout.tsx` con navegación local persistente, estilo Ajustes, y estado activo accesible mediante `aria-current`. +- `/reporting` redirige al apartado inicial `/reporting/dashboard`. +- Eliminados los enlaces redundantes «← Reporting» de las páginas internas. +- Corregido el contrato frontend de `sort` en productos para que el build del admin use el filtro backend existente en vez de enviar `groupBy=revenue`. + +## Evidencia +- `cd project/apps/admin && npm run build`: PASS, 29 rutas generadas. +- `cd project && node_modules/.bin/tsc --noEmit`: PASS. +- `git diff --check`: PASS. +- `./scripts/verify.sh`: PASS. + +## Archivos +- `project/apps/admin/src/app/(dashboard)/layout.tsx` +- `project/apps/admin/src/lib/permissions.ts` +- `project/apps/admin/src/app/(dashboard)/reporting/layout.tsx` +- `project/apps/admin/src/app/(dashboard)/reporting/page.tsx` +- `project/apps/admin/src/app/(dashboard)/reporting/{dashboard,sales,products}/page.tsx` +- `project/apps/admin/src/lib/reporting-client.ts` diff --git a/work/artifacts/F-157/leader-close.json b/work/artifacts/F-157/leader-close.json new file mode 100644 index 0000000..cbafdd3 --- /dev/null +++ b/work/artifacts/F-157/leader-close.json @@ -0,0 +1,15 @@ +{ + "feature_id": "F-157", + "agent": "leader", + "stage": "close", + "verdict": "APPROVED", + "summary": "Reporting queda como una entrada única del sidebar y sus tres apartados viven dentro del módulo.", + "checks": [ + { "item": "Reviewer approved", "ok": true }, + { "item": "Security approved", "ok": true }, + { "item": "QA approved", "ok": true }, + { "item": "Admin build passed", "ok": true }, + { "item": "verify.sh passed", "ok": true } + ], + "issues": [] +} diff --git a/work/artifacts/F-157/qa.json b/work/artifacts/F-157/qa.json new file mode 100644 index 0000000..2b03027 --- /dev/null +++ b/work/artifacts/F-157/qa.json @@ -0,0 +1,15 @@ +{ + "feature_id": "F-157", + "agent": "qa", + "stage": "qa_gate", + "verdict": "APPROVED", + "summary": "Los cuatro criterios de aceptación quedan cubiertos.", + "checks": [ + { "item": "Only one Reporting sidebar entry", "ok": true, "evidence": "Static acceptance check passed" }, + { "item": "Three page-local sections", "ok": true, "evidence": "Nested layout contains Dashboard, Ventas, Productos" }, + { "item": "Active section marker", "ok": true, "evidence": "usePathname plus aria-current=page" }, + { "item": "Admin build", "ok": true, "evidence": "Next.js production build passed" }, + { "item": "Harness", "ok": true, "evidence": "verify.sh passed" } + ], + "issues": [] +} diff --git a/work/artifacts/F-157/reviewer.json b/work/artifacts/F-157/reviewer.json new file mode 100644 index 0000000..48c3c44 --- /dev/null +++ b/work/artifacts/F-157/reviewer.json @@ -0,0 +1,14 @@ +{ + "feature_id": "F-157", + "agent": "reviewer", + "stage": "review_gate", + "verdict": "APPROVED", + "summary": "El sidebar queda plano con una sola entrada Reporting y la navegación de secciones vive en el layout interno.", + "checks": [ + { "item": "Sidebar has only Reporting", "ok": true, "evidence": "permissions.ts contains no reporting child NAV_ITEMS" }, + { "item": "Local section navigation", "ok": true, "evidence": "reporting/layout.tsx lists Dashboard, Ventas and Productos" }, + { "item": "Admin production build", "ok": true, "evidence": "Next.js build generated 29 routes" }, + { "item": "TypeScript", "ok": true, "evidence": "project tsc --noEmit passed" } + ], + "issues": [] +} diff --git a/work/artifacts/F-157/security.json b/work/artifacts/F-157/security.json new file mode 100644 index 0000000..6442161 --- /dev/null +++ b/work/artifacts/F-157/security.json @@ -0,0 +1,13 @@ +{ + "feature_id": "F-157", + "agent": "security", + "stage": "security_gate", + "verdict": "APPROVED", + "summary": "Sin cambios de autenticación ni autorización; Reporting conserva el permiso reporting.read del menú principal y las rutas backend mantienen RBAC existente.", + "checks": [ + { "item": "RBAC unchanged", "ok": true, "evidence": "Only navigation presentation changed" }, + { "item": "Static internal links", "ok": true, "evidence": "All section hrefs are hard-coded same-origin paths" }, + { "item": "No secret or dependency change", "ok": true, "evidence": "No config secrets or packages modified" } + ], + "issues": [] +} diff --git a/work/current.md b/work/current.md index 03c9009..ae1e3c0 100644 --- a/work/current.md +++ b/work/current.md @@ -1,253 +1,17 @@ -# Sprint completo: Todas las 270 features cerradas - -## Estado final (2026-08-22) -- **Backlog**: 270/270 features done ✅ -- **verify.sh**: verde -- **tsc**: 0 errores - -## Sprint POS (F-003..F-010, POS-003..POS-046) - -### Fase 1 POS (POS-003 a POS-010) — COMPLETADO -- **POS-003**: Módulo POS backend (domain types, repos, use cases, unit tests, build-app.ts) -- **POS-004**: Registro de rutas POS (12 rutas administrativas + terminales + sesiones) -- **POS-005**: Búsqueda de productos + CRUD métodos de pago (admin) -- **POS-006**: App Next.js `apps/pos` (15 archivos: configs, layouts, middleware, API client, utils) -- **POS-007**: Pantalla principal del TPV (productos, carrito, descuentos, pago efectivo/tarjeta) -- **POS-008**: POST /pos/sales idempotente (transacción atómica con PostgreSQL) -- **POS-009**: Búsqueda y detalle de clientes para asociación -- **POS-010**: Panel de descuentos (API validación + componente React) - -### Fase 2/3 POS (POS-011 a POS-022) — COMPLETADO -- **POS-011**: Lista de ventas, void, receipts, historial de sesiones -- **POS-012**: Reembolsos, impresión de tickets, analytics summary -- **POS-013**: Alertas stock bajo, loyalty, settings, atajos de teclado -- **POS-014**: Turnos, tasas IVA, multi-tienda, notificaciones, reports diarios y EOD -- **POS-015**: Kitchen display, cajón, importación pedidos, integraciones, exportación CSV/JSON -- **POS-016**: Users management, audit log, sync catálogo, time tracking, lookup EAN/SKU -- **POS-017..POS-022**: Split payments, holds, quotes, tips, gift cards, multi-currency, y features restantes de Phase 2/3 - -### Fase 4/5+6/7 POS (POS-023 a POS-046) — COMPLETADO -- 23 rutas adicionales: advanced analytics, payroll, suppliers, kitchen display status, inventory forecast, supplier orders, order status management, hourly/products/employees reports, categories, tags, stock alerts, y más - -## F-149 cerrada (2026-08-22) — Reporting: product category and brand reports - -- `reporting-service.ts`: ReportingService con summary() + sales() usando CTEs SQL parametrizados. -- `GET /reporting/summary` + `GET /reporting/sales` con filtros/channel/storeId/terminalId/groupBy/pagination. -- 44 tests reporting (14 unit + 15 route + 15 existing); npm run build 0; boundaries 0 nuevas; verify.sh verde; commit `91044da`. -- Gates: reviewer ✅ / security ✅ / qa ✅ / document ✅ / leader-close ✅. -- **Siguiente**: F-147 (Admin: reporting shell and global filters). - -- `049_reporting_payment_lines.js`: tabla `reporting_payment_lines` (13 columnas, FK→orders_orders+pos_stores, 3 CHECK, 3 índices), inmutable (refunds como nuevas filas). patrón: INSERT-only. -- `reporting-payment-lines.itest.ts` 16/16 ✅ (DB real). -- npm run build 0; boundaries 0 nuevas; verify.sh verde; commit `62a368f`. -- Gates: reviewer ✅ / security ✅ / qa ✅ / document ✅ / leader-close ✅. -- **Siguiente**: F-146 (Reporting: service summary and sales API). - -## F-148 cerrada (2026-08-22) — Admin: sales dashboard and channel views - -## F-147 cerrada (2026-08-22) — Admin: reporting shell and global filters - -## F-146 cerrada (2026-08-22) — Reporting: service summary and sales API - -## F-145 cerrada (2026-08-22) — Reporting: payment lines and POS cash-safe capture - -## F-144 cerrada (2026-08-22) — Reporting snapshots: store/VAT/cost/shipping - -- `048_reporting_store_shipping_snapshots.js`: `orders_orders.store_id` (uuid NOT NULL DEFAULT default-store + FK→pos_stores + idx), `orders_orders.shipping_cents` (integer NOT NULL DEFAULT 0), `orders_items.cost_at_sale_cents` (bigint nullable), `orders_items.vat_rate` (text nullable). -- `reporting-snapshots.itest.ts` 3/3 ✅ (DB real, migration 001→048 aplicada). -- tsc 0; boundaries 0 nuevas; verify.sh verde; commit `2ea628f`. -- Gates: reviewer ✅ / security ✅ / qa ✅ / document ✅ / leader-close ✅. -- **Siguiente**: F-145 (Reporting: payment lines and POS cash-safe capture). - -## F-138 cerrada (2026-08-22) — auto-seed default price row on variant creation - -- `POST /products(:id/variants)` create ahora inserta inmediatamente una fila default en `pricing_variant_prices` (`net_unit_amount_cents=0`, `vat_rate='general'`, `currency='EUR'`) vía `PricingService.seedVariantPrice` (inyectado en `CreateProductVariant`, best-effort, `ON CONFLICT DO NOTHING`). Cierra la ventana 404 de `GET /pricing/variants/:id`. Backend-only, sin migración. -- Fuente: pricing `seedVariantPrice` (ports+service+PgPricingRepository), catalog `CreateProductVariant` seed (best-effort), `CatalogRoutesDeps += pricing`, `build-app.ts` hoist `pricing` const; `+` tests `variant-use-cases.test.ts` (3) + AC itest en `catalog.itest.ts`. -- Gates: implementer ✅ / reviewer APPROVED ✅ / security APPROVED ✅ / qa APPROVED ✅ / leader close ✅. -- Verificación: `tsc --noEmit` 0 errores; `npm test` 209 passed / 57 skipped (+3 nuevos); `lint:boundaries` sin violaciones nuevas (R1 preexistente `security.routes.ts → log-broadcaster` no introducido); `./scripts/verify.sh` verde (pre-close + post-close tras corregir `leader-close.json` verdict CLOSED→APPROVED). -- Commit: `feat(F-138): completed feature` (fb01593, amendado). - -## F-154 cerrada (2026-08-22) — separate customers from internal users - -- `GET /users` (Customers) now returns **storefront customers only** (`identity_users.role = 'customer'` filtro literal en `PgProfileRepository.listCustomers`, COUNT + SELECT). `GET /users/:id` owner-or-admin inalterado. -- `GET /admin/users` (Users) now returns **internos/backoffice only** (`role <> 'customer'` base literal; `?role=admin|editor` afinando dentro de internos; `?role=customer` → vacío). -- Frontend: dropdown de Users quita opción `customer` (backend ya fuerza la separación). Customers page sin cambio (llama /users → ahora customer-only). -- Tests: 6 nuevos unitarios (mock, sin DB) en `pg-profile-repository.test.ts` (3) + `security.routes.test.ts` (3). `users.itest.ts` AC2/AC3 flip: /users devuelve ben (customer) no ana (admin). Full suite 206 passed / 56 skipped, sin regresiones. -- Gates: implementer ✅ / reviewer APPROVED ✅ / security APPROVED ✅ / qa APPROVED ✅ / leader close ✅. -- `tsc --noEmit` 0 errores (API + apps/admin); eslint/prettier limpios en archivos tocados; `lint:boundaries` sin violaciones NUEVAS (R1 preexistente en `security.routes.ts → log-broadcaster`, no introducido por F-154, verificado via git diff); `verify.sh` exit 0. -- Commit: `feat(F-154): completed feature`. - -## F-153 cerrada (2026-08-22) — customer email on order view - -- Asocia el email del cliente (`identity_users.email`, NOT NULL) al order read model vía `LEFT JOIN identity_users` en el repo, y lo expone en `serializeOrder` (detail + lista) y en la notificación del force-transition admin (usa `order.email`; se elimina el lookup inline). -- `OrderView.email?: string | null` (additivo, opcional en fixtures de test → serializado como `null`). Sin migración. Boundary intacta (orders→identity_users es referencia SQL, no import TS). -- Tests: 3 nuevos en `pg-order-repository.test.ts` (email resuelto vía JOIN, `null` sin usuario vinculado, `undefined` cuando no existe). Suite 200 passed / 56 skipped, sin regresiones. -- Gates: implementer ✅ / reviewer APPROVED ✅ / security APPROVED ✅ / qa APPROVED ✅ / leader close ✅. -- `tsc --noEmit` 0 errores; eslint 0; prettier baseline-only; `lint:boundaries` sin violaciones nuevas; `verify.sh` exit 0. -- Commit: `feat(F-153): completed feature` + `chore: reset runtime after F-153`. -- Pendiente siguiente por orden: **F-154** (separate customers from internal users). - -## Sesión 2026-08-22 — F-152 cerrada (emails on account creation + order confirmation) - -- `F-152` cerrada: welcome email (`account_created`) on `POST /auth/register` y order confirmation email on `POST /payments/webhook` (PaymentSucceeded). - Best-effort (fire-and-forget + `request.log.warn`), idempotent (gate `outcome.kind === "processed"`), - XSS-safe (`buildWelcomeEmail` escapea name), SMTP config de `store_settings`. -- Gates: implementer ✅ / reviewer APPROVED ✅ / security APPROVED ✅ / qa APPROVED ✅ / leader close ✅. -- `tsc --noEmit` 0 errores; prettier+eslint limpios en archivos tocados; `lint:boundaries` sin nuevas violaciones; - 197 tests ✅; `git diff --check` ✅; `verify.sh` exit 0. -- Commits: `feat(F-152): completed feature` + `chore: reset runtime after F-152` (push omitido, sin remote `origin`). -- `runtime-status.json` reseteado a idle. -- Pendiente siguiente por orden: `F-153` (customer email missing). - -## Sesión 2026-08-22 — F-156 cerrada (CMS dynamic rendering) - -Backlog: **269 features, 212 done, 57 pending, 0 in_progress, 0 blocked**. - -- `F-156` cerrada: CMS content-managed pages (`/about`, `/contact`, `/shipping`) dejan de servir caché stale. `fetchPage` usa `{ cache: 'no-store' }` y las páginas exportan `dynamic = 'force-dynamic'`; el fallback estático y el RBAC se mantienen. -- Gates: implementer ✅ / reviewer APPROVED ✅ / security APPROVED ✅ / qa APPROVED ✅ / leader close ✅. -- Verificado runtime: `curl -sI /about` → 200 con `Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate`; verificador temporal de DB apareció y desapareció sin rebuild. -- `verify.sh` green; cambios commiteados como `feat(F-156): completed feature`; `runtime-status.json` reseteado a idle. -- Pendientes inmediatos por orden sugerido: `F-152` (emails account creation/order confirmation) → `F-153` (customer email missing) → `F-154` (separate customers from internal users). Los tickets Reporting P0 pendientes son `F-143`..`F-150`. - -## Sesión 2026-08-21 — Reporting preparado y builds Next estabilizados - -Backlog: **264 features, 209 done, 55 pending, 0 in_progress, 0 blocked**. - -- `F-141` cerrada: raíces `turbopack`/`outputFileTracingRoot` explícitas en customer frontend y páginas de catálogo dinámicas para que el build no dependa del API. -- `F-142` cerrada: `docs/reporting/REPORTING_ARCHITECTURE.md` y `docs/reporting/REPORTING_TASKS.md` entregados tras analizar modelos reales, datos faltantes, API, rendimiento, caché, RBAC y roadmap P0-P3. -- Tickets Reporting P0 abiertos: `F-143`..`F-150`; todos `pending`. -- `verify.sh` exit 0 y `runtime-status.json` reseteado a idle. -- Próximo ciclo: iniciar `F-143` (contracts, filtros, comparación y RBAC de Reporting) antes de implementar dashboards. - -## Sesión 2026-08-21 — backlog cerrado (nota inicial, desfasada) - -Backlog: 185 features (185 done, 0 pending, 0 in_progress) según la nota original. - -Últimas features cerradas en esa nota: **F-117**, **F-116**, **F-115**, **F-112**, **F-100**. - -Tras esa nota se cerraron **18 features adicionales** (F-118..F-135) sin actualizar `current.md`. Quedan reflejadas en `backlog/features.json` y en `work/history.md`. - -## F-117 cerrada (2026-08-21) -Fix de F-116: renombre las 9 categorías que quedaron en mayúsculas (FRUTAS Y VERDURAS, SNACKS, GRANOLA, SUPLEMENTS, FACIAL, CORPORAL, ASEO PERSONAL, HIERBAS MEDICINALES, PROVEEDORES). Re-ejecución idempotente. - -## F-116 cerrada (2026-08-21) -Traducción de las 30 categorías legacy a Español Title Case (Alimentación, Cosmética, Bebidas, Vino, Cerveza, Macrobiótica, etc.). Slugs conservados. - -## F-115 cerrada (2026-08-21) -Rever de F-100: SKU-MV-{uuid} restaurado en creación de producto y lazy migration. Eliminado endpoint `/products/sku:generate` y el editor de SKU en admin. - -## F-112 cerrada (2026-08-21) -Disclaimer de contenido asistido por IA en la ficha de producto. Migración 040 añade `catalog_products.ai_assisted`. Flag se activa automáticamente al generar contenido con IA. Frontend muestra el `