add bot wsp
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
// src/common/decorators/user.decorator.ts
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const User = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user || null;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
ForbiddenException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { UserRole } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class AdminGuard implements CanActivate {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers['authorization'];
|
||||
|
||||
if (!authHeader || !authHeader.toLowerCase().startsWith('bearer ')) {
|
||||
throw new UnauthorizedException('Token no proporcionado');
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7).trim();
|
||||
|
||||
const found = await this.prisma.token.findUnique({
|
||||
where: { token },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
throw new UnauthorizedException('Token inválido');
|
||||
}
|
||||
|
||||
if (new Date() > found.expiresAt) {
|
||||
throw new UnauthorizedException('Token expirado');
|
||||
}
|
||||
|
||||
// Verificar que el usuario sea ADMIN
|
||||
// if (found?.user?.role !== UserRole.ADMIN) {
|
||||
// throw new ForbiddenException('Acceso denegado: se requieren permisos de administrador');
|
||||
// }
|
||||
|
||||
request.user = found.user;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthTokenGuard implements CanActivate {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers['authorization'];
|
||||
|
||||
if (!authHeader || !authHeader.toLowerCase().startsWith('bearer ')) {
|
||||
throw new UnauthorizedException('Token no proporcionado');
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7).trim(); // lo que viene después de "Bearer "
|
||||
|
||||
const found = await this.prisma.token.findUnique({
|
||||
where: { token },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
throw new UnauthorizedException('Token inválido');
|
||||
}
|
||||
|
||||
if (new Date() > found.expiresAt) {
|
||||
throw new UnauthorizedException('Token expirado');
|
||||
}
|
||||
|
||||
// guardamos el usuario en request para que @User() lo pueda leer
|
||||
request.user = found?.user || undefined;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import { Body, Controller, Get, Patch, UseGuards, ValidationPipe, UsePipes, Post } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiProperty } from '@nestjs/swagger';
|
||||
import { SystemConfigService } from './system-config.service';
|
||||
import { AdminGuard } from './guards/admin.guard';
|
||||
import { User } from './decoratos/user.decorator';
|
||||
import { AuthUser } from './types';
|
||||
import { IsOptional, IsInt, Min, Max, IsDateString } from 'class-validator';
|
||||
|
||||
class UpdateSystemConfigDto {
|
||||
@ApiProperty({
|
||||
description: 'Intervalo en minutos para desactivar sesiones vencidas',
|
||||
example: 30,
|
||||
required: false,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
sessionCleanupIntervalMinutes?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Día del mes (1-28) en que se generan las facturas mensuales',
|
||||
example: 1,
|
||||
required: false,
|
||||
minimum: 1,
|
||||
maximum: 28,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(28)
|
||||
invoiceGenerationDayOfMonth?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Tiempo de expiración de los tokens en minutos',
|
||||
example: 30,
|
||||
required: false,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
tokenExpirationMinutes?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Intervalo en minutos para limpiar tokens expirados',
|
||||
example: 60,
|
||||
required: false,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
tokenCleanupIntervalMinutes?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Intervalo en días para actualizar fotos de perfil de WhatsApp',
|
||||
example: 2,
|
||||
required: false,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
profilePictureUpdateIntervalDays?: number;
|
||||
}
|
||||
|
||||
class ForceInvoicesDto {
|
||||
@ApiProperty({
|
||||
description: 'Fecha del mes para el cual generar facturas (formato ISO 8601)',
|
||||
example: '2025-11-01T00:00:00.000Z',
|
||||
required: true,
|
||||
})
|
||||
@IsDateString()
|
||||
targetMonth!: string;
|
||||
}
|
||||
|
||||
@ApiTags('Configuración del Sistema')
|
||||
@Controller('system-config')
|
||||
export class SystemConfigController {
|
||||
constructor(private readonly systemConfigService: SystemConfigService) { }
|
||||
|
||||
@Get()
|
||||
//@UseGuards(AdminGuard)
|
||||
@ApiOperation({
|
||||
summary: 'Obtener configuración actual del sistema',
|
||||
description: 'Retorna todos los parámetros de configuración actuales que controlan los trabajos programados y tokens'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Configuración obtenida exitosamente',
|
||||
schema: {
|
||||
example: {
|
||||
id: 'clxxx123456',
|
||||
sessionCleanupIntervalMinutes: 30,
|
||||
invoiceGenerationDayOfMonth: 1,
|
||||
tokenExpirationMinutes: 30,
|
||||
tokenCleanupIntervalMinutes: 60,
|
||||
profilePictureUpdateIntervalDays: 2,
|
||||
createdAt: '2025-11-10T00:00:00.000Z',
|
||||
updatedAt: '2025-11-10T00:00:00.000Z'
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({ status: 401, description: 'No autorizado - Token no proporcionado o inválido' })
|
||||
@ApiResponse({ status: 403, description: 'Acceso denegado - Se requieren permisos de administrador' })
|
||||
//@ApiBearerAuth()
|
||||
async getConfig() {
|
||||
return this.systemConfigService.getConfig();
|
||||
}
|
||||
|
||||
@Patch()
|
||||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({
|
||||
summary: 'Actualizar configuración del sistema',
|
||||
description: 'Actualiza uno o más parámetros de configuración. Los cambios toman efecto inmediatamente, excepto tokenExpirationMinutes que requiere reinicio.'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Configuración actualizada exitosamente',
|
||||
schema: {
|
||||
example: {
|
||||
id: 'clxxx123456',
|
||||
sessionCleanupIntervalMinutes: 45,
|
||||
invoiceGenerationDayOfMonth: 5,
|
||||
tokenExpirationMinutes: 60,
|
||||
tokenCleanupIntervalMinutes: 120,
|
||||
profilePictureUpdateIntervalDays: 3,
|
||||
createdAt: '2025-11-10T00:00:00.000Z',
|
||||
updatedAt: '2025-11-10T12:30:00.000Z',
|
||||
warnings: [
|
||||
'El cambio en tokenExpirationMinutes requiere reiniciar la aplicación para que JwtModule lo tome en cuenta.'
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({ status: 400, description: 'Datos inválidos - Revisa las validaciones de cada campo' })
|
||||
@ApiResponse({ status: 401, description: 'No autorizado - Token no proporcionado o inválido' })
|
||||
@ApiResponse({ status: 403, description: 'Acceso denegado - Se requieren permisos de administrador' })
|
||||
@ApiBearerAuth()
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async updateConfig(
|
||||
@Body() data: UpdateSystemConfigDto,
|
||||
@User() user: AuthUser,
|
||||
) {
|
||||
console.log('=== CONTROLLER UPDATE CONFIG ===');
|
||||
console.log('Body received:', JSON.stringify(data, null, 2));
|
||||
console.log('Body type:', typeof data);
|
||||
console.log('Body constructor:', data.constructor.name);
|
||||
console.log('User:', JSON.stringify(user, null, 2));
|
||||
console.log('Body keys:', Object.keys(data));
|
||||
console.log('Body values:', Object.values(data));
|
||||
console.log('invoiceGenerationDayOfMonth type:', typeof data.invoiceGenerationDayOfMonth);
|
||||
console.log('invoiceGenerationDayOfMonth value:', data.invoiceGenerationDayOfMonth);
|
||||
console.log('================================');
|
||||
|
||||
const result = await this.systemConfigService.updateConfig(data, user);
|
||||
|
||||
// Advertencia si se cambió tokenExpirationMinutes
|
||||
const warnings = [];
|
||||
if (data.tokenExpirationMinutes !== undefined) {
|
||||
warnings.push('El cambio en tokenExpirationMinutes requiere reiniciar la aplicación para que JwtModule lo tome en cuenta.');
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
...(warnings.length > 0 && { warnings })
|
||||
};
|
||||
}
|
||||
|
||||
@Get('reload')
|
||||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({
|
||||
summary: 'Recargar configuración desde la base de datos',
|
||||
description: 'Fuerza la recarga de la configuración desde la base de datos en memoria. Útil después de cambios manuales en la BD.'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Configuración recargada exitosamente',
|
||||
schema: {
|
||||
example: {
|
||||
message: 'Configuración recargada exitosamente'
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({ status: 401, description: 'No autorizado - Token no proporcionado o inválido' })
|
||||
@ApiResponse({ status: 403, description: 'Acceso denegado - Se requieren permisos de administrador' })
|
||||
@ApiBearerAuth()
|
||||
async reloadConfig() {
|
||||
await this.systemConfigService.reloadConfig();
|
||||
return { message: 'Configuración recargada exitosamente' };
|
||||
}
|
||||
|
||||
@Post('force-invoices')
|
||||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({
|
||||
summary: 'Forzar generación de facturas para un mes específico',
|
||||
description: 'Genera facturas para todas las sesiones activas y sus asistentes en el mes especificado. Solo crea facturas que no existan previamente.'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Facturas generadas exitosamente',
|
||||
schema: {
|
||||
example: {
|
||||
message: 'Facturas generadas exitosamente para 2025-11',
|
||||
totalCreated: 15,
|
||||
details: [
|
||||
{ sessionId: 'xxx', assistantId: 'yyy', invoiceId: 'zzz' }
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({ status: 400, description: 'Datos inválidos - Fecha requerida' })
|
||||
@ApiResponse({ status: 401, description: 'No autorizado' })
|
||||
@ApiResponse({ status: 403, description: 'Acceso denegado' })
|
||||
@ApiBearerAuth()
|
||||
async forceGenerateInvoices(
|
||||
@Body() data: ForceInvoicesDto,
|
||||
@User() user: AuthUser,
|
||||
) {
|
||||
const result = await this.systemConfigService.forceGenerateMonthlyInvoices(
|
||||
new Date(data.targetMonth),
|
||||
user
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { SystemConfigService } from './system-config.service';
|
||||
import { SystemConfigController } from './system-config.controller';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { InvoicesModule } from '~/invoices/invoices.module';
|
||||
import { UserModule } from '~/user/user.module';
|
||||
import { AuthModule } from '~/auth/auth.module';
|
||||
|
||||
@Module({
|
||||
controllers: [SystemConfigController],
|
||||
providers: [SystemConfigService],
|
||||
imports: [
|
||||
PrismaModule,
|
||||
forwardRef(() => InvoicesModule),
|
||||
forwardRef(() => UserModule),
|
||||
forwardRef(() => AuthModule),
|
||||
],
|
||||
exports: [SystemConfigService],
|
||||
})
|
||||
export class SystemConfigModule { }
|
||||
@@ -0,0 +1,295 @@
|
||||
import { forwardRef, Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaService } from '~/prisma/prisma.service';
|
||||
import { AuthUser } from './types';
|
||||
import { InvoicesService } from '~/invoices/invoices.service';
|
||||
import dayjs from 'dayjs';
|
||||
import { UserService } from '~/user/user.service';
|
||||
import { UpsertUserDto } from '~/user/user.dto';
|
||||
import { AuthService } from '~/auth/auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class SystemConfigService implements OnModuleInit {
|
||||
private readonly logger = new Logger(SystemConfigService.name);
|
||||
private config: {
|
||||
sessionCleanupIntervalMinutes: number;
|
||||
invoiceGenerationDayOfMonth: number;
|
||||
tokenExpirationMinutes: number;
|
||||
tokenCleanupIntervalMinutes: number;
|
||||
profilePictureUpdateIntervalDays: number;
|
||||
} | null = null;
|
||||
|
||||
private firstUrl: string | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@Inject(forwardRef(() => InvoicesService))
|
||||
private readonly invoicesService: InvoicesService,
|
||||
private readonly userService: UserService,
|
||||
@Inject(forwardRef(() => AuthService))
|
||||
private readonly authService: AuthService,
|
||||
|
||||
) { }
|
||||
|
||||
async onModuleInit() {
|
||||
await this.loadConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga la configuración desde la base de datos.
|
||||
* Si no existe, crea una configuración por defecto.
|
||||
*/
|
||||
private async loadConfig() {
|
||||
try {
|
||||
let config = await this.prisma.systemConfig.findFirst();
|
||||
|
||||
if (!config) {
|
||||
//NOTE cREO EL PRIMER USUARIO DEL SISTEMA
|
||||
|
||||
const dataUser: UpsertUserDto = {
|
||||
name: 'ADMIN',
|
||||
role: 'ADMIN',
|
||||
customId: ''
|
||||
}
|
||||
|
||||
const user = await this.userService.upsert(dataUser)
|
||||
|
||||
const token = await this.authService.generarToken(user.id, undefined, 1440); // Token válido por 24 horas
|
||||
|
||||
this.logger.log(`Primer usuario ADMIN creado. token para iniciar sesión: ${token}`);
|
||||
|
||||
this.firstUrl = `${process.env.URL_FRONT}?t=${token}`;
|
||||
|
||||
this.logger.log('No se encontró configuración del sistema, creando valores por defecto...');
|
||||
config = await this.prisma.systemConfig.create({
|
||||
data: {
|
||||
sessionCleanupIntervalMinutes: 30,
|
||||
invoiceGenerationDayOfMonth: 1,
|
||||
tokenExpirationMinutes: 30,
|
||||
tokenCleanupIntervalMinutes: 60,
|
||||
profilePictureUpdateIntervalDays: 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.config = {
|
||||
sessionCleanupIntervalMinutes: config.sessionCleanupIntervalMinutes,
|
||||
invoiceGenerationDayOfMonth: config.invoiceGenerationDayOfMonth,
|
||||
tokenExpirationMinutes: config.tokenExpirationMinutes,
|
||||
tokenCleanupIntervalMinutes: config.tokenCleanupIntervalMinutes,
|
||||
profilePictureUpdateIntervalDays: config.profilePictureUpdateIntervalDays,
|
||||
};
|
||||
|
||||
this.logger.log('Configuración del sistema cargada exitosamente');
|
||||
} catch (err: any) {
|
||||
this.logger.error('Error cargando configuración del sistema', err?.stack || err);
|
||||
// Valores por defecto en caso de error
|
||||
this.config = {
|
||||
sessionCleanupIntervalMinutes: 30,
|
||||
invoiceGenerationDayOfMonth: 1,
|
||||
tokenExpirationMinutes: 30,
|
||||
tokenCleanupIntervalMinutes: 60,
|
||||
profilePictureUpdateIntervalDays: 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recarga la configuración desde la base de datos
|
||||
*/
|
||||
async reloadConfig() {
|
||||
await this.loadConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el intervalo de limpieza de sesiones en minutos
|
||||
*/
|
||||
getSessionCleanupInterval(): number {
|
||||
return this.config?.sessionCleanupIntervalMinutes ?? 30;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el día del mes para generar facturas (1-28)
|
||||
*/
|
||||
getInvoiceGenerationDay(): number {
|
||||
return this.config?.invoiceGenerationDayOfMonth ?? 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el tiempo de expiración de tokens en minutos
|
||||
*/
|
||||
getTokenExpirationMinutes(): number {
|
||||
return this.config?.tokenExpirationMinutes ?? 30;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el intervalo de limpieza de tokens en minutos
|
||||
*/
|
||||
getTokenCleanupInterval(): number {
|
||||
return this.config?.tokenCleanupIntervalMinutes ?? 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el intervalo de actualización de fotos de perfil en días
|
||||
*/
|
||||
getProfilePictureUpdateInterval(): number {
|
||||
return this.config?.profilePictureUpdateIntervalDays ?? 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza la configuración del sistema
|
||||
*/
|
||||
async updateConfig(data: {
|
||||
sessionCleanupIntervalMinutes?: number;
|
||||
invoiceGenerationDayOfMonth?: number;
|
||||
tokenExpirationMinutes?: number;
|
||||
tokenCleanupIntervalMinutes?: number;
|
||||
profilePictureUpdateIntervalDays?: number;
|
||||
}, actionBy?: AuthUser) {
|
||||
const actionByInfo = actionBy ? `${actionBy.name || actionBy.id} (${actionBy.role})` : 'Unknown';
|
||||
this.logger.log(`Update system config request - data: ${JSON.stringify(data)}, actionBy: ${actionByInfo}`);
|
||||
|
||||
try {
|
||||
// Validaciones
|
||||
if (data.invoiceGenerationDayOfMonth !== undefined) {
|
||||
if (data.invoiceGenerationDayOfMonth < 1 || data.invoiceGenerationDayOfMonth > 28) {
|
||||
this.logger.warn(`Invalid invoiceGenerationDayOfMonth: ${data.invoiceGenerationDayOfMonth}, attemptedBy: ${actionByInfo}`);
|
||||
throw new Error('El día de generación de facturas debe estar entre 1 y 28');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.sessionCleanupIntervalMinutes !== undefined && data.sessionCleanupIntervalMinutes < 1) {
|
||||
this.logger.warn(`Invalid sessionCleanupIntervalMinutes: ${data.sessionCleanupIntervalMinutes}, attemptedBy: ${actionByInfo}`);
|
||||
throw new Error('El intervalo de limpieza de sesiones debe ser mayor a 0');
|
||||
}
|
||||
|
||||
if (data.tokenExpirationMinutes !== undefined && data.tokenExpirationMinutes < 1) {
|
||||
this.logger.warn(`Invalid tokenExpirationMinutes: ${data.tokenExpirationMinutes}, attemptedBy: ${actionByInfo}`);
|
||||
throw new Error('El tiempo de expiración de tokens debe ser mayor a 0');
|
||||
}
|
||||
|
||||
if (data.tokenCleanupIntervalMinutes !== undefined && data.tokenCleanupIntervalMinutes < 1) {
|
||||
this.logger.warn(`Invalid tokenCleanupIntervalMinutes: ${data.tokenCleanupIntervalMinutes}, attemptedBy: ${actionByInfo}`);
|
||||
throw new Error('El intervalo de limpieza de tokens debe ser mayor a 0');
|
||||
}
|
||||
|
||||
if (data.profilePictureUpdateIntervalDays !== undefined && data.profilePictureUpdateIntervalDays < 1) {
|
||||
this.logger.warn(`Invalid profilePictureUpdateIntervalDays: ${data.profilePictureUpdateIntervalDays}, attemptedBy: ${actionByInfo}`);
|
||||
throw new Error('El intervalo de actualización de fotos debe ser mayor a 0');
|
||||
}
|
||||
|
||||
this.logger.log(`Validations passed, searching for existing config - actionBy: ${actionByInfo}`);
|
||||
|
||||
// Buscar configuración existente
|
||||
const existing = await this.prisma.systemConfig.findFirst();
|
||||
|
||||
if (!existing) {
|
||||
this.logger.error(`System config not found in database - attemptedBy: ${actionByInfo}`);
|
||||
throw new Error('No se encontró configuración del sistema');
|
||||
}
|
||||
|
||||
this.logger.log(`Found existing config - id: ${existing.id}, updating with data: ${JSON.stringify(data)}, by: ${actionByInfo}`);
|
||||
this.logger.log(`Current values in DB - sessionCleanup: ${existing.sessionCleanupIntervalMinutes}, invoiceDay: ${existing.invoiceGenerationDayOfMonth}, tokenExp: ${existing.tokenExpirationMinutes}, tokenCleanup: ${existing.tokenCleanupIntervalMinutes}`);
|
||||
|
||||
console.log('=== SERVICE BEFORE PRISMA UPDATE ===');
|
||||
console.log('Existing ID:', existing.id);
|
||||
console.log('Data to update:', JSON.stringify(data, null, 2));
|
||||
console.log('Data keys:', Object.keys(data));
|
||||
console.log('Data values:', Object.values(data));
|
||||
console.log('Data object type:', typeof data);
|
||||
console.log('Is data empty?', Object.keys(data).length === 0);
|
||||
|
||||
// Construir el objeto de actualización explícitamente
|
||||
const updateData: any = {};
|
||||
if (data.sessionCleanupIntervalMinutes !== undefined) {
|
||||
updateData.sessionCleanupIntervalMinutes = data.sessionCleanupIntervalMinutes;
|
||||
console.log('Adding sessionCleanupIntervalMinutes:', data.sessionCleanupIntervalMinutes);
|
||||
}
|
||||
if (data.invoiceGenerationDayOfMonth !== undefined) {
|
||||
updateData.invoiceGenerationDayOfMonth = data.invoiceGenerationDayOfMonth;
|
||||
console.log('Adding invoiceGenerationDayOfMonth:', data.invoiceGenerationDayOfMonth);
|
||||
}
|
||||
if (data.tokenExpirationMinutes !== undefined) {
|
||||
updateData.tokenExpirationMinutes = data.tokenExpirationMinutes;
|
||||
console.log('Adding tokenExpirationMinutes:', data.tokenExpirationMinutes);
|
||||
}
|
||||
if (data.tokenCleanupIntervalMinutes !== undefined) {
|
||||
updateData.tokenCleanupIntervalMinutes = data.tokenCleanupIntervalMinutes;
|
||||
console.log('Adding tokenCleanupIntervalMinutes:', data.tokenCleanupIntervalMinutes);
|
||||
}
|
||||
if (data.profilePictureUpdateIntervalDays !== undefined) {
|
||||
updateData.profilePictureUpdateIntervalDays = data.profilePictureUpdateIntervalDays;
|
||||
console.log('Adding profilePictureUpdateIntervalDays:', data.profilePictureUpdateIntervalDays);
|
||||
}
|
||||
|
||||
console.log('Final updateData object:', JSON.stringify(updateData, null, 2));
|
||||
console.log('UpdateData keys:', Object.keys(updateData));
|
||||
console.log('====================================');
|
||||
|
||||
// Actualizar configuración
|
||||
const updated = await this.prisma.systemConfig.update({
|
||||
where: { id: existing.id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
console.log('=== SERVICE AFTER PRISMA UPDATE ===');
|
||||
console.log('Updated result:', JSON.stringify(updated, null, 2));
|
||||
console.log('===================================');
|
||||
|
||||
this.logger.log(`Database updated successfully - configId: ${existing.id}, newValues: ${JSON.stringify(updated)}, by: ${actionByInfo}`);
|
||||
|
||||
// Recargar configuración en memoria
|
||||
await this.loadConfig();
|
||||
|
||||
this.logger.log(`Configuration reloaded in memory - by: ${actionByInfo}`);
|
||||
this.logger.log(`System config update completed successfully - by: ${actionByInfo}`);
|
||||
|
||||
return updated;
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error updating system config by ${actionByInfo}: ${err.message}`, err?.stack || err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene toda la configuración
|
||||
*/
|
||||
async getConfig() {
|
||||
return this.prisma.systemConfig.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuerza la generación de facturas para un mes específico
|
||||
*/
|
||||
async forceGenerateMonthlyInvoices(targetMonth: Date, actionBy?: AuthUser) {
|
||||
const actionByInfo = actionBy ? `${actionBy.name || actionBy.id} (${actionBy.role})` : 'Unknown';
|
||||
const monthStr = dayjs(targetMonth).format('YYYY-MM');
|
||||
|
||||
this.logger.log(`Forzando generación de facturas para ${monthStr} - actionBy: ${actionByInfo}`);
|
||||
|
||||
try {
|
||||
const invoices = await this.invoicesService.createMonthlyInvoicesForActiveAssistants(targetMonth);
|
||||
|
||||
this.logger.log(`Facturas generadas exitosamente para ${monthStr} - total: ${invoices.length}, by: ${actionByInfo}`);
|
||||
|
||||
return {
|
||||
message: `Facturas generadas exitosamente para ${monthStr}`,
|
||||
totalCreated: invoices.length,
|
||||
details: invoices.map(inv => ({
|
||||
sessionId: inv.sessionId,
|
||||
userId: inv.userId,
|
||||
invoiceId: inv.id,
|
||||
amount: inv.amount
|
||||
}))
|
||||
};
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error forzando generación de facturas para ${monthStr} by ${actionByInfo}: ${err.message}`, err?.stack || err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
getFirstUrl(): string | null {
|
||||
const url = this.firstUrl;
|
||||
this.firstUrl = null; // consumir: solo válido una vez
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// common/types.ts
|
||||
import { UserRole } from '@prisma/client';
|
||||
|
||||
export type AuthUser = {
|
||||
id: string;
|
||||
role: UserRole;
|
||||
name?: string;
|
||||
customId?: string;
|
||||
};
|
||||
|
||||
// typings/express.d.ts (asegurate que el tsconfig incluya este archivo)
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: AuthUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user