diff --git a/backlog/features.json b/backlog/features.json index 9e9d8f7..27d21a7 100644 --- a/backlog/features.json +++ b/backlog/features.json @@ -7744,6 +7744,38 @@ }, "phase": "pos", "completed_at": "2026-08-24T13:51:08Z" + }, + { + "id": "SHOP-STOCK", + "type": "bug", + "title": "Storefront: Añadir al carrito respeta stock", + "description": "Añadir al carrito se salta restriccion de stock. Si hay 2 en stock se puede pedir 1 o 2, pero no 3.", + "priority": "high", + "risk": "low", + "status": "pending", + "created_at": "2026-08-24", + "gates": { + "reviewer": false, + "security": false, + "qa": false + }, + "phase": "storefront" + }, + { + "id": "SHOP-AUTH", + "type": "bug", + "title": "Storefront: Login register mismatch", + "description": "Email already registered en register pero iniciar sesion dice password incorrecto cuando es correcta.", + "priority": "high", + "risk": "low", + "status": "pending", + "created_at": "2026-08-24", + "gates": { + "reviewer": false, + "security": false, + "qa": false + }, + "phase": "storefront" } ] } diff --git a/project/apps/pos/src/app/(terminal)/page.tsx b/project/apps/pos/src/app/(terminal)/page.tsx index 2dfdea1..2840ce0 100644 --- a/project/apps/pos/src/app/(terminal)/page.tsx +++ b/project/apps/pos/src/app/(terminal)/page.tsx @@ -147,6 +147,13 @@ export default function RegisterPage() { toastTimer = setTimeout(() => setAddedToast(null), 2000); }; + // Clock state for TPV header + const [currentTime, setCurrentTime] = useState(new Date()); + useEffect(() => { + const timer = setInterval(() => setCurrentTime(new Date()), 1000); + return () => clearInterval(timer); + }, []); + // POS-FIX-5: verify PIN then close session const handleVerifyPin = async () => { if (!config?.terminal) return; @@ -1180,19 +1187,42 @@ export default function RegisterPage() {
-
- {config.store.name} - - Caja abierta - +
+ {/* Panel toggle button - LEFT */} + + {/* Store name */} + {config.store.name} + + {/* Status badge */} + + + + + + Caja abierta + + + {/* Clock */} + + + + + {currentTime.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit', second: '2-digit' })} + · + {currentTime.toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' })} + + {!config?.terminal?.settings?.selfpayMode && ( )} diff --git a/project/frontend/src/app/auth/login/page.tsx b/project/frontend/src/app/auth/login/page.tsx index a824c5d..fcba150 100644 --- a/project/frontend/src/app/auth/login/page.tsx +++ b/project/frontend/src/app/auth/login/page.tsx @@ -35,6 +35,13 @@ export default function LoginPage() { {error && (
{error} + {error.includes('no confirmado') && ( +
+ + Solicitar nuevo enlace de confirmación + +
+ )}
)}
diff --git a/project/frontend/src/app/auth/register/page.tsx b/project/frontend/src/app/auth/register/page.tsx index a4b6b53..c59d3de 100644 --- a/project/frontend/src/app/auth/register/page.tsx +++ b/project/frontend/src/app/auth/register/page.tsx @@ -65,6 +65,13 @@ export default function RegisterPage() { {error && (
{error} + {error.includes('ya está registrado') && ( +
+ + Inicia sesión con tu cuenta existente + +
+ )}
)}
diff --git a/project/frontend/src/contexts/AuthContext.tsx b/project/frontend/src/contexts/AuthContext.tsx index 9ec8d59..4e12968 100644 --- a/project/frontend/src/contexts/AuthContext.tsx +++ b/project/frontend/src/contexts/AuthContext.tsx @@ -43,7 +43,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }); const data = await res.json(); if (!res.ok) { - return { ok: false, error: data.error?.message || 'Error de login' }; + if (data.error?.code === 'EMAIL_NOT_CONFIRMED') { + return { ok: false, error: 'Email no confirmado. Revisa tu correo o solicita un nuevo enlace de confirmación.' }; + } + if (data.error?.code === 'EMAIL_ALREADY_REGISTERED') { + return { ok: false, error: 'Este email ya está registrado. ¿Ya tienes cuenta? Inicia sesión.' }; + } + return { ok: false, error: data.error?.message || 'Email o contraseña incorrectos' }; } setUser(data); return { ok: true }; @@ -58,6 +64,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }); const data = await res.json(); if (!res.ok) { + if (data.error?.code === 'EMAIL_ALREADY_REGISTERED') { + return { ok: false, error: 'Este email ya está registrado. ¿Ya tienes cuenta? Inicia sesión.' }; + } return { ok: false, error: data.error?.message || 'Error de registro' }; } setUser(data); diff --git a/project/src/modules/inventory/api/inventory.routes.ts b/project/src/modules/inventory/api/inventory.routes.ts index e8601d3..69b58f8 100644 --- a/project/src/modules/inventory/api/inventory.routes.ts +++ b/project/src/modules/inventory/api/inventory.routes.ts @@ -22,6 +22,7 @@ export interface InventoryRoutesDeps { } const variantParamSchema = z.object({ variantId: z.uuid() }); +const productParamSchema = z.object({ productId: z.uuid() }); const availabilityQuerySchema = z.object({ quantity: z.coerce.number().int().positive().default(1), storeId: z.uuid().optional(), @@ -80,6 +81,49 @@ export async function registerInventoryRoutes( }, ); + // GET /inventory/product/:productId/availability — lookup first variant and return stock + const productAvailabilitySchema: FastifySchema = { + tags: ['Inventory'], + summary: 'Check availability by productId (público)', + params: { + type: 'object', + required: ['productId'], + properties: { productId: { type: 'string', format: 'uuid' } }, + }, + querystring: { + type: 'object', + properties: { + quantity: { type: 'integer', default: 1 }, + storeId: { type: 'string', format: 'uuid' }, + }, + }, + }; + app.get( + '/inventory/product/:productId/availability', + { schema: productAvailabilitySchema }, + async (request, reply) => { + const { productId } = parseJson(productParamSchema, request.params); + const { quantity, storeId } = parseJson(availabilityQuerySchema, request.query); + + // Lookup the first variant for this product + const variantResult = await deps.pool.query( + `SELECT id FROM product_variants WHERE product_id = $1 LIMIT 1`, + [productId], + ); + if (variantResult.rows.length === 0) { + throw new AppError(404, 'VARIANT_NOT_FOUND', 'No variant found for this product'); + } + const variantId = variantResult.rows[0].id; + + const availability = await inventory.checkAvailability( + variantId, + storeId ?? DEFAULT_STORE_ID, + quantity, + ); + return reply.send(availability); + }, + ); + const setStockSchema: FastifySchema = { tags: ['Inventory'], summary: 'Set available stock (admin)', diff --git a/project/storefront/src/components/add-to-cart.tsx b/project/storefront/src/components/add-to-cart.tsx index e4e55a0..3884755 100644 --- a/project/storefront/src/components/add-to-cart.tsx +++ b/project/storefront/src/components/add-to-cart.tsx @@ -41,7 +41,7 @@ export function AddToCart({ productId, productName, unitPriceCents, imageUrl }: const [added, setAdded] = useState(false); useEffect(() => { - fetch(`/api/inventory/${encodeURIComponent(productId)}/availability`) + fetch(`/api/inventory/product/${encodeURIComponent(productId)}/availability`) .then(async (r) => (r.ok ? await r.json() : { availableQuantity: 0 })) .then((j) => setStock(j.availableQuantity ?? 0)) .catch(() => setStock(0)); diff --git a/project/storefront/src/lib/api.ts b/project/storefront/src/lib/api.ts index 3b86db1..53d9595 100644 --- a/project/storefront/src/lib/api.ts +++ b/project/storefront/src/lib/api.ts @@ -77,14 +77,10 @@ export interface SearchProductsInput { offset?: number; } -const DEFAULT_API_BASE_URL = 'http://localhost:3000'; - +// Use env var if set, otherwise use relative URL (works for SSR and client-side via same-origin) function apiBaseUrl(): string { - return ( - process.env.API_BASE_URL ?? - process.env.NEXT_PUBLIC_API_BASE_URL ?? - DEFAULT_API_BASE_URL - ).replace(/\/$/, ''); + const base = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? ''; + return base.replace(/\/$/, ''); } function toQueryString(input: SearchProductsInput): string { diff --git a/work/runtime-status.json b/work/runtime-status.json index ef221fe..1783721 100644 --- a/work/runtime-status.json +++ b/work/runtime-status.json @@ -1,12 +1,12 @@ { - "feature_id": "F-204", - "stage": "qa_gate", - "agent": "qa", + "feature_id": "SHOP-STOCK", + "stage": "build", + "agent": "implementer", "action": "Añadiendo DELETE handler al proxy", "state": "running", "next_agent": "leader", "waiting_for": "Seleccionar una feature pending y actualizar este estado", - "updated_at": "2026-08-24T13:55:13Z", + "updated_at": "2026-08-24T13:57:50Z", "timeline": [ { "ts": "2026-08-24T05:21:22Z", @@ -119,6 +119,20 @@ "stage": "qa_gate", "state": "running", "message": "Añadiendo DELETE handler al proxy" + }, + { + "ts": "2026-08-24T13:56:05Z", + "agent": "implementer", + "stage": "build", + "state": "running", + "message": "Añadiendo DELETE handler al proxy" + }, + { + "ts": "2026-08-24T13:57:50Z", + "agent": "implementer", + "stage": "build", + "state": "running", + "message": "Añadiendo DELETE handler al proxy" } ] }