From c16c44090ec5ad48ea6ca84f0d21ed86f56f8b5f Mon Sep 17 00:00:00 2001 From: MarioVicente Date: Tue, 7 Oct 2025 16:56:26 -0600 Subject: [PATCH 1/4] Adding cards validations --- api/cars-post/index.js | 50 +++++++++++++++++++++++++++++------------- src/App.jsx | 35 ++++++++++++++++++++++++++--- src/api/cards.js | 6 ++++- 3 files changed, 72 insertions(+), 19 deletions(-) diff --git a/api/cars-post/index.js b/api/cars-post/index.js index 1b5f4fd..e52c84d 100644 --- a/api/cars-post/index.js +++ b/api/cars-post/index.js @@ -1,10 +1,11 @@ -import { CosmosClient } from "@azure/cosmos"; -import { randomUUID } from "crypto"; +const { CosmosClient } = require("@azure/cosmos"); +const { randomUUID } = require("crypto"); function getClientPrincipal(req) { const v = req.headers["x-ms-client-principal"]; if (!v) return null; - return JSON.parse(Buffer.from(v, "base64").toString("utf8")); + try { return JSON.parse(Buffer.from(v, "base64").toString("utf8")); } + catch { return null; } } const endpoint = process.env.COSMOS_ENDPOINT; @@ -12,16 +13,38 @@ const key = process.env.COSMOS_KEY; const databaseId = process.env.COSMOS_DB || "finance"; const containerId = process.env.COSMOS_CARDS_CONTAINER || "cards"; -export default async function (context, req) { +module.exports = async function (context, req) { try { const principal = getClientPrincipal(req); - if (!principal) { context.res = { status: 401, jsonBody: { error: "Unauthorized" } }; return; } - const userId = principal.userId; + if (!principal) return (context.res = { status: 401, body: { error: "Unauthorized" } }); + const userId = principal.userId; const body = req.body || {}; - const { name, limit, balance = 0, cutoffDay = 1, paymentDay = 10 } = body; - if (!name || typeof limit !== "number") { - context.res = { status: 400, jsonBody: { error: "name y limit (number) son requeridos" } }; + const name = (body.name || '').trim(); + const limit = Number(body.limit); + const balance = Number(body.balance || 0); + const cutoffDay = Number(body.cutoffDay || 1); + const paymentDay = Number(body.paymentDay || 10); + + if (!name || Number.isNaN(limit)) { + context.res = { status: 400, body: { error: "name y limit (number) son requeridos" } }; + return; + } + + const client = new CosmosClient({ endpoint, key }); + const container = client.database(databaseId).container(containerId); + + // Duplicado por nombre (case-insensitive) para el mismo usuario + const { resources: dup } = await container.items.query({ + query: "SELECT TOP 1 c.id FROM c WHERE c.userId = @user AND LOWER(c.name) = LOWER(@name)", + parameters: [ + { name: "@user", value: userId }, + { name: "@name", value: name } + ] + }).fetchAll(); + + if (dup && dup.length) { + context.res = { status: 409, body: { error: "DUPLICATE" } }; return; } @@ -37,13 +60,10 @@ export default async function (context, req) { createdAt: new Date().toISOString() }; - const client = new CosmosClient({ endpoint, key }); - const container = client.database(databaseId).container(containerId); const { resource } = await container.items.create(item, { disableAutomaticIdGeneration: true }); - - context.res = { status: 201, jsonBody: resource }; + context.res = { status: 201, body: resource }; } catch (e) { context.log.error(e); - context.res = { status: 500, jsonBody: { error: String(e.message || e) } }; + context.res = { status: 500, body: { error: String(e.message || e) } }; } -} +}; diff --git a/src/App.jsx b/src/App.jsx index 2aef27d..e6843d8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -26,6 +26,10 @@ function useIsMobile() { return isMobile; } +function normalizeName(s) { + return (s || "").trim().toLocaleLowerCase(); +} + const FinanceTracker = () => { // ====== Filtros (mes / histórico) ====== const [rangeMode, setRangeMode] = useState('month'); // 'month' | 'all' @@ -66,6 +70,8 @@ const FinanceTracker = () => { const [newCard, setNewCard] = useState({ name: '', limit: '', balance: '', cutoffDay: '', paymentDay: '' }); + const [addingCard, setAddingCard] = useState(false); + const [cardMsg, setCardMsg] = useState(null); // {type:'ok'|'err', text:string} | null const [activeTab, setActiveTab] = useState('dashboard'); const categories = { @@ -576,7 +582,18 @@ const FinanceTracker = () => { /> + {cardMsg && ( +

+ {cardMsg.text} +

+ )}
diff --git a/src/api/cards.js b/src/api/cards.js index 37c24d1..fd95ce4 100644 --- a/src/api/cards.js +++ b/src/api/cards.js @@ -11,7 +11,11 @@ export async function addCard(card) { credentials: 'include', body: JSON.stringify(card) }); - if (!r.ok) throw new Error('Error al crear tarjeta'); + if (r.status === 409) throw new Error('Ya existe una tarjeta con ese nombre.'); + if (!r.ok) { + const t = await r.text().catch(() => ''); + throw new Error(t || 'Error al crear tarjeta'); + } return r.json(); } From 018c62c70ce246aff83cebabfb6685791043db64 Mon Sep 17 00:00:00 2001 From: MarioVicente Date: Tue, 7 Oct 2025 17:20:45 -0600 Subject: [PATCH 2/4] Fixing adding card issue --- api/cars-post/index.js | 56 ++++++++++++++++++++++++++++++++++++------ src/api/cards.js | 32 +++++++++++------------- 2 files changed, 63 insertions(+), 25 deletions(-) diff --git a/api/cars-post/index.js b/api/cars-post/index.js index e52c84d..57dff74 100644 --- a/api/cars-post/index.js +++ b/api/cars-post/index.js @@ -1,3 +1,4 @@ +// api/cards-post/index.js const { CosmosClient } = require("@azure/cosmos"); const { randomUUID } = require("crypto"); @@ -14,27 +15,53 @@ const databaseId = process.env.COSMOS_DB || "finance"; const containerId = process.env.COSMOS_CARDS_CONTAINER || "cards"; module.exports = async function (context, req) { + const where = (step, extra) => context.log(`[cards-post] ${step}`, extra || ""); try { + where("start"); + const principal = getClientPrincipal(req); - if (!principal) return (context.res = { status: 401, body: { error: "Unauthorized" } }); + if (!principal) { + where("no principal"); + context.res = { status: 401, body: { error: "Unauthorized" } }; + return; + } const userId = principal.userId; const body = req.body || {}; - const name = (body.name || '').trim(); + // Normaliza/valida + const name = (body.name || "").toString().trim(); const limit = Number(body.limit); const balance = Number(body.balance || 0); const cutoffDay = Number(body.cutoffDay || 1); const paymentDay = Number(body.paymentDay || 10); - if (!name || Number.isNaN(limit)) { - context.res = { status: 400, body: { error: "name y limit (number) son requeridos" } }; + if (!name) { + context.res = { status: 400, body: { error: "El nombre es requerido" } }; + return; + } + if (!Number.isFinite(limit) || limit <= 0) { + context.res = { status: 400, body: { error: "El límite debe ser un número > 0" } }; + return; + } + if (!Number.isFinite(balance) || balance < 0) { + context.res = { status: 400, body: { error: "El saldo debe ser un número >= 0" } }; + return; + } + if (!Number.isInteger(cutoffDay) || cutoffDay < 1 || cutoffDay > 31) { + context.res = { status: 400, body: { error: "El día de corte debe estar entre 1 y 31" } }; + return; + } + if (!Number.isInteger(paymentDay) || paymentDay < 1 || paymentDay > 31) { + context.res = { status: 400, body: { error: "El día de pago debe estar entre 1 y 31" } }; return; } + where("cosmos init"); const client = new CosmosClient({ endpoint, key }); const container = client.database(databaseId).container(containerId); - // Duplicado por nombre (case-insensitive) para el mismo usuario + // Dup por nombre (case-insensitive) + userId + where("dup-check", { name, userId }); const { resources: dup } = await container.items.query({ query: "SELECT TOP 1 c.id FROM c WHERE c.userId = @user AND LOWER(c.name) = LOWER(@name)", parameters: [ @@ -44,7 +71,8 @@ module.exports = async function (context, req) { }).fetchAll(); if (dup && dup.length) { - context.res = { status: 409, body: { error: "DUPLICATE" } }; + where("dup-found", dup[0]); + context.res = { status: 409, body: { error: "Ya existe una tarjeta con ese nombre." } }; return; } @@ -60,10 +88,22 @@ module.exports = async function (context, req) { createdAt: new Date().toISOString() }; + where("create", item); const { resource } = await container.items.create(item, { disableAutomaticIdGeneration: true }); + + where("success", resource?.id); context.res = { status: 201, body: resource }; } catch (e) { - context.log.error(e); - context.res = { status: 500, body: { error: String(e.message || e) } }; + // Devuelve error detallado para verlo en el cliente + where("error", { message: e?.message, code: e?.code, body: e?.body }); + let msg = e?.message || "Error al crear tarjeta"; + // Si es error de Cosmos, intenta extraer más detalle + if (e?.body && typeof e.body === "string") { + try { + const jb = JSON.parse(e.body); + if (jb?.message) msg = jb.message; + } catch {} + } + context.res = { status: 500, body: { error: msg, code: e?.code || null } }; } }; diff --git a/src/api/cards.js b/src/api/cards.js index fd95ce4..dde7ab0 100644 --- a/src/api/cards.js +++ b/src/api/cards.js @@ -1,9 +1,3 @@ -export async function listCards() { - const r = await fetch('/api/cards', { credentials: 'include' }); - if (!r.ok) throw new Error('Error al leer tarjetas'); - return r.json(); -} - export async function addCard(card) { const r = await fetch('/api/cards', { method: 'POST', @@ -11,19 +5,23 @@ export async function addCard(card) { credentials: 'include', body: JSON.stringify(card) }); + if (r.status === 409) throw new Error('Ya existe una tarjeta con ese nombre.'); + if (!r.ok) { - const t = await r.text().catch(() => ''); - throw new Error(t || 'Error al crear tarjeta'); + // Muestra el error real + let msg = 'Error al crear tarjeta'; + try { + const ct = r.headers.get('content-type') || ''; + if (ct.includes('application/json')) { + const j = await r.json(); + if (j?.error) msg = j.error; + } else { + const t = await r.text(); + if (t) msg = t; + } + } catch {} + throw new Error(msg); } return r.json(); } - -export async function deleteCard(id) { - const r = await fetch(`/api/cards/${encodeURIComponent(id)}`, { - method: 'DELETE', - credentials: 'include' - }); - if (!r.ok && r.status !== 204) throw new Error('Error al borrar tarjeta'); - return true; -} From 34703cf5ad0e822119d44f49523bb29c29eb5f48 Mon Sep 17 00:00:00 2001 From: MarioVicente Date: Tue, 7 Oct 2025 17:37:16 -0600 Subject: [PATCH 3/4] Fixing adding card 2 --- src/api/cards.js | 19 +++++++++++++++++- src/hooks/useCards.js | 45 ++++++++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/api/cards.js b/src/api/cards.js index dde7ab0..8197702 100644 --- a/src/api/cards.js +++ b/src/api/cards.js @@ -1,3 +1,11 @@ +// src/api/cards.js + +export async function listCards() { + const r = await fetch('/api/cards', { credentials: 'include' }); + if (!r.ok) throw new Error('Error al leer tarjetas'); + return r.json(); +} + export async function addCard(card) { const r = await fetch('/api/cards', { method: 'POST', @@ -9,7 +17,7 @@ export async function addCard(card) { if (r.status === 409) throw new Error('Ya existe una tarjeta con ese nombre.'); if (!r.ok) { - // Muestra el error real + // Muestra el error real del servidor let msg = 'Error al crear tarjeta'; try { const ct = r.headers.get('content-type') || ''; @@ -25,3 +33,12 @@ export async function addCard(card) { } return r.json(); } + +export async function deleteCard(id) { + const r = await fetch(`/api/cards/${encodeURIComponent(id)}`, { + method: 'DELETE', + credentials: 'include' + }); + if (!r.ok && r.status !== 204) throw new Error('Error al borrar tarjeta'); + return true; +} diff --git a/src/hooks/useCards.js b/src/hooks/useCards.js index 7f97327..611049d 100644 --- a/src/hooks/useCards.js +++ b/src/hooks/useCards.js @@ -1,41 +1,46 @@ import { useEffect, useState, useCallback } from 'react'; -import * as api from '../api/cards'; +import { listCards, addCard, deleteCard } from '../api/cards'; // 👈 asegúrate de este path/nombres export function useCards(userId) { - const [cards, setCards] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const [cards, setCards] = useState([]); + const [loading, setLoad] = useState(false); + const [error, setError] = useState(null); + // carga inicial / cuando cambia el usuario useEffect(() => { - let cancelled = false; + let alive = true; async function load() { if (!userId) { setCards([]); return; } - setLoading(true); setError(null); + setLoad(true); + setError(null); try { - const data = await api.listCards(); - if (!cancelled) setCards(data); + const data = await listCards(); // 👈 llamada a API + if (alive) setCards(Array.isArray(data) ? data : []); } catch (e) { - if (!cancelled) setError(e); + if (alive) setError(e); } finally { - if (!cancelled) setLoading(false); + if (alive) setLoad(false); } } load(); - return () => { cancelled = true; }; + return () => { alive = false; }; }, [userId]); + // crear const create = useCallback(async (card) => { - if (!userId) throw new Error('No usuario'); - const c = await api.addCard(card); - setCards(prev => [c, ...prev]); - return c; - }, [userId]); + setError(null); + const created = await addCard(card); // 👈 llamada a API + // optimista / o puedes recargar listCards() si prefieres + setCards(prev => [created, ...prev]); + return created; + }, []); + // borrar const remove = useCallback(async (id) => { - if (!userId) throw new Error('No usuario'); - await api.deleteCard(id); - setCards(prev => prev.filter(x => x.id !== id)); - }, [userId]); + setError(null); + await deleteCard(id); // 👈 llamada a API + setCards(prev => prev.filter(c => c.id !== id)); + }, []); return { cards, loading, error, create, remove }; } From 657f979fcd763e33358eed2a75e5ccf5ad77c945 Mon Sep 17 00:00:00 2001 From: MarioVicente Date: Wed, 8 Oct 2025 07:52:24 -0600 Subject: [PATCH 4/4] renaming cards-post --- api/{cars-post => cards-post}/function.json | 0 api/{cars-post => cards-post}/index.js | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename api/{cars-post => cards-post}/function.json (100%) rename api/{cars-post => cards-post}/index.js (100%) diff --git a/api/cars-post/function.json b/api/cards-post/function.json similarity index 100% rename from api/cars-post/function.json rename to api/cards-post/function.json diff --git a/api/cars-post/index.js b/api/cards-post/index.js similarity index 100% rename from api/cars-post/index.js rename to api/cards-post/index.js