5 Commits

Author SHA1 Message Date
edreitega b64fb4690d Merge pull request 'UX-013: endpoint PUT /carrito/mover-modulo para superadmin' (#6) from feature/UX-013-mover-carrito-modulo into dev
Reviewed-on: #6
2026-08-04 00:55:12 +00:00
Edrei Téllez 4e9ddb6bce UX-013: endpoint PUT /carrito/mover-modulo para superadmin
Agrega MoverModuloCarritoDto y CarritoService.moverModulo() como
método dedicado (no toca update() existente) que valida que el
módulo destino pertenezca a la misma institución del carrito.
Expone PUT /carrito/mover-modulo protegido con validarSuperAdmin.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 14:27:46 -06:00
edreitega f86c4d4f2a Merge pull request 'SPI-007: optimizar count de préstamos activos con vista ligera' (#4) from feature/SPI-007-prestamos-activos-timeout into dev
Reviewed-on: #4
2026-06-29 17:31:33 +00:00
Edrei Téllez ffb2a1d64a SPI-007: optimizar count de préstamos activos con vista ligera
Separar find (full view) del count (informacion_prestamo_view) en el path
activo=true con paginación para evitar timeout por COUNT sobre 8 JOINs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 11:06:27 -06:00
edreitega 8e74802de7 Merge pull request 'SEC-001: migrar HcaptchaGuard a TurnstileGuard con siteverify Cloudflare.' (#2) from feature/SEC-001-turnstile into dev
Reviewed-on: #2
2026-06-23 10:37:03 -06:00
4 changed files with 91 additions and 8 deletions
+27
View File
@@ -23,6 +23,7 @@ import { ValidarUsuarioService } from '../validar-usuario/validar-usuario.servic
import { CarritoDto } from './dto/input/carrito.dto'; import { CarritoDto } from './dto/input/carrito.dto';
import { CarritosDto } from './dto/input/carritos.dto'; import { CarritosDto } from './dto/input/carritos.dto';
import { CreateCarritoDto } from './dto/input/create.dto'; import { CreateCarritoDto } from './dto/input/create.dto';
import { MoverModuloCarritoDto } from './dto/input/mover-modulo.dto';
import { UpdateCarritoDto } from './dto/input/update.dto'; import { UpdateCarritoDto } from './dto/input/update.dto';
import { MessageOutputDto } from '../dto/output/message.dto'; import { MessageOutputDto } from '../dto/output/message.dto';
import { CarritoOutputDto } from './dto/output/carrito.dto'; import { CarritoOutputDto } from './dto/output/carrito.dto';
@@ -183,4 +184,30 @@ export class CarritoController {
body.numero_equipos, body.numero_equipos,
); );
} }
@Serealize(MessageOutputDto)
@Put('mover-modulo')
@UseGuards(AuthGuard('jwt'))
@ApiOperation({
description:
'Mover un carrito a otro módulo de la misma institución (superadmin).',
})
@ApiBearerAuth('jwt')
@ApiBody({
description: 'Variables que necesita el endpoint.',
examples: {
ejemplo: {
value: {
id_carrito: 1,
id_modulo: 2,
},
},
},
})
moverModulo(@Request() req, @Body() body: MoverModuloCarritoDto) {
const superAdmin: Operador = req.user.operador;
this.validarUsuarioService.validarSuperAdmin(superAdmin);
return this.carritoService.moverModulo(body.id_carrito, body.id_modulo);
}
} }
+27
View File
@@ -1,4 +1,5 @@
import { import {
ConflictException,
ForbiddenException, ForbiddenException,
forwardRef, forwardRef,
Inject, Inject,
@@ -243,6 +244,32 @@ export class CarritoService {
}); });
} }
async moverModulo(
id_carrito: number,
id_modulo: number,
): Promise<{ message: string }> {
const carrito = await this.findById(id_carrito);
const modulo = await this.moduloService.findById(id_modulo);
// Validamos que el módulo destino no sea el módulo actual del carrito
if (carrito.modulo.id_modulo === modulo.id_modulo)
throw new ConflictException('Este carrito ya pertenece a este módulo.');
// Validamos que el módulo destino pertenezca a la misma institución del carrito
if (
carrito.modulo.institucion.id_institucion !=
modulo.institucion.id_institucion
)
throw new ForbiddenException(
'No puedes mover este carrito a un módulo que no pertenece a la misma institución.',
);
carrito.modulo = modulo;
return this.repository
.save(carrito)
.then((_) => ({
message: 'Se movió correctamente el carrito de módulo.',
}));
}
private viewToCarrito(infoCarrito: InformacionCarritoView): Carrito { private viewToCarrito(infoCarrito: InformacionCarritoView): Carrito {
if (!infoCarrito) return null; if (!infoCarrito) return null;
return this.repository.create({ return this.repository.create({
@@ -0,0 +1,9 @@
import { IsInt } from 'class-validator';
export class MoverModuloCarritoDto {
@IsInt()
id_carrito: number;
@IsInt()
id_modulo: number;
}
+28 -8
View File
@@ -450,15 +450,35 @@ export class PrestamoService {
if (filtros.pagina) { if (filtros.pagina) {
options.take = 25; options.take = 25;
options.skip = (parseInt(filtros.pagina) - 1) * 25; options.skip = (parseInt(filtros.pagina) - 1) * 25;
return this.fullInformacionPrestamoView
.findAndCount(options)
.then((infoPrestamos) => {
const prestamos: Prestamo[] = [];
for (let i = 0; i < infoPrestamos[0].length; i++) const activoTrue =
prestamos.push(this.fullViewToPrestamo(infoPrestamos[0][i])); (typeof filtros.activo === 'boolean' && filtros.activo) ||
return [prestamos, infoPrestamos[1]]; (typeof filtros.activo === 'string' && filtros.activo === 'true');
}); const canUseLightCount =
activoTrue && !filtros.usuario && !filtros.id_tipo_usuario;
if (canUseLightCount) {
const { usuario: _u, id_tipo_usuario: _tu, ...countWhere } =
busqueda;
const [rows, total] = await Promise.all([
this.fullInformacionPrestamoView.find(options),
this.informacionPrestamoView.count({
where: countWhere as FindOptionsWhere<InformacionPrestamoView>,
}),
]);
const prestamos = rows.map((row) => this.fullViewToPrestamo(row));
return [prestamos, total];
}
const infoPrestamos = await this.fullInformacionPrestamoView.findAndCount(
options,
);
const prestamos: Prestamo[] = [];
for (let i = 0; i < infoPrestamos[0].length; i++)
prestamos.push(this.fullViewToPrestamo(infoPrestamos[0][i]));
return [prestamos, infoPrestamos[1]];
} }
options.order = { id_institucion: 'ASC', id_prestamo: 'ASC' }; options.order = { id_institucion: 'ASC', id_prestamo: 'ASC' };
return this.fullInformacionPrestamoView.find(options); return this.fullInformacionPrestamoView.find(options);