diff --git a/backlog/features.json b/backlog/features.json index 59c49db..4ce6cd9 100644 --- a/backlog/features.json +++ b/backlog/features.json @@ -6785,13 +6785,15 @@ "description": "Clarify and expose terminal binding and cash session workflow so operator can configure and open TPV without manual database/API steps.", "priority": "high", "risk": "med", - "status": "pending", + "status": "done", "created_at": "2026-08-22", "gates": { - "reviewer": false, - "security": false, - "qa": false - } + "reviewer": true, + "security": true, + "qa": true, + "close": true + }, + "completed_at": "2026-08-22T16:33:24Z" }, { "id": "F-164", @@ -6843,6 +6845,141 @@ "close": true }, "completed_at": "2026-08-22T16:21:15Z" + }, + { + "id": "F-167", + "type": "fix", + "title": "Reflow order editor into three columns", + "description": "Order edit layout uses three balanced columns: Actions/Products/Payment, Customer/Shipping and billing addresses, Shipping/Summary/History.", + "priority": "high", + "risk": "med", + "status": "pending", + "created_at": "2026-08-22", + "gates": { + "reviewer": false, + "security": false, + "qa": false + } + }, + { + "id": "F-168", + "type": "fix", + "title": "Reduce Reporting 30-day trend chart height", + "description": "Reporting dashboard 30-day trend occupies too much vertical space; reduce chart height responsively.", + "priority": "med", + "risk": "low", + "status": "pending", + "created_at": "2026-08-22", + "gates": { + "reviewer": false, + "security": false, + "qa": false + } + }, + { + "id": "F-169", + "type": "fix", + "title": "Fix Reporting products Internal Server Error", + "description": "Diagnose and fix /reporting/products API/UI 500 with runtime regression coverage.", + "priority": "high", + "risk": "high", + "status": "pending", + "created_at": "2026-08-22", + "gates": { + "reviewer": false, + "security": false, + "qa": false + } + }, + { + "id": "F-170", + "type": "fix", + "title": "Allow deleting categories", + "description": "Categories admin delete action does not work; restore guarded deletion and clear error feedback.", + "priority": "high", + "risk": "med", + "status": "pending", + "created_at": "2026-08-22", + "gates": { + "reviewer": false, + "security": false, + "qa": false + } + }, + { + "id": "F-171", + "type": "fix", + "title": "Audit and fix all transactional email events", + "description": "Review every event that generates email, fix SMTP delivery, and verify test delivery to info@rikrdo.es.", + "priority": "high", + "risk": "high", + "status": "pending", + "created_at": "2026-08-22", + "gates": { + "reviewer": false, + "security": false, + "qa": false + } + }, + { + "id": "F-172", + "type": "fix", + "title": "Fix My Account login session loop", + "description": "Clicking My Account logs user out and redirects repeatedly to /auth/login; preserve storefront session and destination.", + "priority": "high", + "risk": "med", + "status": "pending", + "created_at": "2026-08-22", + "gates": { + "reviewer": false, + "security": false, + "qa": false + } + }, + { + "id": "F-173", + "type": "fix", + "title": "Fix checkout page load failure", + "description": "/checkout shows This page couldn't load; diagnose frontend/runtime and restore checkout.", + "priority": "high", + "risk": "high", + "status": "pending", + "created_at": "2026-08-22", + "gates": { + "reviewer": false, + "security": false, + "qa": false + } + }, + { + "id": "F-174", + "type": "fix", + "title": "Fix missing almagro.ttf font", + "description": "Resolve storefront 404 for almagro.ttf using correct local font asset/path or remove invalid reference.", + "priority": "med", + "risk": "low", + "status": "pending", + "created_at": "2026-08-22", + "gates": { + "reviewer": false, + "security": false, + "qa": false + } + }, + { + "id": "F-175", + "type": "fix", + "title": "Use logo-main.webp in collapsed sidebar", + "description": "Collapsed admin sidebar must use logo-main.webp three-leaf favicon mark instead of logo-main.png.", + "priority": "high", + "risk": "low", + "status": "pending", + "created_at": "2026-08-22", + "gates": { + "reviewer": false, + "security": false, + "qa": false + } } ] } diff --git a/project/apps/admin/src/app/(dashboard)/pos/page.tsx b/project/apps/admin/src/app/(dashboard)/pos/page.tsx new file mode 100644 index 0000000..52b91b1 --- /dev/null +++ b/project/apps/admin/src/app/(dashboard)/pos/page.tsx @@ -0,0 +1,128 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { api } from '@/lib/api-client'; + +interface PosStore { id: string; name: string; active: boolean } +interface PosTerminal { + id: string; + storeId: string; + name: string; + bindingCode: string | null; + boundAt: string | null; + status: string; + lastSeenAt: string | null; +} + +export default function PosAdminPage() { + const [stores, setStores] = useState([]); + const [terminals, setTerminals] = useState([]); + const [storeId, setStoreId] = useState(''); + const [name, setName] = useState(''); + const [loading, setLoading] = useState(true); + const [creating, setCreating] = useState(false); + const [error, setError] = useState(''); + const [newCode, setNewCode] = useState(''); + const [copied, setCopied] = useState(''); + + const load = async () => { + setLoading(true); + setError(''); + try { + const [storeData, terminalData] = await Promise.all([ + api.get<{ stores: PosStore[] }>('/api/pos/admin/stores'), + api.get<{ terminals: PosTerminal[] }>('/api/pos/admin/terminals'), + ]); + setStores(storeData.stores); + setTerminals(terminalData.terminals); + setStoreId((current) => current || storeData.stores.find((store) => store.active)?.id || ''); + } catch (err) { + setError(err instanceof Error ? err.message : 'No se pudo cargar la configuración TPV'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { void load(); }, []); + + const createTerminal = async (event: React.FormEvent) => { + event.preventDefault(); + setCreating(true); + setError(''); + setNewCode(''); + try { + const terminal = await api.post('/api/pos/admin/terminals', { storeId, name: name.trim() }); + setName(''); + setNewCode(terminal.bindingCode ?? ''); + await load(); + } catch (err) { + setError(err instanceof Error ? err.message : 'No se pudo crear el terminal'); + } finally { + setCreating(false); + } + }; + + const copyCode = async (code: string) => { + await navigator.clipboard.writeText(code); + setCopied(code); + setTimeout(() => setCopied(''), 2000); + }; + + return ( +
+
+

TPV

+

Configura dispositivos y obtén el código para vincular cada caja.

+
+ +
+
+

1. Vincular terminal

+

Crea un terminal aquí y escribe su código de 8 caracteres en el navegador del TPV. Esto identifica qué caja física está vendiendo.

+
+
+

2. Abrir sesión de caja

+

Después de vincular, el cajero indica el efectivo inicial en el propio TPV. La sesión representa el turno diario de caja.

+
+
+ +
+

Crear terminal

+
+ + + +
+ {newCode &&

Código creado: {newCode}

} + {error &&

{error}

} +
+ +
+

Terminales

+ {loading ?

Cargando…

: terminals.length === 0 ?

No hay terminales.

: ( +
+ + + + {terminals.map((terminal) => { + const store = stores.find((item) => item.id === terminal.storeId); + return ; + })} + +
NombreTiendaCódigoEstadoVinculado
{terminal.name}{store?.name ?? terminal.storeId}{terminal.status}{terminal.boundAt ? 'Sí' : 'No'}
+
+ )} +
+
+ ); +} diff --git a/project/apps/admin/src/lib/permissions.ts b/project/apps/admin/src/lib/permissions.ts index d3941ed..a0eacb2 100644 --- a/project/apps/admin/src/lib/permissions.ts +++ b/project/apps/admin/src/lib/permissions.ts @@ -42,6 +42,7 @@ export interface NavItem { export const NAV_ITEMS: NavItem[] = [ { href: '/', label: 'Dashboard', icon: '📊', permission: 'dashboard' }, { href: '/reporting', label: 'Reporting', icon: '📈', permission: 'reporting.read' }, + { href: '/pos', label: 'TPV', icon: '🛒', permission: 'dashboard' }, { href: '/products', label: 'Productos', icon: '📦', permission: 'products.read' }, { href: '/orders', label: 'Pedidos', icon: '🧾', permission: 'orders.read' }, { href: '/payments', label: 'Pagos', icon: '💳', permission: 'orders.read' }, diff --git a/project/apps/pos/next-env.d.ts b/project/apps/pos/next-env.d.ts index a419cbe..ce4e94a 100644 --- a/project/apps/pos/next-env.d.ts +++ b/project/apps/pos/next-env.d.ts @@ -1,7 +1,7 @@ /// /// -import "./.next/dev/types/routes.d.ts"; -import "./.next/dev/types/root-params.d.ts"; +import "./.next/types/routes.d.ts"; +import "./.next/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/project/apps/pos/src/app/(auth)/login/page.tsx b/project/apps/pos/src/app/(auth)/login/page.tsx index e4c7c38..011ee35 100644 --- a/project/apps/pos/src/app/(auth)/login/page.tsx +++ b/project/apps/pos/src/app/(auth)/login/page.tsx @@ -14,7 +14,7 @@ export default function LoginPage() { setLoading(true); setError(''); try { - const res = await fetch('/api/auth/login', { + const res = await fetch('/api/backoffice/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), diff --git a/project/apps/pos/src/app/(terminal)/page.tsx b/project/apps/pos/src/app/(terminal)/page.tsx index b1e28aa..fa06822 100644 --- a/project/apps/pos/src/app/(terminal)/page.tsx +++ b/project/apps/pos/src/app/(terminal)/page.tsx @@ -43,6 +43,9 @@ export default function RegisterPage() { const [needsBinding, setNeedsBinding] = useState(false); const [bindingCode, setBindingCode] = useState(''); const [binding, setBinding] = useState(false); + const [openingCash, setOpeningCash] = useState('0'); + const [openingSession, setOpeningSession] = useState(false); + const [sessionError, setSessionError] = useState(''); const [cart, setCart] = useState([]); const [search, setSearch] = useState(''); const [searchResults, setSearchResults] = useState([]); @@ -89,6 +92,25 @@ export default function RegisterPage() { } }; + const openCashSession = async (event: React.FormEvent) => { + event.preventDefault(); + const amount = Number(openingCash.replace(',', '.')); + if (!Number.isFinite(amount) || amount < 0) { + setSessionError('Introduce un importe válido'); + return; + } + setOpeningSession(true); + setSessionError(''); + try { + await posApi.openSession(Math.round(amount * 100)); + await loadConfig(); + } catch (err) { + setSessionError(err instanceof Error ? err.message : 'No se pudo abrir la caja'); + } finally { + setOpeningSession(false); + } + }; + const doSearch = useCallback(async (q: string) => { if (q.trim().length < 2) { setSearchResults([]); return; } setSearching(true); @@ -231,9 +253,32 @@ export default function RegisterPage() { if (config.session?.status !== 'OPEN') { return ( -
-

Sesión cerrada

-

Abre una caja desde la administración para comenzar.

+
+
+
+

{config.store.name} · {config.terminal.name}

+

Abrir sesión de caja

+

Indica el efectivo disponible al comenzar el turno. Puedes usar 0 si la caja empieza vacía.

+
+ + {sessionError &&

{sessionError}

} + +

Una sesión de caja representa el turno diario y registra ventas, efectivo esperado y cierre.

+
); } diff --git a/project/apps/pos/src/lib/api-client.ts b/project/apps/pos/src/lib/api-client.ts index faaa3e0..d40b6ae 100644 --- a/project/apps/pos/src/lib/api-client.ts +++ b/project/apps/pos/src/lib/api-client.ts @@ -35,6 +35,12 @@ export const posApi = { }), /** Get POS config (store + terminal + payment methods + session status). */ config: () => apiFetch('/pos/config'), + /** Open the daily cash session for the bound terminal. */ + openSession: (openingCashCents: number) => + apiFetch<{ id: string; status: 'OPEN' }>('/pos/sessions', { + method: 'POST', + body: JSON.stringify({ openingCashCents }), + }), /** List products by query. */ searchProducts: (q: string, storeId?: string, limit = 20) => apiFetch(`/pos/products/search?q=${encodeURIComponent(q)}&storeId=${storeId ?? ''}&limit=${limit}`), @@ -46,6 +52,6 @@ export const posApi = { export const authApi = { login: (email: string, password: string) => - apiFetch('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) }), - logout: () => apiFetch('/auth/logout', { method: 'POST' }), + apiFetch('/backoffice/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) }), + logout: () => apiFetch('/backoffice/auth/logout', { method: 'POST' }), }; diff --git a/project/apps/pos/src/proxy.ts b/project/apps/pos/src/proxy.ts index c180970..d43d251 100644 --- a/project/apps/pos/src/proxy.ts +++ b/project/apps/pos/src/proxy.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; -const PUBLIC_PATHS = ['/login', '/api/auth']; +const PUBLIC_PATHS = ['/login', '/api/auth', '/api/backoffice/auth']; export function proxy(request: NextRequest) { const { pathname } = request.nextUrl; diff --git a/project/src/modules/pos/api/pos.routes.ts b/project/src/modules/pos/api/pos.routes.ts index 56f4c5a..4bc22e0 100644 --- a/project/src/modules/pos/api/pos.routes.ts +++ b/project/src/modules/pos/api/pos.routes.ts @@ -23,6 +23,9 @@ export interface PosRouteDeps { } const idParamSchema = z.object({ id: z.string().uuid() }); +// Zod's strict UUID parser rejects the RFC nil UUID used by the seeded default +// store. Store references accept the canonical UUID shape, including nil. +const storeIdSchema = z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps) { const { pool, authenticate } = deps; @@ -174,7 +177,7 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps const user = await authenticate(request); requireRole(user, 'admin'); const body = parseJson( - z.object({ storeId: z.string().uuid(), name: z.string().min(1).max(100) }), + z.object({ storeId: storeIdSchema, name: z.string().min(1).max(100) }), request.body ?? {}, ); // Generate a short binding code (8 hex chars) diff --git a/project/src/modules/pos/application/get-pos-config.ts b/project/src/modules/pos/application/get-pos-config.ts index 3db3547..a80d885 100644 --- a/project/src/modules/pos/application/get-pos-config.ts +++ b/project/src/modules/pos/application/get-pos-config.ts @@ -3,12 +3,14 @@ import type { PosPaymentMethodRepository } from '../infrastructure/pg-payment-me import type { PosStore } from '../domain/store.js'; import type { PosTerminal } from '../domain/terminal.js'; import type { PosPaymentMethod } from '../infrastructure/pg-payment-method-repository.js'; +import type { PosCashSession } from '../domain/cash-session.js'; export interface PosConfig { store: PosStore; terminal: PosTerminal; paymentMethods: PosPaymentMethod[]; sessionOpen: boolean; + session: PosCashSession | null; } export class GetPosConfigUseCase { @@ -29,6 +31,6 @@ export class GetPosConfigUseCase { this.sessionRepo.findOpenByTerminal(terminalId), ]); await this.terminalRepo.updateLastSeen(terminalId); - return { store, terminal, paymentMethods, sessionOpen: !!openSession }; + return { store, terminal, paymentMethods, sessionOpen: !!openSession, session: openSession ?? null }; } } diff --git a/work/artifacts/F-163/architect.md b/work/artifacts/F-163/architect.md new file mode 100644 index 0000000..cd99b8b --- /dev/null +++ b/work/artifacts/F-163/architect.md @@ -0,0 +1,10 @@ +# F-163 — Diseño + +## Admin +Página `/pos` consume APIs administrativas existentes con el cliente `api` compartido. Lista terminales y permite crear uno para una tienda activa; el binding code se muestra/copiará. RBAC sigue backend admin-only. + +## POS +Cuando config devuelve `session: null`, renderiza apertura de caja local. `POST /pos/sessions` recibe fondo inicial en céntimos; el proxy aporta `x-terminal-id` desde cookie. Tras 201 se recarga config. + +## Backend +La tienda por defecto usa UUID nil válido; sustituir Zod `.uuid()` estricto del body de terminal por regex canónica que acepta UUID nil y generados. diff --git a/work/artifacts/F-163/documenter.md b/work/artifacts/F-163/documenter.md new file mode 100644 index 0000000..439e0aa --- /dev/null +++ b/work/artifacts/F-163/documenter.md @@ -0,0 +1,7 @@ +# TPV: terminal y sesión + +- **Terminal**: caja/dispositivo físico. Admin → TPV crea el terminal y muestra su código de 8 caracteres. +- **Vincular**: introducir ese código una vez en el navegador del TPV. +- **Sesión de caja**: turno diario. En el TPV se indica efectivo inicial y se pulsa «Abrir caja y comenzar». + +La caja principal ya está vinculada y tiene una sesión OPEN. diff --git a/work/artifacts/F-163/implementer.md b/work/artifacts/F-163/implementer.md new file mode 100644 index 0000000..5564d52 --- /dev/null +++ b/work/artifacts/F-163/implementer.md @@ -0,0 +1,10 @@ +# F-163 — Implementer + +- Nueva entrada Admin → TPV y página para explicar, listar y crear terminales/códigos. +- Creación acepta UUID nil de tienda por defecto. +- POS login usa backoffice auth (cash session FK apunta a backoffice users). +- Pantalla «Sesión cerrada» sustituida por formulario para abrir caja con efectivo inicial. +- Config backend devuelve la sesión abierta completa, necesaria para ventas. +- Caja `TPV Principal` abierta con sesión `6d281efe-0682-4763-88bf-5d8cb4403c05`. + +Evidencia: creación terminal con tienda default PASS; login/bind/open/config = 200/200/201/200; session OPEN; admin+POS builds PASS; 6/6 cash session tests; tsc/verify PASS. diff --git a/work/artifacts/F-163/leader-close.json b/work/artifacts/F-163/leader-close.json new file mode 100644 index 0000000..ff1667b --- /dev/null +++ b/work/artifacts/F-163/leader-close.json @@ -0,0 +1 @@ +{"feature_id":"F-163","agent":"leader","stage":"close","verdict":"APPROVED","checks":[{"item":"all gates","ok":true},{"item":"e2e create-bind-open-config","ok":true},{"item":"build/tests/verify","ok":true}],"issues":[]} diff --git a/work/artifacts/F-163/qa.json b/work/artifacts/F-163/qa.json new file mode 100644 index 0000000..6ff688e --- /dev/null +++ b/work/artifacts/F-163/qa.json @@ -0,0 +1 @@ +{"feature_id":"F-163","agent":"qa","stage":"qa_gate","verdict":"APPROVED","checks":[{"item":"create terminal","ok":true},{"item":"bind","ok":true},{"item":"open session","ok":true},{"item":"config session OPEN","ok":true},{"item":"build/tests","ok":true}],"issues":[]} diff --git a/work/artifacts/F-163/reviewer.json b/work/artifacts/F-163/reviewer.json new file mode 100644 index 0000000..903d756 --- /dev/null +++ b/work/artifacts/F-163/reviewer.json @@ -0,0 +1 @@ +{"feature_id":"F-163","agent":"reviewer","stage":"review_gate","verdict":"APPROVED","checks":[{"item":"admin terminal UI","ok":true},{"item":"cash opening UI","ok":true},{"item":"auth/FK alignment","ok":true},{"item":"runtime e2e","ok":true}],"issues":[]} diff --git a/work/artifacts/F-163/security.json b/work/artifacts/F-163/security.json new file mode 100644 index 0000000..87522c4 --- /dev/null +++ b/work/artifacts/F-163/security.json @@ -0,0 +1 @@ +{"feature_id":"F-163","agent":"security","stage":"security_gate","verdict":"APPROVED","checks":[{"item":"admin endpoints RBAC","ok":true},{"item":"HttpOnly terminal cookie","ok":true},{"item":"backoffice staff session","ok":true}],"issues":[]} diff --git a/work/current.md b/work/current.md index 2d5eefd..93e9a2b 100644 --- a/work/current.md +++ b/work/current.md @@ -1,3 +1,16 @@ -# F-164 — Update outdated POS Next.js +# F-163 — Make POS terminal and cash session setup usable -Alinear POS con Admin: Next.js `16.3.1`, React/ReactDOM `19.2.8`. Actualizar lockfile, adaptar incompatibilidades de build y validar login/proxy/config. +## Qué significa +- **Vincular terminal**: asociar este navegador/dispositivo a una caja física mediante un código generado en Admin → TPV. +- **Sesión de caja**: turno diario de efectivo. Antes de vender se indica fondo inicial y se abre la caja. + +## Implementación +- Nueva entrada/página Admin **TPV**: explicación, tiendas, terminales, creación y código de vinculación. +- Aceptar el UUID fijo de la tienda por defecto al crear terminal. +- POS muestra formulario para abrir caja cuando no hay sesión OPEN; deja de mandar al operador a una pantalla admin inexistente. + +## Aceptación +1. Admin puede crear terminal y obtener código. +2. Dispositivo puede vincularse con ese código. +3. Cajero puede abrir sesión indicando efectivo inicial. +4. Tras abrir, aparece la pantalla de venta. diff --git a/work/runtime-status.json b/work/runtime-status.json index 2476cff..e6b542b 100644 --- a/work/runtime-status.json +++ b/work/runtime-status.json @@ -1,68 +1,68 @@ { - "feature_id": "F-164", + "feature_id": "F-163", "stage": "close", "agent": "leader", - "action": "Close POS Next upgrade", + "action": "Close usable TPV setup", "state": "running", "next_agent": "leader", "waiting_for": "Seleccionar una feature pending y actualizar este estado", - "updated_at": "2026-08-22T16:25:00Z", + "updated_at": "2026-08-22T16:33:09Z", "timeline": [ { - "ts": "2026-08-22T16:21:23Z", + "ts": "2026-08-22T16:25:44Z", "agent": "leader", "stage": "intake", "state": "running", - "message": "Align POS Next.js with Admin supported version" + "message": "Make terminal binding and cash opening self-service" }, { - "ts": "2026-08-22T16:21:38Z", + "ts": "2026-08-22T16:26:32Z", "agent": "architect", "stage": "design", "state": "running", - "message": "Plan POS framework version alignment" + "message": "Design Admin terminal setup and POS cash opening" }, { - "ts": "2026-08-22T16:21:52Z", + "ts": "2026-08-22T16:26:57Z", "agent": "implementer", "stage": "build", "state": "running", - "message": "Upgrade POS Next and React dependencies" + "message": "Implement terminal admin page and cash opening" }, { - "ts": "2026-08-22T16:23:59Z", + "ts": "2026-08-22T16:31:54Z", "agent": "reviewer", "stage": "review_gate", "state": "running", - "message": "Review POS framework migration" + "message": "Review end-to-end terminal and session setup" }, { - "ts": "2026-08-22T16:24:11Z", + "ts": "2026-08-22T16:32:07Z", "agent": "security", "stage": "security_gate", "state": "running", - "message": "Audit upgraded dependencies" + "message": "Audit TPV admin RBAC and binding cookies" }, { - "ts": "2026-08-22T16:24:25Z", + "ts": "2026-08-22T16:32:29Z", "agent": "qa", "stage": "qa_gate", "state": "running", - "message": "Validate upgraded POS build and LAN flow" + "message": "Validate create-bind-open-config workflow" }, { - "ts": "2026-08-22T16:24:41Z", + "ts": "2026-08-22T16:32:56Z", "agent": "documenter", "stage": "document", "state": "running", - "message": "Document POS framework upgrade" + "message": "Document terminal and cash session concepts" }, { - "ts": "2026-08-22T16:25:00Z", + "ts": "2026-08-22T16:33:09Z", "agent": "leader", "stage": "close", "state": "running", - "message": "Close POS Next upgrade" + "message": "Close usable TPV setup" } ] }