From 91f67482d57926d0e73d3f79cb591cdca79fb355 Mon Sep 17 00:00:00 2001 From: max Date: Fri, 16 May 2025 12:56:47 -0600 Subject: [PATCH 01/17] merge develop --- .../eliminarCliente.controller.js | 24 +- .../consultarLista.controller.js | 6 - .../consultarListaGrupos.controller.js | 6 - .../importarEmpleados.controller.js | 11 +- .../Repositorios/repositorioEmpleados.js | 4 - .../repositorioGrupoDeEmpleados.js | 4 - .../importarEmpleados.routes.js | 2 + .../Controladores/crearProducto.controller.js | 205 ++++++++++++++++++ .../Repositorios/repositorioCrearOpcion.js | 33 +++ .../Repositorios/repositorioCrearProducto.js | 62 ++++++ .../Repositorios/repositorioCrearVariante.js | 32 +++ .../Repositorios/repositorioProductoImagen.js | 43 ++++ .../Repositorios/repositorioVarianteImagen.js | 42 ++++ .../consultarProductos.routes.js | 2 +- .../RutasIndividuales/crearProducto.routes.js | 116 ++++++++++ .../eliminarProducto.routes.js | 2 +- Productos/Rutas/indexProductos.routes.js | 11 +- .../consultarProveedores.controller.js | 51 +++++ .../crearProveedor.controller.js | 50 +++++ .../repositorioConsultarProveedores.js | 25 +++ .../Repositorios/repositorioCrearProvedor.js | 46 ++++ .../consultarProveedores.routes.js | 126 +++++++++++ .../crearProveedor.routes.js | 125 +++++++++++ Proveedores/Rutas/indexProveedores.routes.js | 13 ++ .../Datos/Repositorios/repositorioCrearRol.js | 6 +- Utilidades/Constantes/consultasImagenes.js | 6 + Utilidades/Constantes/consultasOpciones.js | 6 + Utilidades/Constantes/consultasProductos.js | 18 +- Utilidades/Constantes/consultasProveedores.js | 10 + Utilidades/Constantes/consultasUsuarios.js | 8 +- Utilidades/Constantes/consultasVariantes.js | 10 + Utilidades/Constantes/mensajesProductos.js | 47 +++- Utilidades/Constantes/mensajesProveedores.js | 53 +++++ Utilidades/Constantes/rutas.js | 8 + .../Validaciones/validarOpciones.js | 86 ++++++++ .../Validaciones/validarProducto.js | 138 ++++++++++++ .../Validaciones/validarProveedor.js | 84 +++++++ .../Validaciones/validarVariante.js | 44 ++++ Utilidades/Servicios/enviarS3.js | 6 +- app.js | 2 + jsconfig.json | 7 + package.json | 7 + 42 files changed, 1546 insertions(+), 41 deletions(-) create mode 100644 Productos/Controladores/crearProducto.controller.js create mode 100644 Productos/Datos/Repositorios/repositorioCrearOpcion.js create mode 100644 Productos/Datos/Repositorios/repositorioCrearProducto.js create mode 100644 Productos/Datos/Repositorios/repositorioCrearVariante.js create mode 100644 Productos/Datos/Repositorios/repositorioProductoImagen.js create mode 100644 Productos/Datos/Repositorios/repositorioVarianteImagen.js create mode 100644 Productos/Rutas/RutasIndividuales/crearProducto.routes.js create mode 100644 Proveedores/Controladores/consultarProveedores.controller.js create mode 100644 Proveedores/Controladores/crearProveedor.controller.js create mode 100644 Proveedores/Datos/Repositorios/repositorioConsultarProveedores.js create mode 100644 Proveedores/Datos/Repositorios/repositorioCrearProvedor.js create mode 100644 Proveedores/Rutas/RutasIndividuales/consultarProveedores.routes.js create mode 100644 Proveedores/Rutas/RutasIndividuales/crearProveedor.routes.js create mode 100644 Proveedores/Rutas/indexProveedores.routes.js create mode 100644 Utilidades/Constantes/consultasImagenes.js create mode 100644 Utilidades/Constantes/consultasOpciones.js create mode 100644 Utilidades/Constantes/consultasProveedores.js create mode 100644 Utilidades/Constantes/consultasVariantes.js create mode 100644 Utilidades/Constantes/mensajesProveedores.js create mode 100644 Utilidades/Intermediarios/Validaciones/validarOpciones.js create mode 100644 Utilidades/Intermediarios/Validaciones/validarProducto.js create mode 100644 Utilidades/Intermediarios/Validaciones/validarProveedor.js create mode 100644 Utilidades/Intermediarios/Validaciones/validarVariante.js diff --git a/Clientes/Controladores/eliminarCliente.controller.js b/Clientes/Controladores/eliminarCliente.controller.js index 686f7d68..9131470e 100644 --- a/Clientes/Controladores/eliminarCliente.controller.js +++ b/Clientes/Controladores/eliminarCliente.controller.js @@ -1,8 +1,13 @@ +// Importaciones necesarias const repositorio = require('@altertex/cli/repos/repositorioEliminarCliente'); const MENSAJES_CLIENTES = require('@altertex/util/const/mensajesClientes'); +const extraerNombreArchivoS3 = require('@altertex/util/ser/extraerNombreArchivoS3'); +const eliminarImagenS3 = require('@altertex/util/ser/eliminarImagenS3'); +const correrQuery = require('@altertex/util/ser/correrQuery'); +const CONSULTAS_CLIENTES = require('@altertex/util/const/consultasClientes'); /** - * Controlador para eliminar un cliente de la base de datos. + * Controlador para eliminar un cliente de la base de datos y su imagen de S3. * @see [RF15 - Elimina Cliente](https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF15) * * @async @@ -28,6 +33,18 @@ exports.eliminarCliente = async (req, res) => { .json({ mensaje: MENSAJES_CLIENTES.CLIENTE_INVALIDO.mensaje }); } + // Obtener nombre de la imagen asociada (si existe) + let nombreImagen = ''; + try { + const resultadoImagen = await correrQuery(CONSULTAS_CLIENTES.OBTENER_NOMBRE_IMAGEN, [idCliente]); + if (resultadoImagen.length > 0 && resultadoImagen[0].urlImagen) { + nombreImagen = extraerNombreArchivoS3(resultadoImagen[0].urlImagen); + } + } catch { + // console.error('Error al obtener nombre de imagen:', error); + } + + // Eliminar cliente const resultado = await repositorio.eliminarClientePorId(idCliente); if (resultado.affectedRows === 0) { @@ -36,6 +53,11 @@ exports.eliminarCliente = async (req, res) => { .json({ mensaje: MENSAJES_CLIENTES.CLIENTE_NO_ENCONTRADO.mensaje }); } + // Eliminar imagen si hay nombre válido + if (nombreImagen) { + await eliminarImagenS3('clientes/', nombreImagen); + } + return res .status(MENSAJES_CLIENTES.CLIENTE_ELIMINADO.codigo) .json({ mensaje: MENSAJES_CLIENTES.CLIENTE_ELIMINADO.mensaje }); diff --git a/Empleados/Controladores/consultarLista.controller.js b/Empleados/Controladores/consultarLista.controller.js index df7d3a20..433e0e00 100644 --- a/Empleados/Controladores/consultarLista.controller.js +++ b/Empleados/Controladores/consultarLista.controller.js @@ -33,12 +33,6 @@ exports.consultarLista = async (req, res) => { try { const resultados = await repositorio.obtenerEmpleados(idCliente); - if (!resultados || resultados.length === 0) { - return res - .status(MENSAJES_EMPLEADOS.SIN_RESULTADOS.codigo) - .json({ mensaje: MENSAJES_EMPLEADOS.SIN_RESULTADOS.mensaje }); - } - return res.status(MENSAJES_EMPLEADOS.CONSULTA_EXITOSA.codigo).json({ mensaje: MENSAJES_EMPLEADOS.CONSULTA_EXITOSA.mensaje, empleados: resultados, diff --git a/Empleados/Controladores/consultarListaGrupos.controller.js b/Empleados/Controladores/consultarListaGrupos.controller.js index e59897f8..c26fa618 100644 --- a/Empleados/Controladores/consultarListaGrupos.controller.js +++ b/Empleados/Controladores/consultarListaGrupos.controller.js @@ -33,12 +33,6 @@ exports.consultarLista = async (req, res) => { try { const resultados = await repositorio.obtenerGrupoDeEmpleados(idCliente); - if (!resultados || resultados.length === 0) { - return res - .status(MENSAJES_GRUPO_EMPLEADOS.SIN_RESULTADOS.codigo) - .json({ mensaje: MENSAJES_GRUPO_EMPLEADOS.SIN_RESULTADOS.mensaje }); - } - return res.status(MENSAJES_GRUPO_EMPLEADOS.CONSULTA_EXITOSA.codigo).json({ mensaje: MENSAJES_GRUPO_EMPLEADOS.CONSULTA_EXITOSA.mensaje, grupoEmpleados: resultados, diff --git a/Empleados/Controladores/importarEmpleados.controller.js b/Empleados/Controladores/importarEmpleados.controller.js index 104989d4..b2d4b3fc 100644 --- a/Empleados/Controladores/importarEmpleados.controller.js +++ b/Empleados/Controladores/importarEmpleados.controller.js @@ -41,6 +41,7 @@ const MENSAJES_USUARIOS = require('@altertex/util/const/mensajesUsuarios'); * */ exports.importarEmpleados = async (req, res) => { + const idCliente = parseInt(req.user.clienteSeleccionado); const empleados = req.body; if (!Array.isArray(empleados) || empleados.length === 0) { @@ -99,8 +100,8 @@ exports.importarEmpleados = async (req, res) => { continue; } - if (!datos.idCliente) { - errores.push({ fila, error: 'El cliente es requerido' }); + if (typeof datos.idCliente !== 'undefined' && datos.idCliente !== '' && datos.idCliente !== null) { + errores.push({ fila, error: 'El cliente no debe ser incluido en el archivo' }); continue; } @@ -175,8 +176,12 @@ exports.importarEmpleados = async (req, res) => { }); } + for (const empleado of listaParaImportar) { + empleado.idCliente = idCliente; + } + try { - await repositorio.importarEmpleadosMasivo(empleados); + await repositorio.importarEmpleadosMasivo(listaParaImportar); } catch (error) { errores.push({ fila: "N/A", diff --git a/Empleados/Datos/Repositorios/repositorioEmpleados.js b/Empleados/Datos/Repositorios/repositorioEmpleados.js index 7c0e1702..99dcbd3b 100644 --- a/Empleados/Datos/Repositorios/repositorioEmpleados.js +++ b/Empleados/Datos/Repositorios/repositorioEmpleados.js @@ -21,10 +21,6 @@ exports.obtenerEmpleados = async (idCliente) => { try { const empleados = await correrQuery(query, [idCliente]); - if (!empleados || empleados.length === 0) { - throw new Error('No hay empleados'); - } - return empleados; } catch (error) { console.error('Error al obtener los empleados:', error); diff --git a/Empleados/Datos/Repositorios/repositorioGrupoDeEmpleados.js b/Empleados/Datos/Repositorios/repositorioGrupoDeEmpleados.js index 31fba0d5..b7180ad0 100644 --- a/Empleados/Datos/Repositorios/repositorioGrupoDeEmpleados.js +++ b/Empleados/Datos/Repositorios/repositorioGrupoDeEmpleados.js @@ -21,10 +21,6 @@ exports.obtenerGrupoDeEmpleados = async (idCliente) => { try { const gruposDeEmpleados = await correrQuery(query, [idCliente]); - if (!gruposDeEmpleados || gruposDeEmpleados.length === 0) { - throw new Error('No hay grupos de empleados'); - } - return gruposDeEmpleados; } catch (error) { console.error('Error al obtener el grupo de empleados:', error); diff --git a/Empleados/Rutas/RutasIndividuales/importarEmpleados.routes.js b/Empleados/Rutas/RutasIndividuales/importarEmpleados.routes.js index 4e281537..351ca763 100644 --- a/Empleados/Rutas/RutasIndividuales/importarEmpleados.routes.js +++ b/Empleados/Rutas/RutasIndividuales/importarEmpleados.routes.js @@ -8,6 +8,7 @@ const autorizarToken = require('@altertex/util/inter/autorizarToken'); const verificarPermisos = require('@altertex/util/inter/verificarPermisos'); const PERMISOS = require('@altertex/util/const/permisos'); const RUTAS = require('@altertex/util/const/rutas'); +const validarYSanitizar = require("@altertex/util/inter/validarYSanitizar"); /** * @swagger @@ -159,6 +160,7 @@ ruteador.post( RUTAS.EMPLEADOS.IMPORTAR_EMPLEADOS, revisarApiKey(), autorizarToken, + validarYSanitizar, verificarPermisos(PERMISOS.IMPORTAR_EMPLEADOS), controlador.importarEmpleados ); diff --git a/Productos/Controladores/crearProducto.controller.js b/Productos/Controladores/crearProducto.controller.js new file mode 100644 index 00000000..c06a54de --- /dev/null +++ b/Productos/Controladores/crearProducto.controller.js @@ -0,0 +1,205 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +const multer = require('multer'); +const enviarS3 = require('@altertex/util/ser/enviarS3'); +const MENSAJES_PRODUCTOS = require('@altertex/util/const/mensajesProductos'); +const validarProducto = require('@altertex/util/vali/validarProducto'); +const validarVariante = require('@altertex/util/vali/validarVariante'); +const validarOpciones = require('@altertex/util/vali/validarOpciones'); +const repositorioCrearProducto = require('@altertex/pro/repos/repositorioCrearProducto'); +const repositorioProductoImagen = require('@altertex/pro/repos/repositorioProductoImagen'); +const repositorioCrearVariante = require('@altertex/pro/repos/repositorioCrearVariante'); +const repositorioVarianteImagen = require('@altertex/pro/repos/repositorioVarianteImagen'); +const repositorioCrearOpcion = require('@altertex/pro/repos/repositorioCrearOpcion'); +const conexion = require('@altertex/util/bd/db').promise(); + +const upload = multer({ storage: multer.memoryStorage() }); + +/** + * Controlador para crear un producto. + * + * Este controlador maneja la creación de un producto, incluyendo la validación de datos y el manejo de archivos de imagen. + * Realiza la creación del proveedor, producto, variantes y las imágenes asociadas, almacenándolas en un servicio S3. + * Si ocurre algún error en cualquier parte del proceso, se realiza un rollback de la transacción. + * + * @param {object} req - El objeto de solicitud. + * @param {object} req.user - El usuario autenticado. + * @param {string} req.user.clienteSeleccionado - El ID del cliente seleccionado por el usuario. + * @param {string} req.body.proveedor - EL ID del proveedor seleccionado por el usuario. + * @param {object} req.body - El cuerpo de la solicitud. + * @param {string} req.body.producto - Información del producto en formato JSON. + * @param {string} req.body.variantes - Información de las variantes del producto en formato JSON. + * @param {string} req.body.mapaImagenes - Información del mapa de imagenes de las variantes en formato JSON. + * @param {object} req.files - Archivos enviados en la solicitud. + * @param {Array} req.files.imagenProducto - La imagen principal del producto. + * @param {Array} req.files.imagenesVariante - Las imágenes asociadas a las variantes del producto. + * + * @param {object} res - El objeto de respuesta. + * @param {Function} res.status - Método para establecer el código de estado HTTP en la respuesta. + * @param {Function} res.json - Método para enviar una respuesta JSON. + * + * @returns {object} Retorna un mensaje de éxito si el producto se crea correctamente, o un mensaje de error si falla alguna validación o proceso. + * + * @example + * // Ejemplo de cómo usar el controlador + * // Se hace una solicitud POST a /crear-producto con el cuerpo de la solicitud que contiene el proveedor, producto, variantes y archivos de imagen. + * + * // Respuesta exitosa: + * res.status(200).json({ mensaje: 'Producto creado correctamente' }); + * + * // Respuesta de error: + * res.status(400).json({ mensaje: 'Error al crear producto', error: 'Error específico' }); + */ +exports.crearProducto = [ + upload.fields([ + { name: 'imagenProducto', maxCount: 1 }, + { name: 'imagenesVariante', maxCount: 100 }, + ]), + + async (req, res) => { + const idCliente = parseInt(req.user.clienteSeleccionado); + const producto = JSON.parse(req.body.producto); + const variantes = JSON.parse(req.body.variantes); + const mapaImagenes = JSON.parse(req.body.mapaImagenes); + const imagenProducto = req.files.imagenProducto ? req.files.imagenProducto[0] : null; + const imagenesVariante = req.files.imagenesVariante || []; + + // prettier-ignore + if ( + !idCliente + || !mapaImagenes + || !producto + || !Array.isArray(variantes) + || variantes.length === 0 + || !imagenProducto + || !imagenesVariante + ) { + return res.status(MENSAJES_PRODUCTOS.PARAMETROS_INVALIDOS.codigo).json({ + mensaje: MENSAJES_PRODUCTOS.PARAMETROS_INVALIDOS.mensaje, + }); + } + + const errorProducto = validarProducto(producto); + if (errorProducto) { + return res.status(MENSAJES_PRODUCTOS.PARAMETROS_INVALIDOS.codigo).json({ + mensaje: errorProducto.error, + }); + } + + if (imagenesVariante.length !== mapaImagenes.length) { + return res.status(MENSAJES_PRODUCTOS.PARAMETROS_INVALIDOS.codigo).json({ + mensaje: 'La cantidad de imágenes no coincide con el mapa de imágenes', + }); + } + + try { + await conexion.beginTransaction(); + + const idProducto = await repositorioCrearProducto.crearProducto(idCliente, producto); + if (!idProducto) { + throw new Error('Error al crear producto'); + } + + const varianteIdMap = {}; + const variantesPromises = variantes.map(async (variante) => { + const errorVariante = validarVariante({ + nombreVariante: variante.nombreVariante, + descripcion: variante.descripcion, + }); + if (errorVariante) { + throw new Error(errorVariante.error); + } + + const idVariante = await repositorioCrearVariante.crearVariante(idProducto, variante); + if (!idVariante) { + throw new Error('Error al crear variante'); + } + + varianteIdMap[variante.identificador] = { + id: idVariante, + nombre: variante.nombreVariante, + }; + + const errorOpciones = validarOpciones(variante.opciones); + if (errorOpciones) { + throw new Error(errorOpciones.error); + } + + await repositorioCrearOpcion.crearOpcion(idVariante, variante.opciones); + }); + + await Promise.all(variantesPromises); + + const urlImagenProductoPromise = imagenProducto + ? enviarS3({ + Bucket: process.env.AWS_BUCKET_NAME, + Key: `productos/${imagenProducto.originalname}`, + Body: imagenProducto.buffer, + ContentType: imagenProducto.mimetype, + }) + : Promise.resolve(null); + + // prettier-ignore + const urlImagenVariantePromises = imagenesVariante.map((imagenVariante) => + enviarS3({ + Bucket: process.env.AWS_BUCKET_NAME, + Key: `productos/${imagenVariante.originalname}`, + Body: imagenVariante.buffer, + ContentType: imagenVariante.mimetype, + })); + + const [urlImagenProducto, ...urlImagenVariantes] = await Promise.all([ + urlImagenProductoPromise, + ...urlImagenVariantePromises, + ]); + + if ((imagenProducto && !urlImagenProducto) || urlImagenVariantes.includes(null)) { + throw new Error('Error al subir imágenes al servidor'); + } + + if (imagenProducto) { + await repositorioProductoImagen.crearImagen( + idProducto, + imagenProducto.originalname, + producto.nombreComun + ); + } + + const imagenesVariantePromises = imagenesVariante.map(async (imagen, index) => { + const { idVariante: tempIdVariante } = mapaImagenes[index]; + const varianteInfo = varianteIdMap[tempIdVariante]; + + if (!varianteInfo) { + throw new Error(`Variante con ID temporal ${tempIdVariante} no encontrada`); + } + + await repositorioVarianteImagen.crearImagen( + varianteInfo.id, + imagen.originalname, + varianteInfo.nombre + ); + }); + + await Promise.all(imagenesVariantePromises); + + await conexion.commit(); + return res.status(200).json({ mensaje: 'Producto creado correctamente' }); + } catch (error) { + await conexion.rollback(); + + let errorMensaje = MENSAJES_PRODUCTOS.ERROR_CREAR_PRODUCTO; + + if (error.message.includes('Error al subir imágenes al servidor')) { + errorMensaje = MENSAJES_PRODUCTOS.ERROR_ENVIAR_IMAGENES_S3; + } else if (error.message.includes('Error al crear variante')) { + errorMensaje = MENSAJES_PRODUCTOS.ERROR_CREAR_VARIANTE; + } else if (error.message.includes('Error al asociar imagen con variante')) { + errorMensaje = MENSAJES_PRODUCTOS.ERROR_CREAR_IMAGEN_VARIANTE; + } + + return res.status(errorMensaje.codigo).json({ + mensaje: errorMensaje.mensaje, + error: error.message, + }); + } + }, +]; diff --git a/Productos/Datos/Repositorios/repositorioCrearOpcion.js b/Productos/Datos/Repositorios/repositorioCrearOpcion.js new file mode 100644 index 00000000..0d302a9b --- /dev/null +++ b/Productos/Datos/Repositorios/repositorioCrearOpcion.js @@ -0,0 +1,33 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +const correrQuery = require('@altertex/util/ser/correrQuery'); +const consultas = require('@altertex/util/const/consultasOpciones'); + +/** + * Crea una o varias opciones relacionadas con una variante. + * + * @async + * @function crearOpcion + * @param {number} idVariante - ID de la variante a la que se le agregarán las opciones. + * @param {Array} opciones - Un arreglo con los objetos de opciones que se desean agregar. + * @returns {Promise} - Una promesa que se resuelve cuando todas las opciones se crean exitosamente. + */ +exports.crearOpcion = async (idVariante, opciones) => { + const query = consultas.CREAR; + + const promises = opciones.map(async (opcion) => { + const params = [ + idVariante, + opcion.cantidad, + opcion.valorOpcion, + opcion.SKUautomatico, + opcion.SKUcomercial, + opcion.costoAdicional, + opcion.descuento, + opcion.estado, + ]; + + await correrQuery(query, params); + }); + + await Promise.all(promises); +}; diff --git a/Productos/Datos/Repositorios/repositorioCrearProducto.js b/Productos/Datos/Repositorios/repositorioCrearProducto.js new file mode 100644 index 00000000..21b9df48 --- /dev/null +++ b/Productos/Datos/Repositorios/repositorioCrearProducto.js @@ -0,0 +1,62 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +const correrQuery = require('@altertex/util/ser/correrQuery'); +const consultas = require('@altertex/util/const/consultasProductos'); + +/** + * Crea un nuevo producto en la base de datos. + * + * Esta función ejecuta una consulta SQL para insertar un producto en la base de datos, + * utilizando los parámetros proporcionados. Devuelve el ID del producto recién creado + * en caso de éxito, o un array vacío si ocurre algún error durante la operación. + * + * @param {number} clienteSeleccionado - ID del cliente para el cual se está creando el producto. + * @param {object} producto - Objeto que contiene la información del producto. + * @param {number} producto.idProveedor - ID del proveedor asociado al producto. + * @param {string} producto.nombreComun - Nombre común del producto. + * @param {string} producto.nombreComercial - Nombre comercial del producto. + * @param {string} producto.descripcion - Descripción del producto. + * @param {string} producto.marca - Marca del producto. + * @param {string} producto.modelo - Modelo del producto. + * @param {string} producto.tipoProducto - Tipo del producto. + * @param {number} producto.precioPuntos - Precio en puntos del producto. + * @param {number} producto.precioCliente - Precio para el cliente del producto. + * @param {number} producto.precioVenta - Precio de venta del producto. + * @param {number} producto.costo - Costo de producción del producto. + * @param {number} producto.impuesto - Impuesto aplicado al producto. + * @param {number} producto.descuento - Descuento aplicado al producto. + * @param {string} producto.estado - Estado del producto (activo/inactivo). + * @param {boolean} producto.envio - Indica si el producto es apto para envío. + * + * @returns {number|Array} El ID del producto recién creado en caso de éxito, o un array vacío en caso de error. + */ +exports.crearProducto = async (clienteSeleccionado, producto) => { + const query = consultas.CREAR; + const parametros = [ + clienteSeleccionado, + producto.idProveedor, + producto.nombreComun, + producto.nombreComercial, + producto.descripcion, + producto.marca, + producto.modelo, + producto.tipoProducto, + producto.precioPuntos, + producto.precioCliente, + producto.precioVenta, + producto.costo, + producto.impuesto, + producto.descuento, + producto.estado, + producto.envio, + ]; + + try { + const resultados = await correrQuery(query, parametros); + + const idProducto = resultados.insertId; + return idProducto; + } catch (error) { + console.error('Error al crear producto:', error); + return []; + } +}; diff --git a/Productos/Datos/Repositorios/repositorioCrearVariante.js b/Productos/Datos/Repositorios/repositorioCrearVariante.js new file mode 100644 index 00000000..3170a92b --- /dev/null +++ b/Productos/Datos/Repositorios/repositorioCrearVariante.js @@ -0,0 +1,32 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +const correrQuery = require('@altertex/util/ser/correrQuery'); +const consultas = require('@altertex/util/const/consultasVariantes'); + +/** + * Crea una nueva variante para un producto en la base de datos. + * + * Esta función ejecuta una consulta SQL para insertar una variante asociada a un producto, + * utilizando los parámetros proporcionados. Devuelve el ID de la variante recién creada + * en caso de éxito, o un array vacío si ocurre algún error durante la operación. + * + * @param {number} idProducto - ID del producto al que se le va a agregar la variante. + * @param {object} variante - Objeto que contiene la información de la variante. + * @param {string} variante.nombreVariante - Nombre de la variante (por ejemplo, color, tamaño). + * @param {string} variante.descripcion - Descripción de la variante. + * + * @returns {number|Array} El ID de la variante recién creada en caso de éxito, o un array vacío en caso de error. + */ +exports.crearVariante = async (idProducto, variante) => { + const query = consultas.CREAR; + const parametros = [idProducto, variante.nombreVariante, variante.descripcion]; + + try { + const resultados = await correrQuery(query, parametros); + + const idVariante = resultados.insertId; + return idVariante; + } catch (error) { + console.error('Error al crear variante:', error); + return []; + } +}; diff --git a/Productos/Datos/Repositorios/repositorioProductoImagen.js b/Productos/Datos/Repositorios/repositorioProductoImagen.js new file mode 100644 index 00000000..bc1cb8d0 --- /dev/null +++ b/Productos/Datos/Repositorios/repositorioProductoImagen.js @@ -0,0 +1,43 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +const correrQuery = require('@altertex/util/ser/correrQuery'); +const consultasProductos = require('@altertex/util/const/consultasProductos'); +const consultasImagenes = require('@altertex/util/const/consultasImagenes'); + +/** + * Crea una imagen en la base de datos y la asocia a un producto. + * + * Esta función realiza dos operaciones en una transacción: + * - Inserta una nueva imagen en la tabla de imágenes. + * - Crea una relación entre la imagen y el producto correspondiente. + * + * @param {number} idProducto - ID del producto al que se asociará la imagen. + * @param {string} urlImagenProducto - URL o ruta de la imagen del producto almacenada. + * @param {string} nombreComun - Nombre común o descriptivo asociado al producto. + * + * @returns {Promise} El resultado de la inserción de la imagen (incluyendo `insertId`) si es exitoso, o un arreglo vacío en caso de error. + */ +exports.crearImagen = async (idProducto, urlImagenProducto, nombreComun) => { + const queryImagen = consultasImagenes.CREAR; + const queryRelacionImagenProducto = consultasProductos.CREAR_IMAGEN_PRODUCTO; + const parametrosImagen = [urlImagenProducto, 'Imagen Producto', nombreComun]; + + const conexion = require('@altertex/util/bd/db').promise(); + + try { + await conexion.beginTransaction(); + + const resultadoImagen = await correrQuery(queryImagen, parametrosImagen); + const idImagen = resultadoImagen.insertId; + + const parametrosRelacion = [idImagen, idProducto]; + await correrQuery(queryRelacionImagenProducto, parametrosRelacion); + + await conexion.commit(); + + return resultadoImagen; + } catch (error) { + console.error('Error al crear o asociar la imagen:', error); + await conexion.rollback(); + return []; + } +}; diff --git a/Productos/Datos/Repositorios/repositorioVarianteImagen.js b/Productos/Datos/Repositorios/repositorioVarianteImagen.js new file mode 100644 index 00000000..a15a94ef --- /dev/null +++ b/Productos/Datos/Repositorios/repositorioVarianteImagen.js @@ -0,0 +1,42 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +const conexion = require('@altertex/util/bd/db').promise(); +const correrQuery = require('@altertex/util/ser/correrQuery'); +const consultasVariantes = require('@altertex/util/const/consultasVariantes'); +const consultasImagenes = require('@altertex/util/const/consultasImagenes'); + +/** + * Crea una imagen en la base de datos y la asocia a una variante de producto. + * + * Esta función realiza dos operaciones dentro de una transacción: + * - Inserta una nueva imagen en la tabla de imágenes. + * - Crea una relación entre la imagen y la variante correspondiente. + * + * @param {number} idVariante - ID de la variante a la que se asociará la imagen. + * @param {string} urlImagenVariante - URL o ruta de la imagen de la variante almacenada. + * @param {string} nombreComun - Nombre común o descriptivo asociado a la variante. + * + * @returns {Promise} El resultado de la inserción de la imagen (incluyendo `insertId`) si es exitoso, o un arreglo vacío en caso de error. + */ +exports.crearImagen = async (idVariante, urlImagenVariante, nombreComun) => { + const queryImagen = consultasImagenes.CREAR; + const queryRelacionImagenVariante = consultasVariantes.CREAR_IMAGEN_VARIANTE; + const parametrosImagen = [urlImagenVariante, 'Imagen Variante', nombreComun]; + + try { + await conexion.beginTransaction(); + + const resultadoImagen = await correrQuery(queryImagen, parametrosImagen); + const idImagen = resultadoImagen.insertId; + + const parametrosRelacion = [idImagen, idVariante]; + await correrQuery(queryRelacionImagenVariante, parametrosRelacion); + + await conexion.commit(); + + return resultadoImagen; + } catch (error) { + console.error('Error al crear o asociar la imagen:', error); + await conexion.rollback(); + return []; + } +}; diff --git a/Productos/Rutas/RutasIndividuales/consultarProductos.routes.js b/Productos/Rutas/RutasIndividuales/consultarProductos.routes.js index 6e706544..985e230a 100644 --- a/Productos/Rutas/RutasIndividuales/consultarProductos.routes.js +++ b/Productos/Rutas/RutasIndividuales/consultarProductos.routes.js @@ -1,4 +1,4 @@ -//RF[27] Consulta Lista de Productos - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF27] +//RF27 Consulta Lista de Productos - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF27 const express = require('express'); const ruteador = express.Router(); const controlador = require('@altertex/pro/ctrl/consultarProductos.controller'); diff --git a/Productos/Rutas/RutasIndividuales/crearProducto.routes.js b/Productos/Rutas/RutasIndividuales/crearProducto.routes.js new file mode 100644 index 00000000..f50e7394 --- /dev/null +++ b/Productos/Rutas/RutasIndividuales/crearProducto.routes.js @@ -0,0 +1,116 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +const express = require('express'); +const ruteador = express.Router(); +const controlador = require('@altertex/pro/ctrl/crearProducto.controller'); +const revisarApiKey = require('@altertex/util/inter/revisarApiKey'); +const autorizarToken = require('@altertex/util/inter/autorizarToken'); +const verificarPermisos = require('@altertex/util/inter/verificarPermisos'); + +const PERMISOS = require('@altertex/util/const/permisos'); +const RUTAS = require('@altertex/util/const/rutas'); + +/** + * @swagger + * /api/productos/crear: + * post: + * summary: Crear un nuevo producto + * description: Crea un producto con sus variantes e imágenes asociadas + * tags: [Productos] + * security: + * - ApiKeyAuth: [] + * - BearerAuth: [] + * consumes: + * - multipart/form-data + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * required: + * - producto + * - variantes + * - mapaImagenes + * - imagenProducto + * - imagenesVariante + * properties: + * producto: + * type: string + * format: json + * description: Información del producto en formato JSON + * example: '{"nombreComun":"Camisa casual","nombreComercial":"Camisa formal","descripcion":"Camisa de algodón","marca":"Brand","modelo":"M-123","tipoProducto":"Ropa","precioPuntos":100,"precioCliente":50.99,"precioVenta":59.99,"costo":30.00,"impuesto":16,"descuento":0,"idProveedor":1,"estado":1,"envio":1}' + * variantes: + * type: string + * format: json + * description: Array de variantes del producto en formato JSON + * example: '[{"identificador":"var1","nombreVariante":"Color","descripcion":"Color de la prenda","opciones":[{"valorOpcion":"Azul","cantidad":10,"descuento":0,"costoAdicional":0,"SKUautomatico":"SKU-AUTO-1","SKUcomercial":"SKU-COM-1"}]}]' + * mapaImagenes: + * type: string + * format: json + * description: Mapa que relaciona imágenes con variantes en formato JSON + * example: '[{"idVariante":"var1"}]' + * imagenProducto: + * type: string + * format: binary + * description: Imagen principal del producto + * imagenesVariante: + * type: array + * items: + * type: string + * format: binary + * description: Imágenes asociadas a cada variante del producto + * responses: + * 200: + * description: Producto creado correctamente + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: Producto creado correctamente + * 400: + * description: Error en los parámetros proporcionados + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: Los parámetros proporcionados no son válidos + * 401: + * description: No autorizado, token inválido o falta de permisos + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: No tiene permisos para realizar esta acción + * 500: + * description: Error interno del servidor + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: Error al crear producto + * error: + * type: string + * example: Error al crear variante + */ + +ruteador.post( + RUTAS.PRODUCTOS.CREAR, + revisarApiKey(), + autorizarToken, + verificarPermisos(PERMISOS.CREAR_PRODUCTO), + controlador.crearProducto +); + +module.exports = ruteador; diff --git a/Productos/Rutas/RutasIndividuales/eliminarProducto.routes.js b/Productos/Rutas/RutasIndividuales/eliminarProducto.routes.js index db33f7b6..0a25d53a 100644 --- a/Productos/Rutas/RutasIndividuales/eliminarProducto.routes.js +++ b/Productos/Rutas/RutasIndividuales/eliminarProducto.routes.js @@ -6,7 +6,7 @@ const revisarApiKey = require('@altertex/util/inter/revisarApiKey'); const autorizarToken = require('@altertex/util/inter/autorizarToken'); const verificarPermisos = require('@altertex/util/inter/verificarPermisos'); -const PERMISOS = require('../../../Utilidades/Constantes/permisos'); +const PERMISOS = require('@altertex/util/const/permisos'); const RUTAS = require('@altertex/util/const/rutas'); /** diff --git a/Productos/Rutas/indexProductos.routes.js b/Productos/Rutas/indexProductos.routes.js index c2aceb14..47a7202e 100644 --- a/Productos/Rutas/indexProductos.routes.js +++ b/Productos/Rutas/indexProductos.routes.js @@ -1,13 +1,16 @@ const express = require('express'); const ruteador = express.Router(); - -const rutaConsultar = require('@altertex/pro/rutasInd/consultarProductos.routes'); +const rutaConsultarLista = require('@altertex/pro/rutasInd/consultarProductos.routes'); const rutaEliminar = require("@altertex/pro/rutasInd/eliminarProducto.routes"); +const rutaCrearProducto = require('@altertex/pro/rutasInd/crearProducto.routes'); const RUTAS = require('@altertex/util/const/rutas'); -// RF[27] Consulta Lista de Productos - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF27] +//RF27 Consulta Lista de Productos - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF27 +ruteador.use(RUTAS.PRODUCTOS.BASE, rutaConsultarLista); +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +ruteador.use(RUTAS.PRODUCTOS.BASE, rutaCrearProducto); // RF[30] Eliminar Producto - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF30] -ruteador.use(RUTAS.PRODUCTOS.BASE, rutaConsultar, rutaEliminar); +ruteador.use(RUTAS.PRODUCTOS.BASE, rutaEliminar); module.exports = ruteador; \ No newline at end of file diff --git a/Proveedores/Controladores/consultarProveedores.controller.js b/Proveedores/Controladores/consultarProveedores.controller.js new file mode 100644 index 00000000..418c9b02 --- /dev/null +++ b/Proveedores/Controladores/consultarProveedores.controller.js @@ -0,0 +1,51 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +const repositorio = require('@altertex/prove/repos/repositorioConsultarProveedores'); +const MENSAJES_PROVEEDORES = require('@altertex/util/const/mensajesProveedores'); + +/** + * Controlador para la consulta de la lista de proveedores de un cliente. + * + * @async + * @function consultarLista + * @param {object} req - Objeto de solicitud de Express. + * @param {object} req.user - Datos del usuario autenticado. + * @param {number} req.user.clienteSeleccionado - ID del cliente seleccionado para la consulta. + * @param {object} res - Objeto de respuesta de Express. + * + * @returns {Response} Respuesta HTTP con estado: + * - 200 si la consulta es exitosa, junto con los datos de los proveedores. + * - 204 si no hay proveedores registrados. + * - 400 si los parámetros proporcionados son inválidos. + * - 500 si ocurre un error en el servidor. + * + * @throws {Error} Si ocurre un error inesperado durante la operación. + */ +exports.consultarLista = async (req, res) => { + const idCliente = parseInt(req.user?.clienteSeleccionado); + + if (!idCliente || isNaN(idCliente)) { + return res.status(MENSAJES_PROVEEDORES.DATOS_PROVEEDOR_INVALIDOS.codigo).json({ + mensaje: MENSAJES_PROVEEDORES.DATOS_PROVEEDOR_INVALIDOS.mensaje, + }); + } + + try { + const resultados = await repositorio.obtenerProveedores(idCliente); + + if (!resultados || resultados.length === 0) { + return res.status(MENSAJES_PROVEEDORES.LISTA_PROVEEDORES_VACIA.codigo).json({ + mensaje: MENSAJES_PROVEEDORES.LISTA_PROVEEDORES_VACIA.mensaje, + }); + } + + return res.status(MENSAJES_PROVEEDORES.CONSULTA_PROVEEDORES_EXITOSA.codigo).json({ + mensaje: MENSAJES_PROVEEDORES.CONSULTA_PROVEEDORES_EXITOSA.mensaje, + listaProveedores: resultados, + }); + } catch (error) { + console.error('Error al consultar proveedores:', error); + return res.status(MENSAJES_PROVEEDORES.ERROR_CONSULTAR_PROVEEDORES.codigo).json({ + mensaje: MENSAJES_PROVEEDORES.ERROR_CONSULTAR_PROVEEDORES.mensaje, + }); + } +}; diff --git a/Proveedores/Controladores/crearProveedor.controller.js b/Proveedores/Controladores/crearProveedor.controller.js new file mode 100644 index 00000000..5bcaac0f --- /dev/null +++ b/Proveedores/Controladores/crearProveedor.controller.js @@ -0,0 +1,50 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +const validarProveedor = require('@altertex/util/vali/validarProveedor'); +const repositorioCrearProveedor = require('@altertex/prove/repos/repositorioCrearProvedor'); +const MENSAJES_PROVEEDORES = require('@altertex/util/const/mensajesProveedores'); + +/** + * Controlador para crear un proveedor de forma independiente. + * + * @async + * @function crearProveedor + * @param {object} req - Objeto de solicitud de Express. + * @param {object} req.body - Cuerpo de la solicitud HTTP. + * @param {object} res - Objeto de respuesta de Express. + * + * @returns {Response} Respuesta HTTP con estado: + * - 200 si la creación es exitosa, junto con el ID del proveedor. + * - 400 si los parámetros proporcionados son inválidos. + * - 500 si ocurre un error en el servidor. + */ +exports.crearProveedor = async (req, res) => { + const proveedor = req.body; + const idCliente = parseInt(req.user.clienteSeleccionado); + + console.log(req.body); + + const errorProveedor = validarProveedor(proveedor); + if (errorProveedor) { + return res.status(MENSAJES_PROVEEDORES.DATOS_PROVEEDOR_INVALIDOS.codigo).json({ + mensaje: MENSAJES_PROVEEDORES.DATOS_PROVEEDOR_INVALIDOS.mensaje, + }); + } + + try { + const idProveedor = await repositorioCrearProveedor.crearProveedor(idCliente, proveedor); + + if (!idProveedor) { + throw new Error('Error al crear proveedor'); + } + + return res.status(MENSAJES_PROVEEDORES.PROVEEDOR_CREADO_EXITOSAMENTE.codigo).json({ + mensaje: MENSAJES_PROVEEDORES.PROVEEDOR_CREADO_EXITOSAMENTE.mensaje, + }); + } catch (error) { + console.error('Error al crear proveedor:', error); + return res.status(MENSAJES_PROVEEDORES.ERROR_CREAR_PROVEEDOR.codigo).json({ + mensaje: MENSAJES_PROVEEDORES.ERROR_CREAR_PROVEEDOR.mensaje, + error: error.message, + }); + } +}; diff --git a/Proveedores/Datos/Repositorios/repositorioConsultarProveedores.js b/Proveedores/Datos/Repositorios/repositorioConsultarProveedores.js new file mode 100644 index 00000000..9f1ea2df --- /dev/null +++ b/Proveedores/Datos/Repositorios/repositorioConsultarProveedores.js @@ -0,0 +1,25 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +const correrQuery = require('@altertex/util/ser/correrQuery'); +const consultas = require('@altertex/util/const/consultasProveedores'); + +/** + * Obtiene la lista de proveedores registrados para un cliente específico. + * + * Esta función ejecuta una consulta SQL para recuperar los proveedores asociados + * al cliente proporcionado. Devuelve un arreglo de proveedores si la operación + * es exitosa, o un arreglo vacío en caso de error. + * + * @param {string|number} clienteSeleccionado - ID o identificador del cliente del cual se quieren obtener los proveedores. + * + * @returns {Promise} Un arreglo de objetos que representan los proveedores, o un arreglo vacío si ocurre un error. + */ +exports.obtenerProveedores = async (clienteSeleccionado) => { + const query = consultas.OBTENER_LISTA; + try { + const resultados = await correrQuery(query, [clienteSeleccionado]); + return resultados; + } catch (error) { + console.error('Error al obtener los proveedores:', error); + return []; + } +}; diff --git a/Proveedores/Datos/Repositorios/repositorioCrearProvedor.js b/Proveedores/Datos/Repositorios/repositorioCrearProvedor.js new file mode 100644 index 00000000..cc674bb4 --- /dev/null +++ b/Proveedores/Datos/Repositorios/repositorioCrearProvedor.js @@ -0,0 +1,46 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +const correrQuery = require('@altertex/util/ser/correrQuery'); +const consultas = require('@altertex/util/const/consultasProveedores'); + +/** + * Crea un nuevo proveedor en la base de datos. + * + * Esta función ejecuta una consulta SQL para insertar los datos de un proveedor + * utilizando los parámetros proporcionados. Devuelve el ID del proveedor recién creado + * en caso de éxito, o un array vacío si ocurre algún error durante la operación. + * + * @param {string|number} clienteSeleccionado - ID o identificador del cliente del cual se quieren obtener los proveedores. + * @param {object} proveedor - Objeto que contiene la información del proveedor. + * @param {string} proveedor.nombre - Nombre del contacto del proveedor. + * @param {string} proveedor.nombreCompania - Nombre de la compañía del proveedor. + * @param {string} proveedor.telefonoContacto - Teléfono de contacto del proveedor. + * @param {string} proveedor.direccion - Dirección del proveedor. + * @param {string} proveedor.codigoPostal - Código postal de la dirección del proveedor. + * @param {string} proveedor.pais - País del proveedor. + * @param {string} proveedor.estado - Estado o provincia del proveedor. + * + * @returns {Promise} El ID del proveedor recién creado en caso de éxito, o un arreglo vacío en caso de error. + */ +exports.crearProveedor = async (clienteSeleccionado, proveedor) => { + const query = consultas.CREAR; + const parametros = [ + clienteSeleccionado, + proveedor.nombre, + proveedor.nombreCompania, + proveedor.telefonoContacto, + proveedor.direccion, + proveedor.codigoPostal, + proveedor.pais, + proveedor.estado, + ]; + + try { + const resultados = await correrQuery(query, parametros); + + const idProveedor = resultados.insertId; + return idProveedor; + } catch (error) { + console.error('Error al crear proveedor:', error); + return []; + } +}; diff --git a/Proveedores/Rutas/RutasIndividuales/consultarProveedores.routes.js b/Proveedores/Rutas/RutasIndividuales/consultarProveedores.routes.js new file mode 100644 index 00000000..81d249d6 --- /dev/null +++ b/Proveedores/Rutas/RutasIndividuales/consultarProveedores.routes.js @@ -0,0 +1,126 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +const express = require('express'); +const ruteador = express.Router(); +const controlador = require('@altertex/prove/ctrl/consultarProveedores.controller'); +const revisarApiKey = require('@altertex/util/inter/revisarApiKey'); +const autorizarToken = require('@altertex/util/inter/autorizarToken'); +const verificarPermisos = require('@altertex/util/inter/verificarPermisos'); + +const PERMISOS = require('@altertex/util/const/permisos'); +const RUTAS = require('@altertex/util/const/rutas'); + +/** + * @swagger + * /api/proveedores/consultar-lista: + * post: + * summary: Consultar lista de proveedores + * description: Obtiene la lista de proveedores asociados al cliente seleccionado + * tags: [Proveedores] + * security: + * - ApiKeyAuth: [] + * - BearerAuth: [] + * requestBody: + * required: false + * content: + * application/json: + * schema: + * type: object + * example: {} + * responses: + * 200: + * description: Consulta exitosa + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: Consulta de proveedores exitosa + * listaProveedores: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * nombre: + * type: string + * example: Juan Pérez + * nombreCompania: + * type: string + * example: Textiles del Norte S.A. + * telefonoContacto: + * type: string + * example: +52 55 1234 5678 + * correoContacto: + * type: string + * example: juan.perez@textilesnorte.com + * direccion: + * type: string + * example: Av. Industrial 123, Col. Centro + * codigoPostal: + * type: string + * example: 12345 + * pais: + * type: string + * example: México + * estado: + * type: integer + * example: 1 + * fechaCreacion: + * type: string + * format: date-time + * example: 2023-10-15T14:30:00Z + * 204: + * description: No hay proveedores registrados + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: No hay proveedores registrados + * 400: + * description: Parámetros inválidos + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: Los datos del proveedor son inválidos + * 401: + * description: No autorizado, token inválido o falta de permisos + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: No tiene permisos para realizar esta acción + * 500: + * description: Error interno del servidor + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: Error al consultar proveedores + */ + +ruteador.post( + RUTAS.PROVEEDORES.CONSULTAR_LISTA, + revisarApiKey(), + autorizarToken, + verificarPermisos(PERMISOS.CONSULTAR_PRODUCTOS), + controlador.consultarLista +); + +module.exports = ruteador; diff --git a/Proveedores/Rutas/RutasIndividuales/crearProveedor.routes.js b/Proveedores/Rutas/RutasIndividuales/crearProveedor.routes.js new file mode 100644 index 00000000..6eb7fad3 --- /dev/null +++ b/Proveedores/Rutas/RutasIndividuales/crearProveedor.routes.js @@ -0,0 +1,125 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +const express = require('express'); +const ruteador = express.Router(); +const controlador = require('@altertex/prove/ctrl/crearProveedor.controller'); +const revisarApiKey = require('@altertex/util/inter/revisarApiKey'); +const autorizarToken = require('@altertex/util/inter/autorizarToken'); +const verificarPermisos = require('@altertex/util/inter/verificarPermisos'); + +const PERMISOS = require('@altertex/util/const/permisos'); +const RUTAS = require('@altertex/util/const/rutas'); + +/** + * @swagger + * /api/proveedores/crear: + * post: + * summary: Crear un nuevo proveedor + * description: Crea un nuevo proveedor asociado al cliente seleccionado + * tags: [Proveedores] + * security: + * - ApiKeyAuth: [] + * - BearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - nombre + * - nombreCompania + * - telefonoContacto + * - correoContacto + * - direccion + * - codigoPostal + * - pais + * - estado + * properties: + * nombre: + * type: string + * description: Nombre del contacto del proveedor + * example: Juan Pérez + * nombreCompania: + * type: string + * description: Nombre de la compañía del proveedor + * example: Textiles del Norte S.A. + * telefonoContacto: + * type: string + * description: Número telefónico de contacto + * example: +52 55 1234 5678 + * correoContacto: + * type: string + * description: Correo electrónico de contacto + * example: juan.perez@textilesnorte.com + * direccion: + * type: string + * description: Dirección física del proveedor + * example: Av. Industrial 123, Col. Centro + * codigoPostal: + * type: string + * description: Código postal de la dirección + * example: 12345 + * pais: + * type: string + * description: País donde se encuentra el proveedor + * example: México + * estado: + * type: number + * description: Estado del proveedor (1 activo, 0 inactivo) + * enum: [0, 1] + * example: 1 + * responses: + * 200: + * description: Proveedor creado exitosamente + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: Proveedor creado exitosamente + * 400: + * description: Datos del proveedor inválidos + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: Los datos del proveedor son inválidos + * 401: + * description: No autorizado, token inválido o falta de permisos + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: No tiene permisos para realizar esta acción + * 500: + * description: Error interno del servidor + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: Error al crear proveedor + * error: + * type: string + * example: Error al crear proveedor + */ + +ruteador.post( + RUTAS.PROVEEDORES.CREAR, + revisarApiKey(), + autorizarToken, + verificarPermisos(PERMISOS.CREAR_PRODUCTO), + controlador.crearProveedor +); + +module.exports = ruteador; diff --git a/Proveedores/Rutas/indexProveedores.routes.js b/Proveedores/Rutas/indexProveedores.routes.js new file mode 100644 index 00000000..f464092b --- /dev/null +++ b/Proveedores/Rutas/indexProveedores.routes.js @@ -0,0 +1,13 @@ +const express = require('express'); +const ruteador = express.Router(); +const rutaConsultarLista = require('@altertex/prove/rutasInd/consultarProveedores.routes'); +const rutaCrearProveedor = require('@altertex/prove/rutasInd/crearProveedor.routes'); + +const RUTAS = require('@altertex/util/const/rutas'); + +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +ruteador.use(RUTAS.PROVEEDORES.BASE, rutaConsultarLista); +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +ruteador.use(RUTAS.PROVEEDORES.BASE, rutaCrearProveedor); + +module.exports = ruteador; diff --git a/Roles/Datos/Repositorios/repositorioCrearRol.js b/Roles/Datos/Repositorios/repositorioCrearRol.js index 7f4242fe..5cf61ff2 100644 --- a/Roles/Datos/Repositorios/repositorioCrearRol.js +++ b/Roles/Datos/Repositorios/repositorioCrearRol.js @@ -1,5 +1,5 @@ -const db = require("@altertex/util/bd/db"); -const QUERY = require("@altertex/util/const/consultasRoles"); +const db = require('@altertex/util/bd/db'); +const QUERY = require('@altertex/util/const/consultasRoles'); /** * Verifica si un rol con el nombre especificado ya existe en la base de datos. @@ -68,6 +68,6 @@ exports.asociarPermisosARol = async (idRol, permisos) => { await conexion.commit(); } catch { await conexion.rollback(); - throw new Error("Error asociando permisos al rol"); + throw new Error('Error asociando permisos al rol'); } }; diff --git a/Utilidades/Constantes/consultasImagenes.js b/Utilidades/Constantes/consultasImagenes.js new file mode 100644 index 00000000..0c559a6f --- /dev/null +++ b/Utilidades/Constantes/consultasImagenes.js @@ -0,0 +1,6 @@ +module.exports = { + CREAR: ` + INSERT INTO imagen(urlImagen, tipoImagen, textoAlternativo) + VALUES (?, ?, ?); + `, +}; diff --git a/Utilidades/Constantes/consultasOpciones.js b/Utilidades/Constantes/consultasOpciones.js new file mode 100644 index 00000000..825ebfe9 --- /dev/null +++ b/Utilidades/Constantes/consultasOpciones.js @@ -0,0 +1,6 @@ +module.exports = { + CREAR: ` + INSERT INTO opcion (idVariante, cantidad, valorOpcion, SKUautomatico, SKUcomercial, costoAdicional, descuento, estado) + VALUES (?, ?, ?, ?, ?, ?, ?, ?); + `, +}; diff --git a/Utilidades/Constantes/consultasProductos.js b/Utilidades/Constantes/consultasProductos.js index 100d3680..81b5df4c 100644 --- a/Utilidades/Constantes/consultasProductos.js +++ b/Utilidades/Constantes/consultasProductos.js @@ -7,7 +7,23 @@ module.exports = { WHERE i.tipoImagen = "Imagen Producto" AND p.idCliente = ?; `, + CREAR: ` + INSERT INTO producto ( + idCliente, idProveedor, nombreComun, nombreComercial, descripcion, + marca, modelo, tipoProducto, precioPuntos, precioCliente, + precioVenta, costo, impuesto, descuento, estado, envio + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + `, + CREAR_IMAGEN_PRODUCTO: ` + INSERT INTO imagen_producto (idImagen, idProducto) + VALUES (?, ?); + `, + CREAR_DATOS_ENVIO: ` + INSERT INTO datos_envio (idProducto, peso, longitud, ancho, altura, volumen, tipoPaquete) + VALUES (?, ?, ?, ?, ?, ?,?); + `, ELIMINAR_PRODUCTOS: "DELETE FROM producto WHERE idProducto IN (?)", -}; \ No newline at end of file +}; diff --git a/Utilidades/Constantes/consultasProveedores.js b/Utilidades/Constantes/consultasProveedores.js new file mode 100644 index 00000000..73fad2c5 --- /dev/null +++ b/Utilidades/Constantes/consultasProveedores.js @@ -0,0 +1,10 @@ +module.exports = { + OBTENER_LISTA: ` + SELECT * FROM proveedor + WHERE idCliente = ?; + `, + CREAR: ` + INSERT INTO proveedor (idCliente, nombre, nombreCompania, telefonoContacto, direccion, codigoPostal, pais, estado) + VALUES (?, ?, ?, ?, ?, ?, ?, ?); + `, +}; diff --git a/Utilidades/Constantes/consultasUsuarios.js b/Utilidades/Constantes/consultasUsuarios.js index 7c3b903e..f354157a 100644 --- a/Utilidades/Constantes/consultasUsuarios.js +++ b/Utilidades/Constantes/consultasUsuarios.js @@ -80,7 +80,13 @@ module.exports = { LEFT JOIN usuario_rol ur ON u.idUsuario = ur.idUsuario LEFT JOIN rol r ON ur.idRol = r.idRol LEFT JOIN usuario_cliente uc ON u.idUsuario = uc.idUsuario - LEFT JOIN cliente c ON uc.idCliente = c.idCliente; + LEFT JOIN cliente c ON uc.idCliente = c.idCliente + WHERE u.idUsuario NOT IN ( + SELECT ur2.idUsuario + FROM usuario_rol ur2 + WHERE ur2.idRol = 3 + ); + `, ELIMINAR_USUARIOS: ` diff --git a/Utilidades/Constantes/consultasVariantes.js b/Utilidades/Constantes/consultasVariantes.js new file mode 100644 index 00000000..b9ebc210 --- /dev/null +++ b/Utilidades/Constantes/consultasVariantes.js @@ -0,0 +1,10 @@ +module.exports = { + CREAR: ` + INSERT INTO variante (idProducto, nombreVariante, descripcion) + VALUES (?, ?, ?); + `, + CREAR_IMAGEN_VARIANTE: ` + INSERT INTO imagen_variante (idImagen, idVariante) + VALUES (?, ?); + `, +}; diff --git a/Utilidades/Constantes/mensajesProductos.js b/Utilidades/Constantes/mensajesProductos.js index 01b9a9b2..8f357e88 100644 --- a/Utilidades/Constantes/mensajesProductos.js +++ b/Utilidades/Constantes/mensajesProductos.js @@ -2,25 +2,62 @@ module.exports = { // 200 - OK CONSULTA_EXITOSA: { codigo: 200, - mensaje: "Lista de productos obtenida exitosamente.", + mensaje: 'Lista de productos obtenida exitosamente.', + }, + PRODUCTO_CREADO_EXITOSAMENTE: { + codigo: 200, + mensaje: 'Producto creado correctamente.', }, // 204 - No Content SIN_RESULTADOS: { - codigo: 200, - mensaje: "No se encontraron productos registrados para el cliente.", + codigo: 204, + mensaje: 'No se encontraron productos registrados para el cliente.', + }, + + // 400 - Bad Request + PARAMETROS_INVALIDOS: { + codigo: 400, + mensaje: 'Los parámetros proporcionados no son válidos o están incompletos.', + }, + LIMITE_OFFSET_INVALIDOS: { + codigo: 400, + mensaje: 'Los valores de límite u offset deben ser números positivos.', }, // 403 - Forbidden PERMISO_DENEGADO: { codigo: 403, - mensaje: "No tiene permiso para consultar productos de este cliente.", + mensaje: 'No tiene permiso para consultar productos de este cliente.', }, // 500 - Internal Server Error ERROR_CONSULTAR_PRODUCTOS: { codigo: 500, - mensaje: "Ocurrió un error al obtener la lista de productos.", + mensaje: 'Ocurrió un error al obtener la lista de productos.', + }, + ERROR_CREAR_PRODUCTO: { + codigo: 500, + mensaje: 'Ocurrió un error al crear el producto. Por favor, intente nuevamente más tarde.', + }, + ERROR_ENVIAR_IMAGENES_S3: { + codigo: 500, + mensaje: 'Ocurrió un error al subir las imágenes al servidor. Intente nuevamente.', + }, + ERROR_CREAR_VARIANTE: { + codigo: 500, + mensaje: + 'Ocurrió un error al crear una variante del producto. Verifique los datos de variantes.', + }, + ERROR_CREAR_OPCION: { + codigo: 500, + mensaje: 'Ocurrió un error al crear una opción para la variante. Revise las opciones enviadas.', + }, + + ERROR_CREAR_IMAGEN_VARIANTE: { + codigo: 500, + mensaje: + 'Ocurrió un error al asociar la imagen con la variante. Verifique los datos de las imágenes de variantes.', }, // 200 - OK diff --git a/Utilidades/Constantes/mensajesProveedores.js b/Utilidades/Constantes/mensajesProveedores.js new file mode 100644 index 00000000..20d8f3fa --- /dev/null +++ b/Utilidades/Constantes/mensajesProveedores.js @@ -0,0 +1,53 @@ +module.exports = { + // 200 - OK + PROVEEDOR_CREADO_EXITOSAMENTE: { + codigo: 200, + mensaje: 'El proveedor fue creado exitosamente.', + }, + CONSULTA_PROVEEDORES_EXITOSA: { + codigo: 200, + mensaje: 'Lista de proveedores obtenida exitosamente.', + }, + PROVEEDOR_OBTENIDO_EXITOSAMENTE: { + codigo: 200, + mensaje: 'Información del proveedor obtenida exitosamente.', + }, + + // 204 - No Content + LISTA_PROVEEDORES_VACIA: { + codigo: 204, + mensaje: 'No hay proveedores registrados actualmente.', + }, + + // 400 - Bad Request + DATOS_PROVEEDOR_INVALIDOS: { + codigo: 400, + mensaje: 'Los datos del proveedor proporcionados son inválidos o están incompletos.', + }, + + // 403 - Forbidden + ACCESO_NO_AUTORIZADO_PROVEEDORES: { + codigo: 403, + mensaje: 'No tiene permiso para consultar o modificar proveedores.', + }, + + // 404 - Not Found + PROVEEDOR_NO_ENCONTRADO: { + codigo: 404, + mensaje: 'No se encontró un proveedor con el ID proporcionado.', + }, + + // 500 - Internal Server Error + ERROR_CREAR_PROVEEDOR: { + codigo: 500, + mensaje: 'Ocurrió un error al intentar crear el proveedor.', + }, + ERROR_CONSULTAR_PROVEEDORES: { + codigo: 500, + mensaje: 'Ocurrió un error al obtener la lista de proveedores.', + }, + ERROR_CONSULTAR_PROVEEDOR: { + codigo: 500, + mensaje: 'Ocurrió un error al obtener la información del proveedor.', + }, +}; diff --git a/Utilidades/Constantes/rutas.js b/Utilidades/Constantes/rutas.js index 8e24d1de..61cca03a 100644 --- a/Utilidades/Constantes/rutas.js +++ b/Utilidades/Constantes/rutas.js @@ -36,11 +36,19 @@ module.exports = { PRODUCTOS: { BASE: '/productos', CONSULTAR_LISTA: '/consultar-lista', + CREAR: '/crear', ELIMINAR_PRODUCTO: '/eliminar', }, + PROVEEDORES: { + BASE: '/proveedores', + CONSULTAR_LISTA: '/consultar-lista', + CREAR: '/crear', + }, SETS_PRODUCTOS: { BASE: '/sets-productos', CONSULTAR_LISTA: '/consultar-lista', + CREAR: '/crear', + SUBIR_IMAGEN: '/subir-imagen', ELIMINAR_SET_PRODUCTOS: '/eliminar', }, CLIENTES: { diff --git a/Utilidades/Intermediarios/Validaciones/validarOpciones.js b/Utilidades/Intermediarios/Validaciones/validarOpciones.js new file mode 100644 index 00000000..8dc60a95 --- /dev/null +++ b/Utilidades/Intermediarios/Validaciones/validarOpciones.js @@ -0,0 +1,86 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +/** + * Valida un conjunto de opciones para un producto. + * + * Esta función valida cada opción dentro del arreglo de opciones pasado como argumento. + * Se asegura de que los valores de las propiedades cumplan con los tipos y rangos especificados, + * y retorna un objeto de error si algún valor es inválido. + * + * @param {Array} opciones - Un arreglo de objetos que representan las opciones de un producto. + * @param {number} opciones[].cantidad - La cantidad de la opción, que debe ser un número entero positivo o cero. + * @param {string} opciones[].valorOpcion - El valor de la opción, que debe ser una cadena de texto de máximo 100 caracteres. + * @param {string} [opciones[].SKUautomatico] - El SKU automático de la opción, que debe ser una cadena de texto de máximo 50 caracteres, si se proporciona. + * @param {string} [opciones[].SKUcomercial] - El SKU comercial de la opción, que debe ser una cadena de texto de máximo 50 caracteres, si se proporciona. + * @param {number} opciones[].costoAdicional - El costo adicional de la opción, que debe ser un número positivo o cero. + * @param {number} opciones[].descuento - El descuento de la opción, que debe ser un número entre 0 y 100. + * @param {number} opciones[].estado - El estado de la opción, que debe ser 1 (activo) o 0 (inactivo). + * + * @returns {object|null} Un objeto con una propiedad `error` si hay un error de validación, o `null` si todas las opciones son válidas. + * @example + * const opciones = [ + * { + * cantidad: 5, + * valorOpcion: 'Tamaño M', + * SKUautomatico: 'SKU123', + * SKUcomercial: 'S123', + * costoAdicional: 15.50, + * descuento: 10, + * estado: 1 + * } + * ]; + * + * const resultado = validarOpciones(opciones); + * console.log(resultado); // null si todo está bien, o un objeto de error si algo es inválido + */ +module.exports = (opciones) => { + for (const opcion of opciones) { + // prettier-ignore + if ( + typeof opcion.cantidad !== 'number' + || opcion.cantidad < 0 + || !Number.isInteger(opcion.cantidad) + ) { + return { error: 'cantidad de la opción debe ser un número entero positivo o cero.' }; + } + + // prettier-ignore + if ( + !opcion.valorOpcion + || typeof opcion.valorOpcion !== 'string' + || opcion.valorOpcion.length > 100 + ) { + return { + error: 'valorOpcion es requerido y debe ser una cadena de texto de máximo 100 caracteres.', + }; + } + + // prettier-ignore + if ( + opcion.SKUautomatico + && (typeof opcion.SKUautomatico !== 'string' || opcion.SKUautomatico.length > 50) + ) { + return { error: 'SKUautomatico debe ser una cadena de texto de máximo 50 caracteres.' }; + } + // prettier-ignore + if ( + opcion.SKUcomercial + && (typeof opcion.SKUcomercial !== 'string' || opcion.SKUcomercial.length > 50) + ) { + return { error: 'SKUcomercial debe ser una cadena de texto de máximo 50 caracteres.' }; + } + + if (typeof opcion.costoAdicional !== 'number' || opcion.costoAdicional < 0) { + return { error: 'costoAdicional debe ser un número positivo o cero.' }; + } + + if (typeof opcion.descuento !== 'number' || opcion.descuento < 0 || opcion.descuento > 100) { + return { error: 'descuento debe ser un número entre 0 y 100.' }; + } + + if (typeof opcion.estado !== 'number' || (opcion.estado !== 0 && opcion.estado !== 1)) { + return { error: 'estado debe ser 1 (activo) o 0 (inactivo).' }; + } + } + + return null; +}; diff --git a/Utilidades/Intermediarios/Validaciones/validarProducto.js b/Utilidades/Intermediarios/Validaciones/validarProducto.js new file mode 100644 index 00000000..5738d072 --- /dev/null +++ b/Utilidades/Intermediarios/Validaciones/validarProducto.js @@ -0,0 +1,138 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +/** + * Valida los campos de un producto. + * + * Esta función valida los diferentes campos de un objeto `producto` asegurándose de que cumplan con los tipos y restricciones especificados. Si algún campo no cumple con las condiciones, se devuelve un objeto de error con un mensaje específico. Si todos los campos son válidos, se retorna `null`. + * + * @param {object} producto - El objeto que representa un producto a validar. + * @param {number|null} producto.idProveedor - El ID del proveedor, debe ser un número entero positivo o `null`. + * @param {string} producto.nombreComun - El nombre común del producto, debe ser una cadena de texto de máximo 100 caracteres. + * @param {string|null} producto.nombreComercial - El nombre comercial del producto, debe ser una cadena de texto de máximo 150 caracteres o `null`. + * @param {string|null} producto.descripcion - Descripción del producto, debe ser una cadena de texto de máximo 1000 caracteres o `null`. + * @param {string|null} producto.marca - La marca del producto, debe ser una cadena de texto de máximo 100 caracteres o `null`. + * @param {string|null} producto.modelo - El modelo del producto, debe ser una cadena de texto de máximo 100 caracteres o `null`. + * @param {string|null} producto.tipoProducto - El tipo de producto, debe ser una cadena de texto de máximo 50 caracteres o `null`. + * @param {number|null} producto.precioPuntos - El precio en puntos, debe ser un número entero positivo o `null`. + * @param {number|null} producto.precioCliente - El precio para el cliente, debe ser un número mayor o igual a cero o `null`. + * @param {number|null} producto.precioVenta - El precio de venta, debe ser un número mayor o igual a cero o `null`. + * @param {number|null} producto.costo - El costo del producto, debe ser un número mayor o igual a cero o `null`. + * @param {number|null} producto.impuesto - El impuesto del producto, debe ser un número mayor o igual a cero o `null`. + * @param {number|null} producto.descuento - El descuento del producto, debe ser un número mayor o igual a cero o `null`. + * @param {number} producto.estado - El estado del producto, debe ser 0 (inactivo) o 1 (activo). + * @param {number} producto.envio - Indica si el envío está disponible, debe ser 0 (no disponible) o 1 (disponible). + * + * @returns {object|null} Un objeto con una propiedad `error` si hay un error de validación, o `null` si todos los campos son válidos. + * @example + * const producto = { + * idProveedor: 1, + * nombreComun: 'Producto X', + * nombreComercial: 'Producto X Comercial', + * descripcion: 'Descripción del producto', + * marca: 'Marca X', + * modelo: 'Modelo 123', + * tipoProducto: 'Tipo A', + * precioPuntos: 100, + * precioCliente: 200.50, + * precioVenta: 250, + * costo: 150, + * impuesto: 25, + * descuento: 10, + * estado: 1, + * envio: 1, + * }; + * + * const resultado = validarProducto(producto); + * console.log(resultado); // null si todo está bien, o un objeto de error si algo es inválido + */ +// prettier-ignore +module.exports = (producto) => { + if ( + producto.idProveedor !== null + && (typeof producto.idProveedor !== 'number' + || producto.idProveedor <= 0 + || !Number.isInteger(producto.idProveedor)) + ) { + return { error: 'idProveedor debe ser un número entero positivo o NULL.' }; + } + + if ( + !producto.nombreComun + || typeof producto.nombreComun !== 'string' + || producto.nombreComun.length > 100 + ) { + return { + error: 'nombreComun es requerido, debe ser una cadena de texto y no exceder 100 caracteres.', + }; + } + + if ( + producto.nombreComercial !== null + && (typeof producto.nombreComercial !== 'string' || producto.nombreComercial.length > 150) + ) { + return { + error: 'nombreComercial debe ser una cadena de texto o NULL y no exceder 150 caracteres.', + }; + } + + if ( + producto.descripcion !== null + && (typeof producto.descripcion !== 'string' || producto.descripcion.length > 1000) + ) { + return { + error: 'descripcion debe ser una cadena de texto o NULL y no exceder 1000 caracteres.', + }; + } + + if ( + producto.marca !== null + && (typeof producto.marca !== 'string' || producto.marca.length > 100) + ) { + return { error: 'marca debe ser una cadena de texto o NULL y no exceder 100 caracteres.' }; + } + + if ( + producto.modelo !== null + && (typeof producto.modelo !== 'string' || producto.modelo.length > 100) + ) { + return { error: 'modelo debe ser una cadena de texto o NULL y no exceder 100 caracteres.' }; + } + + if ( + producto.tipoProducto !== null + && (typeof producto.tipoProducto !== 'string' || producto.tipoProducto.length > 50) + ) { + return { + error: 'tipoProducto debe ser una cadena de texto o NULL y no exceder 50 caracteres.', + }; + } + + const camposNumericos = [ + 'precioPuntos', + 'precioCliente', + 'precioVenta', + 'costo', + 'impuesto', + 'descuento', + ]; + + for (const campo of camposNumericos) { + if ( + producto[campo] !== null + && (typeof producto[campo] !== 'number' + || (campo === 'precioPuntos' && !Number.isInteger(producto[campo])) + || (campo !== 'precioPuntos' && producto[campo] < 0)) + ) { + return { error: `${campo} debe ser un número válido y mayor o igual a cero.` }; + } + } + + if (producto.estado !== undefined && producto.estado !== 0 && producto.estado !== 1) { + return { error: 'estado debe ser 0 (inactivo) o 1 (activo).' }; + } + + if (producto.envio !== undefined && producto.envio !== 0 && producto.envio !== 1) { + return { error: 'envio debe ser 0 (no disponible) o 1 (disponible).' }; + } + + return null; +}; diff --git a/Utilidades/Intermediarios/Validaciones/validarProveedor.js b/Utilidades/Intermediarios/Validaciones/validarProveedor.js new file mode 100644 index 00000000..2618b3dc --- /dev/null +++ b/Utilidades/Intermediarios/Validaciones/validarProveedor.js @@ -0,0 +1,84 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +/** + * Valida los campos de un proveedor. + * + * Esta función verifica que los campos del objeto `proveedor` cumplan con los requisitos de tipo, longitud y formato. + * Si algún campo no es válido, devuelve un objeto con un mensaje de error específico. + * Si todos los campos son válidos, retorna `null`. + * + * @param {object} proveedor - Objeto que representa al proveedor a validar. + * @param {string} proveedor.nombre - Nombre del proveedor (obligatorio, máximo 100 caracteres). + * @param {string} [proveedor.nombreCompania] - Nombre de la compañía (opcional, máximo 150 caracteres). + * @param {string} [proveedor.telefonoContacto] - Teléfono de contacto (opcional, máximo 20 caracteres). + * @param {string} [proveedor.correoContacto] - Correo electrónico de contacto (opcional, válido y máximo 100 caracteres). + * @param {string} [proveedor.direccion] - Dirección del proveedor (opcional, máximo 200 caracteres). + * @param {string} [proveedor.codigoPostal] - Código postal (opcional, máximo 20 caracteres). + * @param {string} [proveedor.pais] - País del proveedor (opcional, máximo 50 caracteres). + * @param {number} proveedor.estado - Estado del proveedor: 1 (activo) o 0 (inactivo). + * + * @returns {{ error: string } | null} Retorna un objeto con la propiedad `error` si hay un error de validación, o `null` si todo es válido. + */ +module.exports = (proveedor) => { + /** + * Verifica si un texto es válido (tipo string, no vacío, dentro del límite de caracteres). + * + * @param {string} valor - Texto a validar. + * @param {number} max - Longitud máxima permitida. + * @returns {boolean} `true` si es válido, `false` en caso contrario. + */ + // prettier-ignore + const esTextoValido = (valor, max) => typeof valor === 'string' && valor.trim().length > 0 && valor.trim().length <= max; + + if (!esTextoValido(proveedor.nombre, 100)) { + return { + error: 'nombre es obligatorio y debe ser una cadena de texto de máximo 100 caracteres.', + }; + } + + if (proveedor.nombreCompania != null && !esTextoValido(proveedor.nombreCompania, 150)) { + return { + error: 'nombreCompania debe ser una cadena de texto no vacía de máximo 150 caracteres.', + }; + } + + if (proveedor.telefonoContacto != null && !esTextoValido(proveedor.telefonoContacto, 20)) { + return { + error: 'telefonoContacto debe ser una cadena de texto no vacía de máximo 20 caracteres.', + }; + } + + // prettier-ignore + if (proveedor.correoContacto != null) { + const correo = proveedor.correoContacto.trim(); + const regexCorreo = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if ( + typeof correo !== 'string' + || correo.length === 0 + || correo.length > 100 + || !regexCorreo.test(correo) + ) { + return { + error: + 'correoContacto debe ser un correo electrónico válido y con un máximo de 100 caracteres.', + }; + } + } + + if (proveedor.direccion != null && !esTextoValido(proveedor.direccion, 200)) { + return { error: 'direccion debe ser una cadena de texto no vacía de máximo 200 caracteres.' }; + } + + if (proveedor.codigoPostal != null && !esTextoValido(proveedor.codigoPostal, 20)) { + return { error: 'codigoPostal debe ser una cadena de texto no vacía de máximo 20 caracteres.' }; + } + + if (proveedor.pais != null && !esTextoValido(proveedor.pais, 50)) { + return { error: 'pais debe ser una cadena de texto no vacía de máximo 50 caracteres.' }; + } + + if (typeof proveedor.estado !== 'number' || (proveedor.estado !== 1 && proveedor.estado !== 0)) { + return { error: 'estado debe ser 1 (activo) o 0 (inactivo).' }; + } + + return null; +}; diff --git a/Utilidades/Intermediarios/Validaciones/validarVariante.js b/Utilidades/Intermediarios/Validaciones/validarVariante.js new file mode 100644 index 00000000..b8646350 --- /dev/null +++ b/Utilidades/Intermediarios/Validaciones/validarVariante.js @@ -0,0 +1,44 @@ +//RF26 Crea Producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26 +/** + * Valida los campos de una variante. + * + * Esta función valida los diferentes campos de un objeto `variante` asegurándose de que cumplan con los tipos y restricciones especificadas. Si algún campo no cumple con las condiciones, se devuelve un objeto de error con un mensaje específico. Si todos los campos son válidos, se retorna `null`. + * + * @param {object} variante - El objeto que representa una variante a validar. + * @param {string} variante.nombreVariante - El nombre de la variante, debe ser una cadena de texto de máximo 100 caracteres. + * @param {string|null} variante.descripcion - La descripción de la variante, debe ser una cadena de texto de máximo 1000 caracteres o `null`. + * + * @returns {object|null} Un objeto con una propiedad `error` si hay un error de validación, o `null` si todos los campos son válidos. + * @example + * const variante = { + * nombreVariante: 'Tamaño L', + * descripcion: 'Variante para talla grande', + * }; + * + * const resultado = validarVariante(variante); + * console.log(resultado); // null si todo está bien, o un objeto de error si algo es inválido + */ +// prettier-ignore +module.exports = (variante) => { + if ( + !variante.nombreVariante + || typeof variante.nombreVariante !== 'string' + || variante.nombreVariante.length > 100 + ) { + return { + error: + 'nombreVariante es requerido, debe ser una cadena de texto y no exceder 100 caracteres.', + }; + } + + if ( + variante.descripcion !== null + && (typeof variante.descripcion !== 'string' || variante.descripcion.length > 1000) + ) { + return { + error: 'descripcion debe ser una cadena de texto o NULL y no exceder 1000 caracteres.', + }; + } + + return null; +}; diff --git a/Utilidades/Servicios/enviarS3.js b/Utilidades/Servicios/enviarS3.js index e7cdc54b..3cf09461 100644 --- a/Utilidades/Servicios/enviarS3.js +++ b/Utilidades/Servicios/enviarS3.js @@ -1,7 +1,11 @@ const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); const s3 = new S3Client({ - region: 'us-east-1', + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + }, }); /** diff --git a/app.js b/app.js index cc9419ea..20abb017 100644 --- a/app.js +++ b/app.js @@ -17,6 +17,7 @@ const rutasAutenticacion = require('@altertex/aut/rutas/indexAutenticacion.route const rutasUsuarios = require('@altertex/usu/rutas/indexUsuarios.routes'); const rutasCategorias = require('@altertex/cat/rutas/indexCategorias.routes'); const rutasProductos = require('@altertex/pro/rutas/indexProductos.routes'); +const rutasProveedores = require('@altertex/prove/rutas/indexProveedores.routes'); const rutasSetsProductos = require('@altertex/setspro/rutas/indexSetsProductos.routes'); const rutasEmpleados = require('@altertex/emp/rutas/indexEmpleados.routes'); const rutasClientes = require('@altertex/cli/rutas/indexClientes.routes'); @@ -45,6 +46,7 @@ cronCuotas.start(); app.use(RUTAS.API, rutasAutenticacion); app.use(RUTAS.API, rutasUsuarios); app.use(RUTAS.API, rutasProductos); +app.use(RUTAS.API, rutasProveedores); app.use(RUTAS.API, rutasSetsProductos); app.use(RUTAS.API, rutasEmpleados); app.use(RUTAS.API, rutasClientes); diff --git a/jsconfig.json b/jsconfig.json index ee8f430a..9e8820a1 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -32,6 +32,7 @@ "@altertex/util/bd/*": ["Utilidades/BaseDeDatos/*"], "@altertex/util/const/*": ["Utilidades/Constantes/*"], "@altertex/util/inter/*": ["Utilidades/Intermediarios/*"], + "@altertex/util/vali/*": ["Utilidades/Intermediarios/Validaciones/*"], "@altertex/util/ser/*": ["Utilidades/Servicios/*"], "@altertex/rol/*": ["Roles/*"], "@altertex/rol/ctrl/*": ["Roles/Controladores/*"], @@ -45,6 +46,12 @@ "@altertex/pro/repos/*": ["Productos/Datos/Repositorios/*"], "@altertex/pro/rutas/*": ["Productos/Rutas/*"], "@altertex/pro/rutasInd/*": ["Productos/Rutas/RutasIndividuales/*"], + "@altertex/prove/*": ["Proveedores/*"], + "@altertex/prove/ctrl/*": ["Proveedores/Controladores/*"], + "@altertex/prove/datos/*": ["Proveedores/Datos/*"], + "@altertex/prove/repos/*": ["Proveedores/Datos/Repositorios/*"], + "@altertex/prove/rutas/*": ["Proveedores/Rutas/*"], + "@altertex/prove/rutasInd/*": ["Proveedores/Rutas/RutasIndividuales/*"], "@altertex/cuota/*": ["Cuotas/*"], "@altertex/cuota/ctrl/*": ["Cuotas/Controladores/*"], "@altertex/cuota/datos/*": ["Cuotas/Datos/*"], diff --git a/package.json b/package.json index c01a7cdc..9eabb5a3 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "@altertex/util/bd": "Utilidades/BaseDeDatos/", "@altertex/util/const": "Utilidades/Constantes/", "@altertex/util/inter": "Utilidades/Intermediarios/", + "@altertex/util/vali": "Utilidades/Intermediarios/Validaciones", "@altertex/rol": "Roles", "@altertex/rol/ctrl": "Roles/Controladores", "@altertex/rol/datos": "Roles/Datos", @@ -98,6 +99,12 @@ "@altertex/pro/repos": "Productos/Datos/Repositorios", "@altertex/pro/rutas": "Productos/Rutas/", "@altertex/pro/rutasInd": "Productos/Rutas/RutasIndividuales", + "@altertex/prove": "Proveedores", + "@altertex/prove/ctrl": "Proveedores/Controladores/", + "@altertex/prove/datos": "Proveedores/Datos/", + "@altertex/prove/repos": "Proveedores/Datos/Repositorios", + "@altertex/prove/rutas": "Proveedores/Rutas/", + "@altertex/prove/rutasInd": "Proveedores/Rutas/RutasIndividuales", "@altertex/cat": "Categorias/", "@altertex/cat/ctrl": "Categorias/Controladores/", "@altertex/cat/datos": "Categorias/Datos/", From 6159f23adad31bc97b721b5f7727cba6d9859ddb Mon Sep 17 00:00:00 2001 From: max Date: Fri, 23 May 2025 16:27:48 -0600 Subject: [PATCH 02/17] cambio clienteRedis.js --- Configuracion/clienteRedis.js | 2 +- package-lock.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Configuracion/clienteRedis.js b/Configuracion/clienteRedis.js index 5f5f3a76..f6ec29e3 100644 --- a/Configuracion/clienteRedis.js +++ b/Configuracion/clienteRedis.js @@ -7,4 +7,4 @@ const redis = createClient({ redis.on('error', (err) => console.error('Error Redis', err)); redis.connect(); -module.exports = redis; \ No newline at end of file +module.exports = redis; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0a1357e6..c9877cab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10422,6 +10422,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/redis/-/redis-5.1.0.tgz", "integrity": "sha512-5G5k9sYo5H5L0kd7UETiJZFTkIClH31fSmaEk2eU8E7mrmF0J1t6RqmFXOCJmSRlNd3QyvDUK/AWL9psbKaN0Q==", + "license": "MIT", "dependencies": { "@redis/bloom": "5.1.0", "@redis/client": "5.1.0", From 2d724968ef77e29b9a286ff284670e9d37af2b46 Mon Sep 17 00:00:00 2001 From: max Date: Thu, 29 May 2025 16:20:37 -0600 Subject: [PATCH 03/17] feat(usuarios): implement update user functionality with controller, repository, and routes --- .../actualizarUsuario.controller.js | 49 +++++++++++++++++++ .../repositorioActualizarUsuario.js | 40 +++++++++++++++ .../actualizarUsuario.routes.js | 0 Usuarios/Rutas/indexUsuarios.routes.js | 1 + Utilidades/Constantes/consultasUsuarios.js | 10 +++- Utilidades/Constantes/mensajesUsuarios.js | 8 +++ 6 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 Usuarios/Controladores/actualizarUsuario.controller.js create mode 100644 Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js create mode 100644 Usuarios/Rutas/RutasIndividuales/actualizarUsuario.routes.js diff --git a/Usuarios/Controladores/actualizarUsuario.controller.js b/Usuarios/Controladores/actualizarUsuario.controller.js new file mode 100644 index 00000000..8569655c --- /dev/null +++ b/Usuarios/Controladores/actualizarUsuario.controller.js @@ -0,0 +1,49 @@ +const MENSAJES = require('@altertex/util/const/mensajesUsuarios'); +const repositorio = require('@altertex/emp/repos/repositorioActualizarUsuario'); +//RF[4] Actualizar Usuario - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF4] +/** + * Controlador para actualizar la información de un usuario. + * + * Este endpoint recibe un objeto con los cambios que se aplicarán + * sobre un usuario y usa su repositorio para hacer el cambio en la + * base de datos. + * + * @function actualizarUsuario + * @async + * @param {Express.Request} req - Objeto de solicitud HTTP de Express. + * @param {object} req.body - Cuerpo de la solicitud. + * @param {object|Array} req.body.cambios - Información del usuario a actualizar. + * @param {Express.Response} res - Objecto de respuesta HTTP de Express. + * @returns {Promise} Retorna una respuesta JSON indicando éxito o un error. + */ +exports.actualizarUsuario = async (req, res) => { + let datos; + + // Si no hay cambios + if (req.body.id || req.body.idUsuario) { + datos = [req.body]; + } else if (req.body.cambios) { + // Si la información viene en el formato esperado (hay cambios) + datos = Array.isArray(req.body.cambios) ? req.body.cambios : [req.body.cambios]; + } else { + return res + .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) + .json({ mensaje: MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje }); + } +}; + +if (!datos || datos.length === 0) { + return res + .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) + .json({ mensaje: MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje }); +} +try { + await repositorio.actualizarUsuario(datos); + return res + .status(MENSAJES.USUARIO_ACTUALIZADO.codigo) + .json({ mensaje: MENSAJES.USUARIO_ACTUALIZADO.mensaje, datos }); +} catch { + return res + .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) + .json({ mensaje: MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje }); +} diff --git a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js new file mode 100644 index 00000000..c4854ef8 --- /dev/null +++ b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js @@ -0,0 +1,40 @@ +const correrQuery = require('@altertex/util/ser/correrQuery'); +const MENSAJES = require('@altertex/util/const/mensajesUsuarios'); +const CONSULTAS_USUARIOS = require('@altertex/util/const/consultasUsuarios'); + +//RF[4] Actualizar Usuario - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF4 +/** + * Repositorio para actualizar los datos de un usuario en la BD. + * + * Recorre un arreglo de objetos que contienen la información de cada usuario para + * actualizar su información. + * + * Utiliza un array de objetos con la información del usuario, y hace la + * consulta correspondiente a la base de datos. + * + * @function actualizarUsuario + * @async + * @param {Array<{ idUsuario: number, nombreCompleto: string, correoElectronico: string, telefono: string }>} datos - Lista de + * información del usuario a actualizar. + * @throws {Error} Si el arreglo está vacío o si ocurre un error en la base de datos. + * @returns {Promise} Promesa que se resuelve cuando todas las actualizaciones han sido ejecutadas. + */ +exports.actualizarUsuario = async (datos) => { + if (!Array.isArray(datos) || datos.length === 0) { + throw new Error('Sin datos para actualizar.'); + } + try { + await Promise.all( + datos.map(({ idUsuario, nombreCompleto, correoElectronico, telefono }) => { + return correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR, [ + nombreCompleto, + correoElectronico, + telefono, + idUsuario, + ]); + }) + ); + } catch { + throw new Error(MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje); + } +}; diff --git a/Usuarios/Rutas/RutasIndividuales/actualizarUsuario.routes.js b/Usuarios/Rutas/RutasIndividuales/actualizarUsuario.routes.js new file mode 100644 index 00000000..e69de29b diff --git a/Usuarios/Rutas/indexUsuarios.routes.js b/Usuarios/Rutas/indexUsuarios.routes.js index 5dc5ed19..9926a904 100644 --- a/Usuarios/Rutas/indexUsuarios.routes.js +++ b/Usuarios/Rutas/indexUsuarios.routes.js @@ -4,6 +4,7 @@ const rutasCrearUsuario = require('@altertex/usu/rutasInd/crearUsuario.routes'); const rutasLeerUsuario = require('@altertex/usu/rutasInd/leerUsuario.routes'); const rutasConsultarListaUsuarios = require('@altertex/usu/rutasInd/consultarListaUsuarios.routes'); const rutasEliminarUsuario = require('@altertex/usu/rutasInd/eliminarUsuario.routes'); +const rutasActualizarUsuario = require('@altertex/usu/rutasInd/actualizarUsuario.routes'); const RUTAS = require('@altertex/util/const/rutas'); diff --git a/Utilidades/Constantes/consultasUsuarios.js b/Utilidades/Constantes/consultasUsuarios.js index c0668c4c..7768c4a6 100644 --- a/Utilidades/Constantes/consultasUsuarios.js +++ b/Utilidades/Constantes/consultasUsuarios.js @@ -93,6 +93,14 @@ module.exports = { DELETE FROM usuario WHERE idUsuario = (?); `, + + ACTUALIZAR: ` + UPDATE usuario SET + nombreCompleto = ?, correoElectronico = ?, estatus = ? + , + cantidadPuntos = ?, antiguedad = ? WHERE idUsuario = ?; + `, + VALIDAR_CORREO: ` SELECT idUsuario FROM usuario @@ -176,6 +184,4 @@ module.exports = { WHERE idUsuario IN (?) AND puedeActivar2FA = true; `, - - }; diff --git a/Utilidades/Constantes/mensajesUsuarios.js b/Utilidades/Constantes/mensajesUsuarios.js index ac2ddeee..2869d7cd 100644 --- a/Utilidades/Constantes/mensajesUsuarios.js +++ b/Utilidades/Constantes/mensajesUsuarios.js @@ -14,6 +14,10 @@ module.exports = { codigo: 200, mensaje: 'Lista de usuarios obtenida exitosamente.', }, + USUARIO_ACTUALIZADO: { + codigo: 200, + mensaje: 'Usuario actualizado correctamente.', + }, // 204 - Sin contenido USUARIOS_NO_ENCONTRADOS: { @@ -87,4 +91,8 @@ module.exports = { codigo: 500, mensaje: 'Ocurrió un error al intentar eliminar el usuario.', }, + ERROR_ACTUALIZAR_USUARIO: { + codigo: 500, + mensaje: 'Ocurrió un error al intentar actualizar el usuario.', + }, }; From 162ae4233f500d79daaa940d18bc518daa647230 Mon Sep 17 00:00:00 2001 From: max Date: Fri, 30 May 2025 18:31:22 -0600 Subject: [PATCH 04/17] feat(usuarios): add update user route and controller implementation --- .../actualizarEmpleado.routes.js | 1 - .../actualizarUsuario.controller.js | 35 +-- .../repositorioActualizarUsuario.js | 1 + .../actualizarUsuario.routes.js | 227 ++++++++++++++++++ Usuarios/Rutas/indexUsuarios.routes.js | 1 + Utilidades/Constantes/consultasUsuarios.js | 22 +- Utilidades/Constantes/rutas.js | 3 +- 7 files changed, 266 insertions(+), 24 deletions(-) diff --git a/Empleados/Rutas/RutasIndividuales/actualizarEmpleado.routes.js b/Empleados/Rutas/RutasIndividuales/actualizarEmpleado.routes.js index 544300ab..fabe5d4e 100644 --- a/Empleados/Rutas/RutasIndividuales/actualizarEmpleado.routes.js +++ b/Empleados/Rutas/RutasIndividuales/actualizarEmpleado.routes.js @@ -10,7 +10,6 @@ const revisarPermisos = require('@altertex/util/inter/verificarPermisos'); const validarYSanitizar = require('@altertex/util/inter/validarYSanitizar'); const limitePeticionesDiarias = require('@altertex/util/inter/limitePeticiones'); - //RF[19] Actualizar Empleado - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF19] /** diff --git a/Usuarios/Controladores/actualizarUsuario.controller.js b/Usuarios/Controladores/actualizarUsuario.controller.js index 8569655c..6e58b502 100644 --- a/Usuarios/Controladores/actualizarUsuario.controller.js +++ b/Usuarios/Controladores/actualizarUsuario.controller.js @@ -1,6 +1,7 @@ const MENSAJES = require('@altertex/util/const/mensajesUsuarios'); -const repositorio = require('@altertex/emp/repos/repositorioActualizarUsuario'); +const repositorio = require('@altertex/usu/repos/repositorioActualizarUsuario'); //RF[4] Actualizar Usuario - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF4] + /** * Controlador para actualizar la información de un usuario. * @@ -30,20 +31,20 @@ exports.actualizarUsuario = async (req, res) => { .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) .json({ mensaje: MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje }); } -}; -if (!datos || datos.length === 0) { - return res - .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) - .json({ mensaje: MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje }); -} -try { - await repositorio.actualizarUsuario(datos); - return res - .status(MENSAJES.USUARIO_ACTUALIZADO.codigo) - .json({ mensaje: MENSAJES.USUARIO_ACTUALIZADO.mensaje, datos }); -} catch { - return res - .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) - .json({ mensaje: MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje }); -} + if (!datos || datos.length === 0) { + return res + .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) + .json({ mensaje: MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje }); + } + try { + await repositorio.actualizarUsuario(datos); + return res + .status(MENSAJES.USUARIO_ACTUALIZADO.codigo) + .json({ mensaje: MENSAJES.USUARIO_ACTUALIZADO.mensaje, datos }); + } catch { + return res + .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) + .json({ mensaje: MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje }); + } +}; diff --git a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js index c4854ef8..c9b0d69a 100644 --- a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js +++ b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js @@ -3,6 +3,7 @@ const MENSAJES = require('@altertex/util/const/mensajesUsuarios'); const CONSULTAS_USUARIOS = require('@altertex/util/const/consultasUsuarios'); //RF[4] Actualizar Usuario - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF4 + /** * Repositorio para actualizar los datos de un usuario en la BD. * diff --git a/Usuarios/Rutas/RutasIndividuales/actualizarUsuario.routes.js b/Usuarios/Rutas/RutasIndividuales/actualizarUsuario.routes.js index e69de29b..d3239e98 100644 --- a/Usuarios/Rutas/RutasIndividuales/actualizarUsuario.routes.js +++ b/Usuarios/Rutas/RutasIndividuales/actualizarUsuario.routes.js @@ -0,0 +1,227 @@ +const express = require('express'); +const ruteador = express.Router(); +const PERMISOS = require('@altertex/util/const/permisos'); +const RUTAS = require('@altertex/util/const/rutas'); +const controlador = require('@altertex/usu/ctrl/actualizarUsuario.controller'); +const revisarApiKey = require('@altertex/util/inter/revisarApiKey'); +const autorizarToken = require('@altertex/util/inter/autorizarToken'); +const revisarPermisos = require('@altertex/util/inter/verificarPermisos'); +const validarYSanitizar = require('@altertex/util/inter/validarYSanitizar'); +const limitePeticionesDiarias = require('@altertex/util/inter/limitePeticiones'); + +//RF[4] Actualizar Usuario - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF4] + +/** + * @swagger + * /api/usuarios/actualizar: + * put: + * summary: Actualiza la información de un usuario. + * tags: + * - Usuarios + * security: + * - ApiKeyAuth: [] + * - BearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * oneOf: + * - type: object + * description: Información del usuario a actualizar directamente + * required: + * - idUsuario + * - nombreCompleto + * - correoElectronico + * - contrasenia + * - numeroTelefono + * - direccion + * - fechaNacimiento + * - genero + * - estatus + * - idRol + * properties: + * idUsuario: + * type: integer + * example: 30 + * nombreCompleto: + * type: string + * example: "Luis Hernández" + * correoElectronico: + * type: string + * example: "lhernandez@gmail.com" + * contrasenia: + * type: string + * example: "NuevaContraseniaSegura123" + * numeroTelefono: + * type: string + * example: "5551234567" + * direccion: + * type: string + * example: "Av. Revolución 123, CDMX" + * fechaNacimiento: + * type: string + * format: date + * example: "1995-06-15" + * genero: + * type: string + * enum: [Masculino, Femenino, Otro] + * example: "Masculino" + * estatus: + * type: boolean + * example: true + * idRol: + * type: integer + * example: 2 + * - type: object + * properties: + * usuarios: + * oneOf: + * - type: object + * required: + * - idUsuario + * - nombreCompleto + * - correoElectronico + * - contrasenia + * - numeroTelefono + * - direccion + * - fechaNacimiento + * - genero + * - estatus + * - idRol + * properties: + * idUsuario: + * type: integer + * example: 30 + * nombreCompleto: + * type: string + * example: "Luis Hernández" + * correoElectronico: + * type: string + * example: "lhernandez@gmail.com" + * contrasenia: + * type: string + * example: "NuevaContraseniaSegura123" + * numeroTelefono: + * type: string + * example: "5551234567" + * direccion: + * type: string + * example: "Av. Revolución 123, CDMX" + * fechaNacimiento: + * type: string + * format: date + * example: "1995-06-15" + * genero: + * type: string + * example: "Masculino" + * estatus: + * type: boolean + * example: true + * idRol: + * type: integer + * example: 2 + * - type: array + * items: + * type: object + * required: + * - idUsuario + * - nombreCompleto + * - correoElectronico + * - contrasenia + * - numeroTelefono + * - direccion + * - fechaNacimiento + * - genero + * - estatus + * - idRol + * properties: + * idUsuario: + * type: integer + * example: 30 + * nombreCompleto: + * type: string + * example: "Luis Hernández" + * correoElectronico: + * type: string + * example: "lhernandez@gmail.com" + * contrasenia: + * type: string + * example: "NuevaContraseniaSegura123" + * numeroTelefono: + * type: string + * example: "5551234567" + * direccion: + * type: string + * example: "Av. Revolución 123, CDMX" + * fechaNacimiento: + * type: string + * format: date + * example: "1995-06-15" + * genero: + * type: string + * example: "Masculino" + * estatus: + * type: boolean + * example: true + * idRol: + * type: integer + * example: 2 + * responses: + * 200: + * description: Información del usuario actualizada correctamente. + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: "Usuario actualizado con éxito." + * datos: + * type: array + * items: + * type: object + * 400: + * description: Error en los datos enviados o en la actualización. + * content: + * application/json: + * schema: + * type: object + * properties: + * mensaje: + * type: string + * example: "Error al actualizar el usuario." + * x-codeSamples: + * - lang: JavaScript + * label: cURL + * source: | + * curl -X PUT "https://tu-api.com/api/usuarios/actualizar" \ + * -H "x-api-key: TU_API_KEY" \ + * -H "Authorization: Bearer TU_TOKEN" \ + * -H "Content-Type: application/json" \ + * -d '{ + * "idUsuario": 30, + * "nombreCompleto": "Luis Hernández", + * "correoElectronico": "lhernandez@gmail.com", + * "contrasenia": "NuevaContraseniaSegura123", + * "numeroTelefono": "5551234567", + * "direccion": "Av. Revolución 123, CDMX", + * "fechaNacimiento": "1995-06-15", + * "genero": "Masculino", + * "estatus": true, + * "idRol": 2 + * }' + */ + +ruteador.put( + RUTAS.USUARIOS.ACTUALIZAR, + revisarApiKey(), + validarYSanitizar, + autorizarToken, + limitePeticionesDiarias, + revisarPermisos(PERMISOS.ACTUALIZAR_USUARIO), + controlador.actualizarUsuario +); + +module.exports = ruteador; diff --git a/Usuarios/Rutas/indexUsuarios.routes.js b/Usuarios/Rutas/indexUsuarios.routes.js index 9926a904..f6fa16d0 100644 --- a/Usuarios/Rutas/indexUsuarios.routes.js +++ b/Usuarios/Rutas/indexUsuarios.routes.js @@ -12,5 +12,6 @@ ruteador.use(RUTAS.USUARIOS.BASE, rutasConsultarListaUsuarios); ruteador.use(RUTAS.USUARIOS.BASE, rutasCrearUsuario); ruteador.use(RUTAS.USUARIOS.BASE, rutasLeerUsuario); ruteador.use(RUTAS.USUARIOS.BASE, rutasEliminarUsuario); +ruteador.use(RUTAS.USUARIOS.BASE, rutasActualizarUsuario); module.exports = ruteador; diff --git a/Utilidades/Constantes/consultasUsuarios.js b/Utilidades/Constantes/consultasUsuarios.js index 7768c4a6..ff9fc335 100644 --- a/Utilidades/Constantes/consultasUsuarios.js +++ b/Utilidades/Constantes/consultasUsuarios.js @@ -94,11 +94,23 @@ module.exports = { WHERE idUsuario = (?); `, - ACTUALIZAR: ` - UPDATE usuario SET - nombreCompleto = ?, correoElectronico = ?, estatus = ? - , - cantidadPuntos = ?, antiguedad = ? WHERE idUsuario = ?; + ACTUALIZAR_DATOS_USUARIO: ` +UPDATE usuario SET + nombreCompleto = ?, + correoElectronico = ?, + contrasenia = ?, + numeroTelefono = ?, + direccion = ?, + fechaNacimiento = ?, + genero = ?, + estatus = ? +WHERE idUsuario = ?; + `, + + ACTUALIZAR_ROL_USUARIO: ` +UPDATE usuario_rol SET +idRol = ? +WHERE idUsuario = ?; `, VALIDAR_CORREO: ` diff --git a/Utilidades/Constantes/rutas.js b/Utilidades/Constantes/rutas.js index 92946212..31f8128c 100644 --- a/Utilidades/Constantes/rutas.js +++ b/Utilidades/Constantes/rutas.js @@ -8,7 +8,7 @@ module.exports = { CERRAR_SESION: '/cerrar-sesion', USUARIO_AUTENTICADO: '/autenticar', ACTIVAR_2FA: '/activar-2fa', - VERIFICAR_2FA: '/verificar-2fa' + VERIFICAR_2FA: '/verificar-2fa', }, USUARIOS: { BASE: '/usuarios', @@ -16,6 +16,7 @@ module.exports = { CREAR: '/crear', ELIMINAR_USUARIOS: '/eliminar-usuarios', LEER: '/consultar-usuario', + ACTUALIZAR: '/actualizar-usuario', }, CATEGORIAS: { BASE: '/categorias', From 5c04f59f1a48f0d4f130e0855d97539194349b0c Mon Sep 17 00:00:00 2001 From: max Date: Tue, 3 Jun 2025 13:30:00 -0600 Subject: [PATCH 05/17] feat(usuarios): enhance user update functionality and improve user list response formatting --- .../consultarListaUsuarios.controller.js | 13 +++++- .../repositorioActualizarUsuario.js | 29 +++++++++++-- Utilidades/Constantes/consultasUsuarios.js | 41 +++++++++++-------- 3 files changed, 61 insertions(+), 22 deletions(-) diff --git a/Usuarios/Controladores/consultarListaUsuarios.controller.js b/Usuarios/Controladores/consultarListaUsuarios.controller.js index 1a52225f..70e675df 100644 --- a/Usuarios/Controladores/consultarListaUsuarios.controller.js +++ b/Usuarios/Controladores/consultarListaUsuarios.controller.js @@ -26,9 +26,20 @@ exports.consultarListaUsuarios = async (req, res) => { .json({ mensaje: MENSAJES_USUARIOS.USUARIOS_NO_ENCONTRADOS.mensaje }); } + const generoMap = { + masculino: 'Hombre', + femenino: 'Mujer', + otro: 'Otro', + }; + + const resultadosMapeados = resultados.map((usuario) => ({ + ...usuario, + genero: generoMap[usuario.genero] || usuario.genero, + })); + return res.status(MENSAJES_USUARIOS.LISTA_USUARIOS_OBTENIDA.codigo).json({ mensaje: MENSAJES_USUARIOS.LISTA_USUARIOS_OBTENIDA.mensaje, - listaUsuarios: resultados, + listaUsuarios: resultadosMapeados, }); } catch { return res diff --git a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js index c9b0d69a..3cb1f688 100644 --- a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js +++ b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js @@ -26,13 +26,36 @@ exports.actualizarUsuario = async (datos) => { } try { await Promise.all( - datos.map(({ idUsuario, nombreCompleto, correoElectronico, telefono }) => { - return correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR, [ + datos.map(async (usuario) => { + const { + idUsuario, nombreCompleto, correoElectronico, - telefono, + contrasenia, + numeroTelefono, + direccion, + fechaNacimiento, + genero, + estatus, + idCliente, + } = usuario; + + // Actualiza datos del usuario + await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO, [ + nombreCompleto, + correoElectronico, + contrasenia, + numeroTelefono, + direccion, + fechaNacimiento, + genero, + estatus, idUsuario, ]); + + if (idCliente !== undefined) { + await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_CLIENTE_USUARIO, [idCliente, idUsuario]); + } }) ); } catch { diff --git a/Utilidades/Constantes/consultasUsuarios.js b/Utilidades/Constantes/consultasUsuarios.js index 919d8122..c7589a64 100644 --- a/Utilidades/Constantes/consultasUsuarios.js +++ b/Utilidades/Constantes/consultasUsuarios.js @@ -54,7 +54,7 @@ module.exports = { u.fechaNacimiento, u.genero, u.estatus, - r.nombre AS rol, + r.idRol AS rol, uc.idCliente, c.nombreComercial AS nombreCliente FROM usuario u @@ -66,23 +66,22 @@ module.exports = { `, OBTENER_LISTA: ` - SELECT u.idUsuario, - u.nombreCompleto AS nombre, - r.nombre AS rol, - c.nombreComercial AS cliente, - u.estatus, - u.correoElectronico AS correo, - u.numeroTelefono AS telefono - FROM usuario u - LEFT JOIN usuario_rol ur ON u.idUsuario = ur.idUsuario - LEFT JOIN rol r ON ur.idRol = r.idRol - LEFT JOIN usuario_cliente uc ON u.idUsuario = uc.idUsuario - LEFT JOIN cliente c ON uc.idCliente = c.idCliente - WHERE u.idUsuario NOT IN (SELECT ur2.idUsuario - FROM usuario_rol ur2 - WHERE ur2.idRol = 3); - - `, + SELECT u.idUsuario, + u.nombreCompleto AS nombre, + r.idRol AS rol, + c.nombreComercial AS cliente, + u.estatus, + u.correoElectronico AS correo, + u.numeroTelefono AS telefono + FROM usuario u + LEFT JOIN usuario_rol ur ON u.idUsuario = ur.idUsuario + LEFT JOIN rol r ON ur.idRol = r.idRol + LEFT JOIN usuario_cliente uc ON u.idUsuario = uc.idUsuario + LEFT JOIN cliente c ON uc.idCliente = c.idCliente + WHERE u.idUsuario NOT IN (SELECT ur2.idUsuario + FROM usuario_rol ur2 + WHERE ur2.idRol = 3); +`, ELIMINAR_USUARIOS: ` DELETE @@ -103,6 +102,12 @@ UPDATE usuario SET WHERE idUsuario = ?; `, + ACTUALIZAR_CLIENTE_USUARIO: ` +UPDATE usuario_cliente SET + idCliente = ? +WHERE idUsuario = ?; + `, + ACTUALIZAR_ROL_USUARIO: ` UPDATE usuario_rol SET idRol = ? From 836b786463018c39df301594e4b7507a33d0de1b Mon Sep 17 00:00:00 2001 From: max Date: Wed, 4 Jun 2025 12:12:35 -0600 Subject: [PATCH 06/17] feat(usuarios): enhance user-client association logic in actualizarUsuario function --- .../Datos/Repositorios/repositorioActualizarUsuario.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js index 3cb1f688..9cd79266 100644 --- a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js +++ b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js @@ -54,7 +54,15 @@ exports.actualizarUsuario = async (datos) => { ]); if (idCliente !== undefined) { - await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_CLIENTE_USUARIO, [idCliente, idUsuario]); + // Intenta actualizar primero + const resultado = await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_CLIENTE_USUARIO, [ + idCliente, + idUsuario, + ]); + // Si no se actualizó ninguna fila, inserta la relación + if (resultado.affectedRows === 0) { + await correrQuery(CONSULTAS_USUARIOS.ASOCIAR_USUARIO_A_CLIENTE, [idUsuario, idCliente]); + } } }) ); From 513f18ec5346b1dc32c0ef4bab649f93275c90ef Mon Sep 17 00:00:00 2001 From: max Date: Thu, 5 Jun 2025 20:10:08 -0600 Subject: [PATCH 07/17] Co-authored-by: Nicolas Hood Figueroa --- .../actualizarUsuario.controller.js | 2 + .../repositorioActualizarUsuario.js | 68 +++++++++++++------ Utilidades/Constantes/consultasUsuarios.js | 45 +++++++----- 3 files changed, 78 insertions(+), 37 deletions(-) diff --git a/Usuarios/Controladores/actualizarUsuario.controller.js b/Usuarios/Controladores/actualizarUsuario.controller.js index 6e58b502..b071e318 100644 --- a/Usuarios/Controladores/actualizarUsuario.controller.js +++ b/Usuarios/Controladores/actualizarUsuario.controller.js @@ -1,5 +1,7 @@ const MENSAJES = require('@altertex/util/const/mensajesUsuarios'); const repositorio = require('@altertex/usu/repos/repositorioActualizarUsuario'); +const bcrypt = require('bcryptjs'); + //RF[4] Actualizar Usuario - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF4] /** diff --git a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js index 9cd79266..cd216091 100644 --- a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js +++ b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js @@ -40,29 +40,57 @@ exports.actualizarUsuario = async (datos) => { idCliente, } = usuario; - // Actualiza datos del usuario - await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO, [ - nombreCompleto, - correoElectronico, - contrasenia, - numeroTelefono, - direccion, - fechaNacimiento, - genero, - estatus, - idUsuario, - ]); + const conContrasena = () => (usuario.contrasenia == '' ? false : true); - if (idCliente !== undefined) { - // Intenta actualizar primero - const resultado = await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_CLIENTE_USUARIO, [ - idCliente, + // Actualiza datos del usuario + if (conContrasena()) { + await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO, [ + nombreCompleto, + correoElectronico, + contrasenia, + numeroTelefono, + direccion, + fechaNacimiento, + genero, + estatus, + idUsuario, + ]); + } else { + await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO_SIN_CONTRASENA, [ + nombreCompleto, + correoElectronico, + numeroTelefono, + direccion, + fechaNacimiento, + genero, + estatus, idUsuario, ]); - // Si no se actualizó ninguna fila, inserta la relación - if (resultado.affectedRows === 0) { - await correrQuery(CONSULTAS_USUARIOS.ASOCIAR_USUARIO_A_CLIENTE, [idUsuario, idCliente]); - } + } + + if (usuario.cliente) { + // Pasar el cliente/clientes a un array + const clientes = Array.isArray(usuario.cliente) ? usuario.cliente : [usuario.cliente]; + + await correrQuery(CONSULTAS_USUARIOS.ELIMINAR_USUARIO_CLIENTE, [idUsuario]).then( + // Eliminar todos los cliente asociados al usuario + async () => { + try { + for (const cliente of clientes) { + // Asociar cada cliente seleccionado al usuario + if (cliente) { + // Evitar errores si el cliente es undefined o null + await correrQuery(CONSULTAS_USUARIOS.ASOCIAR_USUARIO_A_CLIENTE, [ + idUsuario, + cliente, + ]); + } + } + } catch (error) { + return error; + } + } + ); } }) ); diff --git a/Utilidades/Constantes/consultasUsuarios.js b/Utilidades/Constantes/consultasUsuarios.js index c7589a64..f263f80a 100644 --- a/Utilidades/Constantes/consultasUsuarios.js +++ b/Utilidades/Constantes/consultasUsuarios.js @@ -40,6 +40,11 @@ module.exports = { VALUES (?, ?); `, + BORRAR_ASOCIACIONES_CLIENTE_USUARIO: ` + DELETE FROM usuario_cliente + WHERE idUsuario = ?; + `, + ASOCIAR_USUARIO_A_CLIENTE: ` INSERT INTO usuario_cliente (idUsuario, idCliente) VALUES (?, ?); @@ -90,28 +95,34 @@ module.exports = { `, ACTUALIZAR_DATOS_USUARIO: ` -UPDATE usuario SET - nombreCompleto = ?, - correoElectronico = ?, - contrasenia = ?, - numeroTelefono = ?, - direccion = ?, - fechaNacimiento = ?, - genero = ?, - estatus = ? -WHERE idUsuario = ?; + UPDATE usuario SET + nombreCompleto = ?, + correoElectronico = ?, + contrasenia = ?, + numeroTelefono = ?, + direccion = ?, + fechaNacimiento = ?, + genero = ?, + estatus = ? + WHERE idUsuario = ?; `, - ACTUALIZAR_CLIENTE_USUARIO: ` -UPDATE usuario_cliente SET - idCliente = ? -WHERE idUsuario = ?; + ACTUALIZAR_DATOS_USUARIO_SIN_CONTRASENA: ` + UPDATE usuario SET + nombreCompleto = ?, + correoElectronico = ?, + numeroTelefono = ?, + direccion = ?, + fechaNacimiento = ?, + genero = ?, + estatus = ? + WHERE idUsuario = ?; `, ACTUALIZAR_ROL_USUARIO: ` -UPDATE usuario_rol SET -idRol = ? -WHERE idUsuario = ?; + UPDATE usuario_rol SET + idRol = ? + WHERE idUsuario = ?; `, VALIDAR_CORREO: ` From b5dcd00c67c760a1cd39ff0a0f9c8829fd97c653 Mon Sep 17 00:00:00 2001 From: max Date: Thu, 5 Jun 2025 21:22:33 -0600 Subject: [PATCH 08/17] feat(usuarios): add password hashing for user updates in actualizarUsuario function --- Usuarios/Controladores/actualizarUsuario.controller.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Usuarios/Controladores/actualizarUsuario.controller.js b/Usuarios/Controladores/actualizarUsuario.controller.js index b071e318..16a60a44 100644 --- a/Usuarios/Controladores/actualizarUsuario.controller.js +++ b/Usuarios/Controladores/actualizarUsuario.controller.js @@ -28,6 +28,8 @@ exports.actualizarUsuario = async (req, res) => { } else if (req.body.cambios) { // Si la información viene en el formato esperado (hay cambios) datos = Array.isArray(req.body.cambios) ? req.body.cambios : [req.body.cambios]; + const contraseniaEncriptada = await bcrypt.hash(datos[0]['contrasenia'], 10); + datos[0]['contrasenia'] = contraseniaEncriptada; } else { return res .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) From 1ce82c1627fcc29ffdb345ff38353d5b8a31025e Mon Sep 17 00:00:00 2001 From: max Date: Fri, 6 Jun 2025 15:30:52 -0600 Subject: [PATCH 09/17] fix(usuarios): improve error handling in actualizarUsuario and clarify idCliente alias in consultasUsuarios --- Usuarios/Controladores/actualizarUsuario.controller.js | 2 +- Utilidades/Constantes/consultasUsuarios.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Usuarios/Controladores/actualizarUsuario.controller.js b/Usuarios/Controladores/actualizarUsuario.controller.js index 16a60a44..f4159747 100644 --- a/Usuarios/Controladores/actualizarUsuario.controller.js +++ b/Usuarios/Controladores/actualizarUsuario.controller.js @@ -46,7 +46,7 @@ exports.actualizarUsuario = async (req, res) => { return res .status(MENSAJES.USUARIO_ACTUALIZADO.codigo) .json({ mensaje: MENSAJES.USUARIO_ACTUALIZADO.mensaje, datos }); - } catch { + } catch (e) { return res .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) .json({ mensaje: MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje }); diff --git a/Utilidades/Constantes/consultasUsuarios.js b/Utilidades/Constantes/consultasUsuarios.js index f263f80a..bbf590d5 100644 --- a/Utilidades/Constantes/consultasUsuarios.js +++ b/Utilidades/Constantes/consultasUsuarios.js @@ -60,7 +60,7 @@ module.exports = { u.genero, u.estatus, r.idRol AS rol, - uc.idCliente, + uc.idCliente AS idCliente, c.nombreComercial AS nombreCliente FROM usuario u LEFT JOIN usuario_rol ur ON u.idUsuario = ur.idUsuario From 57b6dbd0f36581aa99d7b7e8ab94f5fd98b79e60 Mon Sep 17 00:00:00 2001 From: max Date: Fri, 6 Jun 2025 17:47:31 -0600 Subject: [PATCH 10/17] fix(package): add license information for csurf package --- package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package-lock.json b/package-lock.json index de61bb78..7420e567 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6133,6 +6133,7 @@ "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", "deprecated": "This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions", + "license": "MIT", "dependencies": { "cookie": "0.4.0", "cookie-signature": "1.0.6", From ed5b689191bece682c4dc83dd4f29d83554a6bcb Mon Sep 17 00:00:00 2001 From: max Date: Fri, 6 Jun 2025 19:49:33 -0600 Subject: [PATCH 11/17] fix(actualizarUsuario): streamline password handling and improve error management --- .../actualizarUsuario.controller.js | 6 +- .../repositorioActualizarUsuario.js | 115 ++++++++++-------- 2 files changed, 65 insertions(+), 56 deletions(-) diff --git a/Usuarios/Controladores/actualizarUsuario.controller.js b/Usuarios/Controladores/actualizarUsuario.controller.js index f4159747..122a8ee1 100644 --- a/Usuarios/Controladores/actualizarUsuario.controller.js +++ b/Usuarios/Controladores/actualizarUsuario.controller.js @@ -28,8 +28,8 @@ exports.actualizarUsuario = async (req, res) => { } else if (req.body.cambios) { // Si la información viene en el formato esperado (hay cambios) datos = Array.isArray(req.body.cambios) ? req.body.cambios : [req.body.cambios]; - const contraseniaEncriptada = await bcrypt.hash(datos[0]['contrasenia'], 10); - datos[0]['contrasenia'] = contraseniaEncriptada; + const contraseniaEncriptada = await bcrypt.hash(datos[0].contrasenia, 10); + datos[0].contrasenia = contraseniaEncriptada; } else { return res .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) @@ -46,7 +46,7 @@ exports.actualizarUsuario = async (req, res) => { return res .status(MENSAJES.USUARIO_ACTUALIZADO.codigo) .json({ mensaje: MENSAJES.USUARIO_ACTUALIZADO.mensaje, datos }); - } catch (e) { + } catch { return res .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) .json({ mensaje: MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje }); diff --git a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js index cd216091..7bed3405 100644 --- a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js +++ b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js @@ -26,73 +26,82 @@ exports.actualizarUsuario = async (datos) => { } try { await Promise.all( - datos.map(async (usuario) => { - const { - idUsuario, - nombreCompleto, - correoElectronico, - contrasenia, - numeroTelefono, - direccion, - fechaNacimiento, - genero, - estatus, - idCliente, - } = usuario; - - const conContrasena = () => (usuario.contrasenia == '' ? false : true); - - // Actualiza datos del usuario - if (conContrasena()) { - await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO, [ - nombreCompleto, - correoElectronico, - contrasenia, - numeroTelefono, - direccion, - fechaNacimiento, - genero, - estatus, + datos.map( + /** + * Actualiza la información de un usuario individualmente. + * @param {object} usuario - Objeto con los datos del usuario a actualizar. + * @returns {Promise} + */ + async (usuario) => { + const { idUsuario, - ]); - } else { - await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO_SIN_CONTRASENA, [ nombreCompleto, correoElectronico, + contrasenia, numeroTelefono, direccion, fechaNacimiento, genero, estatus, - idUsuario, - ]); - } + } = usuario; + + const conContrasena = () => (usuario.contrasenia == '' ? false : true); + + // Actualiza datos del usuario + if (conContrasena()) { + await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO, [ + nombreCompleto, + correoElectronico, + contrasenia, + numeroTelefono, + direccion, + fechaNacimiento, + genero, + estatus, + idUsuario, + ]); + } else { + await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO_SIN_CONTRASENA, [ + nombreCompleto, + correoElectronico, + numeroTelefono, + direccion, + fechaNacimiento, + genero, + estatus, + idUsuario, + ]); + } - if (usuario.cliente) { - // Pasar el cliente/clientes a un array - const clientes = Array.isArray(usuario.cliente) ? usuario.cliente : [usuario.cliente]; + if (usuario.cliente) { + // Pasar el cliente/clientes a un array + const clientes = Array.isArray(usuario.cliente) ? usuario.cliente : [usuario.cliente]; - await correrQuery(CONSULTAS_USUARIOS.ELIMINAR_USUARIO_CLIENTE, [idUsuario]).then( - // Eliminar todos los cliente asociados al usuario - async () => { - try { - for (const cliente of clientes) { - // Asociar cada cliente seleccionado al usuario - if (cliente) { - // Evitar errores si el cliente es undefined o null - await correrQuery(CONSULTAS_USUARIOS.ASOCIAR_USUARIO_A_CLIENTE, [ - idUsuario, - cliente, - ]); + await correrQuery(CONSULTAS_USUARIOS.ELIMINAR_USUARIO_CLIENTE, [idUsuario]).then( + /** + * Función que reasocia los clientes al usuario después de eliminarlos. + * @returns {Promise} + */ + async () => { + try { + for (const cliente of clientes) { + // Asociar cada cliente seleccionado al usuario + if (cliente) { + // Evitar errores si el cliente es undefined o null + await correrQuery(CONSULTAS_USUARIOS.ASOCIAR_USUARIO_A_CLIENTE, [ + idUsuario, + cliente, + ]); + } } + } catch (error) { + return error; } - } catch (error) { - return error; } - } - ); + ); + } } - }) + ) ); } catch { throw new Error(MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje); From 0a7d94380038013ee01c7c627601d56e6af30f7a Mon Sep 17 00:00:00 2001 From: max Date: Fri, 6 Jun 2025 20:04:35 -0600 Subject: [PATCH 12/17] fix(actualizarUsuario): simplify password check logic in user update function --- Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js index 7bed3405..1bc17637 100644 --- a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js +++ b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js @@ -45,10 +45,10 @@ exports.actualizarUsuario = async (datos) => { estatus, } = usuario; - const conContrasena = () => (usuario.contrasenia == '' ? false : true); + const conContrasena = usuario.contrasenia == '' ? false : true; // Actualiza datos del usuario - if (conContrasena()) { + if (conContrasena) { await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO, [ nombreCompleto, correoElectronico, From 45f6beedd0ad365ae5d6556596fcbacd1cfed0fe Mon Sep 17 00:00:00 2001 From: max Date: Fri, 6 Jun 2025 21:53:40 -0600 Subject: [PATCH 13/17] [FIX] - Arreglar actualizar rol de usuario --- .../Datos/Repositorios/repositorioActualizarUsuario.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js index 1bc17637..cb7ed582 100644 --- a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js +++ b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js @@ -41,12 +41,15 @@ exports.actualizarUsuario = async (datos) => { numeroTelefono, direccion, fechaNacimiento, + idRol, genero, estatus, } = usuario; const conContrasena = usuario.contrasenia == '' ? false : true; + console.log('roles', idRol); + // Actualiza datos del usuario if (conContrasena) { await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO, [ @@ -100,6 +103,12 @@ exports.actualizarUsuario = async (datos) => { } ); } + + const result = await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_ROL_USUARIO, [ + idRol, + idUsuario, + ]); + console.log('Resultado de actualizar rol:', result); } ) ); From 12953d9d43c9af9754ae71aa24f167c945c340d9 Mon Sep 17 00:00:00 2001 From: max Date: Fri, 6 Jun 2025 21:59:26 -0600 Subject: [PATCH 14/17] =?UTF-8?q?integraci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Constantes/mensajesSetsProductos.js | 4 + ...actualizarSetsProductos.controller.test.js | 83 +++++++++++++++++++ ...ultarListaSetsProductos.controller.test.js | 81 ++++++++++++++++++ jest.config.js | 4 + 4 files changed, 172 insertions(+) create mode 100644 _tests_/SetsProductos/actualizarSetsProductos.controller.test.js create mode 100644 _tests_/SetsProductos/consultarListaSetsProductos.controller.test.js diff --git a/Utilidades/Constantes/mensajesSetsProductos.js b/Utilidades/Constantes/mensajesSetsProductos.js index a2333e04..3d8291c9 100644 --- a/Utilidades/Constantes/mensajesSetsProductos.js +++ b/Utilidades/Constantes/mensajesSetsProductos.js @@ -88,4 +88,8 @@ module.exports = { codigo: 400, mensaje: 'La lista de productos contiene elementos inválidos', }, + PARAMETROS_INVALIDOS: { + codigo: 400, + mensaje: 'Parámetros inválidos', + }, }; diff --git a/_tests_/SetsProductos/actualizarSetsProductos.controller.test.js b/_tests_/SetsProductos/actualizarSetsProductos.controller.test.js new file mode 100644 index 00000000..3aea3b8d --- /dev/null +++ b/_tests_/SetsProductos/actualizarSetsProductos.controller.test.js @@ -0,0 +1,83 @@ +const request = require('supertest'); +const express = require('express'); +const bodyParser = require('body-parser'); +const MENSAJES = require('@altertex/util/const/mensajesSetsProductos'); + +jest.mock('@altertex/setspro/repos/repositorioActualizarSetsProductos', () => ({ + actualizarSetProductos: jest.fn() +})); + +const repositorio = require('@altertex/setspro/repos/repositorioActualizarSetsProductos'); +const controller = require('@altertex/setspro/ctrl/actualizarSetsProductos.controller'); + +const app = express(); +app.use(bodyParser.json()); + +// 👇 Middleware para simular usuario con cliente seleccionado +app.use((req, res, next) => { + req.user = { clienteSeleccionado: 1 }; + next(); +}); + +app.put('/sets-productos', controller.actualizarSetProductos); + +describe('Controlador actualizarSetProductos', () => { + // Silenciar console.error durante las pruebas para evitar ruido en la salida + beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + // Restaurar el comportamiento original de console.error + afterAll(() => { + console.error.mockRestore(); + }); + + // Limpia los mocks después de cada test + afterEach(() => jest.clearAllMocks()); + + it('debe devolver 400 si faltan campos requeridos', async () => { + const res = await request(app).put('/sets-productos').send({ + nombre: 'Set incompleto' + }); + + expect(res.statusCode).toBe(MENSAJES.FORMATO_INVALIDO_DATOS.codigo); + expect(res.body.mensaje).toBe(MENSAJES.FORMATO_INVALIDO_DATOS.mensaje); + expect(res.body.detalles).toMatch(/nombre y lista de productos/i); + }); + + it('debe devolver 200 si la actualización es exitosa', async () => { + repositorio.actualizarSetProductos.mockResolvedValue(); + + const datos = { + idSetProducto: 1, + nombre: 'Set Actualizado', + descripcion: 'Nueva descripción', + activo: true, + productos: [101, 102] + }; + + const res = await request(app).put('/sets-productos').send(datos); + + expect(res.statusCode).toBe(MENSAJES.SET_ACTUALIZADO.codigo); + expect(res.body.mensaje).toBe(MENSAJES.SET_ACTUALIZADO.mensaje); + expect(res.body.datos).toEqual(datos); + expect(repositorio.actualizarSetProductos).toHaveBeenCalledWith(1, datos); + }); + + it('debe devolver 500 si el repositorio lanza un error', async () => { + repositorio.actualizarSetProductos.mockRejectedValue(new Error('Fallo DB')); + + const datos = { + idSetProducto: 2, + nombre: 'Set con error', + descripcion: 'desc', + activo: false, + productos: [] + }; + + const res = await request(app).put('/sets-productos').send(datos); + + expect(res.statusCode).toBe(MENSAJES.ERROR_ACTUALIZAR_SET.codigo); + expect(res.body.mensaje).toBe('Fallo DB'); + }); +}); diff --git a/_tests_/SetsProductos/consultarListaSetsProductos.controller.test.js b/_tests_/SetsProductos/consultarListaSetsProductos.controller.test.js new file mode 100644 index 00000000..3c4d609a --- /dev/null +++ b/_tests_/SetsProductos/consultarListaSetsProductos.controller.test.js @@ -0,0 +1,81 @@ +// Mocks antes de importar el controlador +jest.mock('@altertex/setspro/repos/repositorioConsultarSetsProductos', () => ({ + obtenerSetsProductos: jest.fn(), +})); + +// Importaciones +const controlador = require('@altertex/setspro/ctrl/consultarSetsProductos.controller'); +const repositorio = require('@altertex/setspro/repos/repositorioConsultarSetsProductos'); +const MENSAJES = require('@altertex/util/const/mensajesSetsProductos'); + +describe('Controlador consultarLista (sets de productos)', () => { + let req; + let res; + + beforeEach(() => { + jest.clearAllMocks(); + + req = { + user: { + clienteSeleccionado: '101', + }, + }; + + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + + jest.spyOn(console, 'error').mockImplementation(() => {}); // Silenciar errores + }); + + // Escenario 1: ID de cliente inválido + test('Debe retornar error si el ID del cliente no es válido', async () => { + req.user.clienteSeleccionado = undefined; + + await controlador.consultarLista(req, res); + expect(res.status).toHaveBeenCalledWith(MENSAJES.PARAMETROS_INVALIDOS.codigo); + expect(res.json).toHaveBeenCalledWith({ + mensaje: MENSAJES.PARAMETROS_INVALIDOS.mensaje, + }); + }); + + // Escenario 2: Lista de sets obtenida exitosamente + test('Debe retornar la lista de sets si la consulta es exitosa', async () => { + const mockSets = [ + { + idSet: 1, + nombre: 'Set A', + productos: ['Producto 1', 'Producto 2'], + }, + { + idSet: 2, + nombre: 'Set B', + productos: ['Producto 3'], + }, + ]; + + repositorio.obtenerSetsProductos.mockResolvedValue(mockSets); + + await controlador.consultarLista(req, res); + + expect(repositorio.obtenerSetsProductos).toHaveBeenCalledWith(101); + expect(res.status).toHaveBeenCalledWith(MENSAJES.CONSULTA_EXITOSA.codigo); + expect(res.json).toHaveBeenCalledWith({ + mensaje: MENSAJES.CONSULTA_EXITOSA.mensaje, + setsProductos: mockSets, + }); + }); + + // Escenario 3: Error inesperado en el repositorio + test('Debe manejar errores inesperados del repositorio', async () => { + repositorio.obtenerSetsProductos.mockRejectedValue(new Error('Error')); + + await controlador.consultarLista(req, res); + + expect(res.status).toHaveBeenCalledWith(MENSAJES.ERROR_CONSULTAR_SETS_PRODUCTOS.codigo); + expect(res.json).toHaveBeenCalledWith({ + mensaje: MENSAJES.ERROR_CONSULTAR_SETS_PRODUCTOS.mensaje, + }); + }); +}); diff --git a/jest.config.js b/jest.config.js index f30998ff..9162afb7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -92,6 +92,10 @@ module.exports = { '^@altertex/usu/datos/(.*)$': '/Usuarios/Datos/$1', '^@altertex/usu/(.*)$': '/Usuarios/$1', + // Sets de productos + "^@altertex/setspro/ctrl/(.*)$": "/SetsProductos/Controladores/$1", + "^@altertex/setspro/repos/(.*)$": "/SetsProductos/Datos/Repositorios/$1", + // Generic mapping as fallback '^@altertex/(.*)$': '/$1', From 8f574ebe629538a307d4f11e136551a26aa35610 Mon Sep 17 00:00:00 2001 From: NicoH00d Date: Fri, 6 Jun 2025 23:04:09 -0600 Subject: [PATCH 15/17] fix: arreglar consulta --- Utilidades/Constantes/consultasUsuarios.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Utilidades/Constantes/consultasUsuarios.js b/Utilidades/Constantes/consultasUsuarios.js index bbf590d5..e0a5d56f 100644 --- a/Utilidades/Constantes/consultasUsuarios.js +++ b/Utilidades/Constantes/consultasUsuarios.js @@ -60,6 +60,7 @@ module.exports = { u.genero, u.estatus, r.idRol AS rol, + r.nombre AS nombreRol, uc.idCliente AS idCliente, c.nombreComercial AS nombreCliente FROM usuario u From 277fbfa782e19f3b54c2a88b757d3dc77aa43bba Mon Sep 17 00:00:00 2001 From: NicoH00d Date: Thu, 12 Jun 2025 23:23:37 -0600 Subject: [PATCH 16/17] Manejo de errores con nuevo estandar --- .../actualizarUsuario.controller.js | 41 ++--- .../repositorioActualizarUsuario.js | 152 ++++++++---------- Utilidades/Constantes/consultasUsuarios.js | 5 + Utilidades/Constantes/mensajesUsuarios.js | 4 + 4 files changed, 94 insertions(+), 108 deletions(-) diff --git a/Usuarios/Controladores/actualizarUsuario.controller.js b/Usuarios/Controladores/actualizarUsuario.controller.js index 122a8ee1..e6538745 100644 --- a/Usuarios/Controladores/actualizarUsuario.controller.js +++ b/Usuarios/Controladores/actualizarUsuario.controller.js @@ -1,4 +1,3 @@ -const MENSAJES = require('@altertex/util/const/mensajesUsuarios'); const repositorio = require('@altertex/usu/repos/repositorioActualizarUsuario'); const bcrypt = require('bcryptjs'); @@ -20,35 +19,27 @@ const bcrypt = require('bcryptjs'); * @returns {Promise} Retorna una respuesta JSON indicando éxito o un error. */ exports.actualizarUsuario = async (req, res) => { - let datos; + const cambios = req.body.cambios || req.body; - // Si no hay cambios - if (req.body.id || req.body.idUsuario) { - datos = [req.body]; - } else if (req.body.cambios) { - // Si la información viene en el formato esperado (hay cambios) - datos = Array.isArray(req.body.cambios) ? req.body.cambios : [req.body.cambios]; - const contraseniaEncriptada = await bcrypt.hash(datos[0].contrasenia, 10); - datos[0].contrasenia = contraseniaEncriptada; - } else { - return res - .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) - .json({ mensaje: MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje }); + if (!cambios) { + return res.status(400).json({ mensaje: 'No se enviaron los datos del usuario' }); + } + + const datos = Array.isArray(cambios) ? cambios : [cambios]; + + if (!datos[0].idUsuario) { + return res.status(400).json({ mensaje: 'ID del usuario no proporcionado' }); } - if (!datos || datos.length === 0) { - return res - .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) - .json({ mensaje: MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje }); + if (datos[0].contrasenia) { + const contraseniaEncriptada = await bcrypt.hash(datos[0].contrasenia, 10); + datos[0].contrasenia = contraseniaEncriptada; } + try { await repositorio.actualizarUsuario(datos); - return res - .status(MENSAJES.USUARIO_ACTUALIZADO.codigo) - .json({ mensaje: MENSAJES.USUARIO_ACTUALIZADO.mensaje, datos }); - } catch { - return res - .status(MENSAJES.ERROR_ACTUALIZAR_USUARIO.codigo) - .json({ mensaje: MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje }); + return res.status(200).json({ mensaje: 'Usuario actualizado correctamente' }); + } catch (error) { + return res.status(400).json({ mensaje: error.message }); } }; diff --git a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js index cb7ed582..8100363a 100644 --- a/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js +++ b/Usuarios/Datos/Repositorios/repositorioActualizarUsuario.js @@ -22,97 +22,83 @@ const CONSULTAS_USUARIOS = require('@altertex/util/const/consultasUsuarios'); */ exports.actualizarUsuario = async (datos) => { if (!Array.isArray(datos) || datos.length === 0) { - throw new Error('Sin datos para actualizar.'); + throw new Error(MENSAJES.ERROR_OBTENER_USUARIO.mensaje); } + try { - await Promise.all( - datos.map( - /** - * Actualiza la información de un usuario individualmente. - * @param {object} usuario - Objeto con los datos del usuario a actualizar. - * @returns {Promise} - */ - async (usuario) => { - const { - idUsuario, - nombreCompleto, - correoElectronico, - contrasenia, - numeroTelefono, - direccion, - fechaNacimiento, - idRol, - genero, - estatus, - } = usuario; + for (const usuario of datos) { + const { + idUsuario, + correoElectronico, + nombreCompleto, + contrasenia, + numeroTelefono, + direccion, + fechaNacimiento, + idRol, + genero, + estatus, + cliente, + } = usuario; - const conContrasena = usuario.contrasenia == '' ? false : true; + const resultadoCorreo = await correrQuery( + CONSULTAS_USUARIOS.VALIDAR_CORREO_DUPLICADO_ACTUALIZACION, + [correoElectronico, idUsuario] + ); - console.log('roles', idRol); + if (resultadoCorreo.length > 0) { + throw new Error(MENSAJES.USUARIO_YA_EXISTE.mensaje); + } - // Actualiza datos del usuario - if (conContrasena) { - await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO, [ - nombreCompleto, - correoElectronico, - contrasenia, - numeroTelefono, - direccion, - fechaNacimiento, - genero, - estatus, - idUsuario, - ]); - } else { - await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO_SIN_CONTRASENA, [ - nombreCompleto, - correoElectronico, - numeroTelefono, - direccion, - fechaNacimiento, - genero, - estatus, - idUsuario, - ]); - } + const conContrasena = contrasenia && contrasenia.trim() !== ''; - if (usuario.cliente) { - // Pasar el cliente/clientes a un array - const clientes = Array.isArray(usuario.cliente) ? usuario.cliente : [usuario.cliente]; + if (conContrasena) { + await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO, [ + nombreCompleto, + correoElectronico, + contrasenia, + numeroTelefono, + direccion, + fechaNacimiento, + genero, + estatus, + idUsuario, + ]); + } else { + await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_DATOS_USUARIO_SIN_CONTRASENA, [ + nombreCompleto, + correoElectronico, + numeroTelefono, + direccion, + fechaNacimiento, + genero, + estatus, + idUsuario, + ]); + } - await correrQuery(CONSULTAS_USUARIOS.ELIMINAR_USUARIO_CLIENTE, [idUsuario]).then( - /** - * Función que reasocia los clientes al usuario después de eliminarlos. - * @returns {Promise} - */ - async () => { - try { - for (const cliente of clientes) { - // Asociar cada cliente seleccionado al usuario - if (cliente) { - // Evitar errores si el cliente es undefined o null - await correrQuery(CONSULTAS_USUARIOS.ASOCIAR_USUARIO_A_CLIENTE, [ - idUsuario, - cliente, - ]); - } - } - } catch (error) { - return error; - } - } - ); - } + // Asociar cliente(s) + if (cliente) { + const clientes = Array.isArray(cliente) ? cliente : [cliente]; + + // Eliminar asociaciones anteriores + await correrQuery(CONSULTAS_USUARIOS.ELIMINAR_USUARIO_CLIENTE, [idUsuario]); - const result = await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_ROL_USUARIO, [ - idRol, - idUsuario, - ]); - console.log('Resultado de actualizar rol:', result); + for (const idCliente of clientes) { + if (idCliente) { + await correrQuery(CONSULTAS_USUARIOS.ASOCIAR_USUARIO_A_CLIENTE, [ + idUsuario, + idCliente, + ]); + } } - ) - ); - } catch { - throw new Error(MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje); + } + await correrQuery(CONSULTAS_USUARIOS.ACTUALIZAR_ROL_USUARIO, [idRol, idUsuario]); + } + } catch (error) { + if (error.code === 'ER_TRUNCATED_WRONG_VALUE') { + throw new Error(MENSAJES.ERROR_FECHA_NO_VALIDA.mensaje); + } + throw new Error(error.message || MENSAJES.ERROR_ACTUALIZAR_USUARIO.mensaje); } }; diff --git a/Utilidades/Constantes/consultasUsuarios.js b/Utilidades/Constantes/consultasUsuarios.js index e0a5d56f..e2062013 100644 --- a/Utilidades/Constantes/consultasUsuarios.js +++ b/Utilidades/Constantes/consultasUsuarios.js @@ -220,4 +220,9 @@ module.exports = { WHERE idUsuario IN (?) AND puedeActivar2FA = true; `, + VALIDAR_CORREO_DUPLICADO_ACTUALIZACION: ` + SELECT idUsuario + FROM usuario + WHERE correoElectronico = ? AND idUsuario <> ?; + `, }; diff --git a/Utilidades/Constantes/mensajesUsuarios.js b/Utilidades/Constantes/mensajesUsuarios.js index b6675267..9e01f97d 100644 --- a/Utilidades/Constantes/mensajesUsuarios.js +++ b/Utilidades/Constantes/mensajesUsuarios.js @@ -95,4 +95,8 @@ module.exports = { codigo: 500, mensaje: 'Ocurrió un error al intentar actualizar el usuario.', }, + ERROR_FECHA_NO_VALIDA: { + codigo: 500, + mensaje: 'La fecha de nacimiento proporcionada no es válida.', + }, }; From fd6df3fc58bb54762b55a56d81b7ea91f0c52883 Mon Sep 17 00:00:00 2001 From: NicoH00d Date: Thu, 12 Jun 2025 23:33:30 -0600 Subject: [PATCH 17/17] =?UTF-8?q?Validaci=C3=B3n=20para=20eliminar=20al=20?= =?UTF-8?q?usuario=20logeado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Usuarios/Controladores/eliminarUsuario.controller.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Usuarios/Controladores/eliminarUsuario.controller.js b/Usuarios/Controladores/eliminarUsuario.controller.js index 7078f34a..7bf238c5 100644 --- a/Usuarios/Controladores/eliminarUsuario.controller.js +++ b/Usuarios/Controladores/eliminarUsuario.controller.js @@ -42,6 +42,12 @@ exports.eliminarUsuario = async (req, res) => { } const idsNumericos = idsUsuarios.map(Number); + + if (idsNumericos.includes(idSolicitante)) { + return res.status(403).json({ + mensaje: 'No puedes eliminar tu propio usuario.', + }); + } const [usuariosObjetivo] = await db.query(` SELECT u.idUsuario, r.nombre AS rol