This commit is contained in:
2022-08-15 13:19:28 -05:00
32 changed files with 628 additions and 365 deletions
+9 -5
View File
@@ -10,8 +10,10 @@ import { AuthModule } from './auth/auth.module';
import { BcryptModule } from './bcrypt/bcrypt.module';
import { CarreraProgramaModule } from './carrera-programa/carrera-programa.module';
import { CarritoModule } from './carrito/carrito.module';
import { CarritoMotivoModule } from './carrito-motivo/carrito-motivo.module';
import { CronModule } from './cron/cron.module';
import { EquipoModule } from './equipo/equipo.module';
import { EquipoMotivoModule } from './equipo-motivo/equipo-motivo.module';
import { EquipoProgramaModule } from './equipo-programa/equipo-programa.module';
import { EquipoTipoEntradaModule } from './equipo-tipo-entrada/equipo-tipo-entrada.module';
import { HoraExcepcionModule } from './hora-excepcion/hora-excepcion.module';
@@ -26,7 +28,6 @@ import { InstitucionUsuarioModule } from './institucion-usuario/institucion-usua
import { MarcaModule } from './marca/marca.module';
import { ModeloModule } from './modelo/modelo.module';
import { ModuloModule } from './modulo/modulo.module';
import { MotivoModule } from './motivo/motivo.module';
import { MultaModule } from './multa/multa.module';
import { NodemailerModule } from './nodemailer/nodemailer.module';
import { OperadorModule } from './operador/operador.module';
@@ -40,8 +41,10 @@ import { UsuarioModule } from './usuario/usuario.module';
import { Carrera } from './institucion-carrera/entity/carrera.entity';
import { CarreraPrograma } from './carrera-programa/entity/carrera-programa.entity';
import { Carrito } from './carrito/entity/carrito.entity';
import { CarritoMotivo } from './carrito-motivo/entity/carrito-motivo.entity';
import { Dia } from './institucion-dia/entity/dia.entity';
import { Equipo } from './equipo/entity/equipo.entity';
import { EquipoMotivo } from './equipo-motivo/entity/equipo-motivo.entity';
import { EquipoPrograma } from './equipo-programa/entity/equipo-programa.entity';
import { EquipoTipoEntrada } from './equipo-tipo-entrada/entity/equipo-tipo-entrada.entity';
import { HoraExcepcion } from './hora-excepcion/entity/hora-excepcion.entity';
@@ -57,7 +60,6 @@ import { InstitucionUsuario } from './institucion-usuario/entity/institucion-usu
import { Marca } from './marca/entity/marca.entity';
import { Modelo } from './modelo/entity/modelo.entity';
import { Modulo } from './modulo/entity/modulo.entity';
import { Motivo } from './motivo/entity/motivo.entity';
import { Multa } from './multa/entity/multa.entity';
import { Nivel } from './institucion-carrera/entity/nivel.entity';
import { Operador } from './operador/entity/operador.entity';
@@ -93,13 +95,15 @@ import { Usuario } from './usuario/entity/usuario.entity';
username: configService.get<string>('DB_USER'),
password: configService.get<string>('DB_PASSWORD'),
port: parseInt(configService.get<string>('DB_PORT')),
synchronize: false,
synchronize: configService.get<string>('STATE') !== 'produccion',
entities: [
Carrera,
CarreraPrograma,
Carrito,
CarritoMotivo,
Dia,
Equipo,
EquipoMotivo,
EquipoPrograma,
EquipoTipoEntrada,
HoraExcepcion,
@@ -115,7 +119,6 @@ import { Usuario } from './usuario/entity/usuario.entity';
Marca,
Modelo,
Modulo,
Motivo,
Multa,
Nivel,
Operador,
@@ -134,6 +137,7 @@ import { Usuario } from './usuario/entity/usuario.entity';
BcryptModule,
CarreraProgramaModule,
CarritoModule,
CarritoMotivoModule,
CronModule,
EquipoProgramaModule,
EquipoTipoEntradaModule,
@@ -150,7 +154,7 @@ import { Usuario } from './usuario/entity/usuario.entity';
MarcaModule,
ModeloModule,
ModuloModule,
MotivoModule,
EquipoMotivoModule,
MultaModule,
NodemailerModule,
OperadorModule,
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CarritoMotivoController } from './carrito-motivo.controller';
describe('CarritoMotivoController', () => {
let controller: CarritoMotivoController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CarritoMotivoController],
}).compile();
controller = module.get<CarritoMotivoController>(CarritoMotivoController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
@@ -0,0 +1,64 @@
import {
Controller,
ForbiddenException,
Get,
Query,
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import {
ApiBearerAuth,
ApiOperation,
ApiQuery,
ApiTags,
} from '@nestjs/swagger';
import { Serealize } from '../interceptors/serialize.interceptor';
import { CaritoMotivoService } from './carrito-motivo.service';
import { Operador } from '../operador/entity/operador.entity';
import { GetDto } from './dto/input/get.dto';
import { CarritoMotivoOutputDto } from './dto/output/carrito-motivo.dto';
@Controller('carrito-motivo')
@ApiTags('carrito-motivo')
export class CarritoMotivoController {
constructor(private carritoMotivoService: CaritoMotivoService) {}
@Serealize(CarritoMotivoOutputDto)
@Get()
@UseGuards(AuthGuard('jwt'))
@ApiOperation({
description:
'Endpoint que retorna 25 cambios de status que ha tenido un carrito dependiendo de la página en la que se encuentra el alumno.',
})
@ApiBearerAuth('jwt')
@ApiQuery({
description: 'Id del carrito.',
name: 'id_carrito',
type: 'string',
})
@ApiQuery({
description: 'Página en la que se encuentra el usuario.',
name: 'pagina',
type: 'string',
})
get(@Request() req, @Query() query: GetDto) {
const operador: Operador = req.user.operador;
if (
!operador ||
(operador.tipoUsuario.id_tipo_usuario != 3 &&
operador.tipoUsuario.id_tipo_usuario != 4)
)
throw new ForbiddenException(
'No tienes los permisos necesarios para acceder a esta información.',
);
return this.carritoMotivoService.findAllByIdCarrito(
parseInt(query.id_carrito),
parseInt(query.pagina),
);
}
// @Get('reporte')
// reporte() {}
}
@@ -0,0 +1,23 @@
import { forwardRef, Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CarritoMotivoController } from './carrito-motivo.controller';
import { CaritoMotivoService } from './carrito-motivo.service';
import { CarritoMotivo } from './entity/carrito-motivo.entity';
import { CarritoModule } from '../carrito/carrito.module';
import { OperadorModule } from '../operador/operador.module';
import { StatusModule } from '../status/status.module';
@Module({
imports: [
forwardRef(() => CarritoModule),
OperadorModule,
StatusModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
TypeOrmModule.forFeature([CarritoMotivo]),
],
controllers: [CarritoMotivoController],
providers: [CaritoMotivoService],
exports: [CaritoMotivoService],
})
export class CarritoMotivoModule {}
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CaritoMotivoService } from './carrito-motivo.service';
describe('CaritoMotivoService', () => {
let service: CaritoMotivoService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CaritoMotivoService],
}).compile();
service = module.get<CaritoMotivoService>(CaritoMotivoService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
@@ -0,0 +1,49 @@
import * as moment from 'moment';
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Carrito } from 'src/carrito/entity/carrito.entity';
import { CarritoMotivo } from './entity/carrito-motivo.entity';
import { Operador } from '../operador/entity/operador.entity';
import { CarritoService } from '../carrito/carrito.service';
import { OperadorService } from '../operador/operador.service';
@Injectable()
export class CaritoMotivoService {
constructor(
@InjectRepository(CarritoMotivo)
private repository: Repository<CarritoMotivo>,
@Inject(forwardRef(() => CarritoService))
private carritoService: CarritoService,
private operadorService: OperadorService,
) {}
async create(
carrito: Carrito,
operador: Operador,
status: boolean,
motivo: string,
) {
const ahora = moment();
return this.repository.save(
this.repository.create({
carrito,
fecha_creacion: ahora.toDate(),
motivo,
operador,
status,
}),
);
}
findAllByIdCarrito(id_carrito: number, pagina: number) {
return this.carritoService.findById(id_carrito).then((carrito) =>
this.repository.findAndCount({
where: { carrito },
skip: (pagina - 1) * 25,
take: 25,
}),
);
}
}
+9
View File
@@ -0,0 +1,9 @@
import { IsNumberString } from 'class-validator';
export class GetDto {
@IsNumberString()
id_carrito: string;
@IsNumberString()
pagina: string;
}
@@ -0,0 +1,20 @@
import { Expose, Type } from 'class-transformer';
import { OperadorMinOutputDto } from '../../../operador/dto/output/operador-min.dto';
export class CarritoMotivoOutputDto {
@Expose()
id_motivo;
@Expose()
fecha_creacion;
@Expose()
motivo;
@Expose()
status;
@Expose()
@Type(() => OperadorMinOutputDto)
operador;
}
@@ -0,0 +1,32 @@
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Carrito } from '../../carrito/entity/carrito.entity';
import { Operador } from '../../operador/entity/operador.entity';
@Entity()
export class CarritoMotivo {
@PrimaryGeneratedColumn()
id_motivo: number;
@Column({ type: Date, nullable: false })
fecha_creacion: Date;
@Column({ type: String, nullable: false, length: 250 })
motivo: string;
@Column({ type: Boolean, nullable: false })
status: boolean;
@ManyToOne(() => Carrito, (carrito) => carrito.motivos)
@JoinColumn({ name: 'id_carrito' })
carrito: Carrito;
@ManyToOne(() => Operador, (operador) => operador.motivos, { eager: true })
@JoinColumn({ name: 'id_operador' })
operador: Operador;
}
+3 -1
View File
@@ -1,9 +1,10 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CarritoController } from './carrito.controller';
import { CarritoService } from './carrito.service';
import { Carrito } from './entity/carrito.entity';
import { CarritoMotivoModule } from '../carrito-motivo/carrito-motivo.module';
import { InstitucionModule } from '../institucion/institucion.module';
import { InstitucionTipoCarritoModule } from '../institucion-tipo-carrito/institucion-tipo-carrito.module';
import { MarcaModule } from '../marca/marca.module';
@@ -12,6 +13,7 @@ import { ModuloModule } from '../modulo/modulo.module';
@Module({
imports: [
forwardRef(() => CarritoMotivoModule),
InstitucionModule,
InstitucionTipoCarritoModule,
MarcaModule,
+1 -1
View File
@@ -1,4 +1,4 @@
import { IsBoolean, IsInt, IsOptional, IsString } from 'class-validator';
import { IsBoolean, IsInt, IsOptional } from 'class-validator';
export class UpdateCarritoDto {
@IsInt()
+4
View File
@@ -6,6 +6,7 @@ import {
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { CarritoMotivo } from '../../carrito-motivo/entity/carrito-motivo.entity';
import { Equipo } from '../../equipo/entity/equipo.entity';
import { Marca } from '../../marca/entity/marca.entity';
import { Modelo } from '../../modelo/entity/modelo.entity';
@@ -41,6 +42,9 @@ export class Carrito {
@JoinColumn({ name: 'id_tipo_carrito' })
tipoCarrito: TipoCarrito;
@OneToMany(() => CarritoMotivo, (motivo) => motivo.carrito)
motivos: CarritoMotivo[];
@OneToMany(() => Equipo, (equipo) => equipo.carrito)
equipos: Equipo[];
}
@@ -2,7 +2,7 @@ import { Expose, Type } from 'class-transformer';
import { OperadorMinOutputDto } from '../../../operador/dto/output/operador-min.dto';
import { StatusOutputDto } from '../../../status/dto/output/status.dto';
export class MotivoOutputDto {
export class EquipoMotivoOutputDto {
@Expose()
id_motivo;
@@ -10,7 +10,7 @@ import { Operador } from '../../operador/entity/operador.entity';
import { Status } from '../../status/entity/status.entity';
@Entity()
export class Motivo {
export class EquipoMotivo {
@PrimaryGeneratedColumn()
id_motivo: number;
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EquipoMotivoController } from './equipo-motivo.controller';
describe('EquipoMotivoController', () => {
let controller: EquipoMotivoController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [EquipoMotivoController],
}).compile();
controller = module.get<EquipoMotivoController>(EquipoMotivoController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
@@ -14,17 +14,17 @@ import {
ApiTags,
} from '@nestjs/swagger';
import { Serealize } from '../interceptors/serialize.interceptor';
import { MotivoService } from './motivo.service';
import { EquipoMotivoService } from './equipo-motivo.service';
import { Operador } from 'src/operador/entity/operador.entity';
import { IdEquipoPaginaDto } from '../dto/id-equipo-pagina.dto';
import { MotivoOutputDto } from './dto/output/motivo.dto';
import { EquipoMotivoOutputDto } from './dto/output/equipo-motivo.dto';
@Controller('motivo')
@ApiTags('motivo')
export class MotivoController {
constructor(private motivoService: MotivoService) {}
@Controller('equipo-motivo')
@ApiTags('equipo-motivo')
export class EquipoMotivoController {
constructor(private equipoMotivoService: EquipoMotivoService) {}
@Serealize(MotivoOutputDto)
@Serealize(EquipoMotivoOutputDto)
@Get()
@UseGuards(AuthGuard('jwt'))
@ApiOperation({
@@ -53,7 +53,7 @@ export class MotivoController {
throw new ForbiddenException(
'No tienes los permisos necesarios para acceder a esta información.',
);
return this.motivoService.findAllByIdEquipo(
return this.equipoMotivoService.findAllByIdEquipo(
parseInt(query.id_equipo),
parseInt(query.pagina),
);
@@ -1,9 +1,9 @@
import { forwardRef, Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MotivoController } from './motivo.controller';
import { MotivoService } from './motivo.service';
import { Motivo } from './entity/motivo.entity';
import { EquipoMotivoController } from './equipo-motivo.controller';
import { EquipoMotivoService } from './equipo-motivo.service';
import { EquipoMotivo } from './entity/equipo-motivo.entity';
import { EquipoModule } from '../equipo/equipo.module';
import { OperadorModule } from '../operador/operador.module';
import { StatusModule } from '../status/status.module';
@@ -14,10 +14,10 @@ import { StatusModule } from '../status/status.module';
OperadorModule,
StatusModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
TypeOrmModule.forFeature([Motivo]),
TypeOrmModule.forFeature([EquipoMotivo]),
],
controllers: [MotivoController],
providers: [MotivoService],
exports: [MotivoService],
controllers: [EquipoMotivoController],
providers: [EquipoMotivoService],
exports: [EquipoMotivoService],
})
export class MotivoModule {}
export class EquipoMotivoModule {}
@@ -1,15 +1,15 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MotivoService } from './motivo.service';
import { EquipoMotivoService } from './equipo-motivo.service';
describe('MotivoService', () => {
let service: MotivoService;
describe('EquipoMotivoService', () => {
let service: EquipoMotivoService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [MotivoService],
providers: [EquipoMotivoService],
}).compile();
service = module.get<MotivoService>(MotivoService);
service = module.get<EquipoMotivoService>(EquipoMotivoService);
});
it('should be defined', () => {
@@ -3,7 +3,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Equipo } from '../equipo/entity/equipo.entity';
import { Motivo } from './entity/motivo.entity';
import { EquipoMotivo } from './entity/equipo-motivo.entity';
import { Operador } from '../operador/entity/operador.entity';
import { Status } from '../status/entity/status.entity';
import { EquipoService } from '../equipo/equipo.service';
@@ -11,9 +11,10 @@ import { OperadorService } from '../operador/operador.service';
import { StatusService } from '../status/status.service';
@Injectable()
export class MotivoService {
export class EquipoMotivoService {
constructor(
@InjectRepository(Motivo) private repository: Repository<Motivo>,
@InjectRepository(EquipoMotivo)
private repository: Repository<EquipoMotivo>,
@Inject(forwardRef(() => EquipoService))
private equipoService: EquipoService,
private operadorService: OperadorService,
+3 -3
View File
@@ -11,7 +11,7 @@ import { EquipoPrograma } from '../../equipo-programa/entity/equipo-programa.ent
import { EquipoTipoEntrada } from '../../equipo-tipo-entrada/entity/equipo-tipo-entrada.entity';
import { Marca } from '../../marca/entity/marca.entity';
import { Modelo } from '../../modelo/entity/modelo.entity';
import { Motivo } from '../../motivo/entity/motivo.entity';
import { EquipoMotivo } from '../../equipo-motivo/entity/equipo-motivo.entity';
import { Prestamo } from '../../prestamo/entity/prestamo.entity';
import { Status } from '../../status/entity/status.entity';
@@ -48,8 +48,8 @@ export class Equipo {
@JoinColumn({ name: 'id_status' })
status: Status;
@OneToMany(() => Motivo, (motivo) => motivo.equipo)
motivos: Motivo[];
@OneToMany(() => EquipoMotivo, (motivo) => motivo.equipo)
motivos: EquipoMotivo[];
@OneToMany(() => Prestamo, (prestamo) => prestamo.equipo)
prestamos: Prestamo[];
+2 -2
View File
@@ -13,7 +13,7 @@ import { InstitucionTipoEntradaModule } from '../institucion-tipo-entrada/instit
import { MarcaModule } from '../marca/marca.module';
import { ModeloModule } from '../modelo/modelo.module';
import { ModuloModule } from '../modulo/modulo.module';
import { MotivoModule } from '../motivo/motivo.module';
import { EquipoMotivoModule } from '../equipo-motivo/equipo-motivo.module';
import { OperadorModule } from '../operador/operador.module';
import { StatusModule } from '../status/status.module';
@@ -28,7 +28,7 @@ import { StatusModule } from '../status/status.module';
MarcaModule,
ModeloModule,
ModuloModule,
forwardRef(() => MotivoModule),
forwardRef(() => EquipoMotivoModule),
OperadorModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
StatusModule,
+9 -4
View File
@@ -27,7 +27,7 @@ import { InstitucionTipoEntradaService } from '../institucion-tipo-entrada/insti
import { MarcaService } from '../marca/marca.service';
import { ModeloService } from '../modelo/modelo.service';
import { ModuloService } from '../modulo/modulo.service';
import { MotivoService } from '../motivo/motivo.service';
import { EquipoMotivoService } from '../equipo-motivo/equipo-motivo.service';
import { OperadorService } from '../operador/operador.service';
import { StatusService } from '../status/status.service';
@@ -45,8 +45,8 @@ export class EquipoService {
private marcaService: MarcaService,
private modeloService: ModeloService,
private moduloService: ModuloService,
@Inject(forwardRef(() => MotivoService))
private motivoService: MotivoService,
@Inject(forwardRef(() => EquipoMotivoService))
private equipoMotivoService: EquipoMotivoService,
private operadorService: OperadorService,
private statusService: StatusService,
) {}
@@ -408,7 +408,12 @@ export class EquipoService {
status.id_status >= 4 ||
(equipo.status.id_status >= 4 && status.id_status === 1)
)
await this.motivoService.create(equipo, operador, status, motivo);
await this.equipoMotivoService.create(
equipo,
operador,
status,
motivo,
);
equipo.status = status;
}
if (carrito) equipo.carrito = carrito;
@@ -17,7 +17,7 @@ export class Carrera {
@Column({ type: String, nullable: false, length: 150 })
carrera: string;
@Column({ type: String, nullable: false, length: 3 })
@Column({ type: String, nullable: false, length: 10 })
clave: string;
@ManyToOne(() => Nivel, (nivel) => nivel.carreras, { eager: true })
@@ -18,16 +18,16 @@ export class InstitucionDia {
@Column({ type: Boolean, nullable: false, default: false })
activo: boolean;
@Column({ type: String, nullable: false, default: '18:45', length: 5 })
@Column({ type: String, nullable: false, default: '18:35', length: 5 })
hora_extra: string;
@Column({ type: String, nullable: false, default: '17:45', length: 5 })
@Column({ type: String, nullable: false, default: '17:35', length: 5 })
hora_fin: string;
@Column({ type: String, nullable: false, default: '08:50', length: 5 })
hora_inicio: string;
@Column({ type: String, nullable: false, default: '20:00', length: 5 })
@Column({ type: String, nullable: false, default: '19:45', length: 5 })
hora_tope: string;
@ManyToOne(() => Dia, (dia) => dia.institucionesDias, { eager: true })
-18
View File
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MotivoController } from './motivo.controller';
describe('MotivoController', () => {
let controller: MotivoController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MotivoController],
}).compile();
controller = module.get<MotivoController>(MotivoController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
+104
View File
@@ -7,6 +7,110 @@ import { NodemailerMessageDto } from './dto/nodemailer-message.dto';
export class NodemailerService {
constructor(private configService: ConfigService) {}
correoPasswordAdmin(operador, password: string) {
return `<h2>Estimad@ Responsable del programa PC Puma:</h2>
<p>Su registro se ha realizado exitosamente, para acceder al servicio del sistema de préstamo deberá hacerlo con los siguientes datos:</p>
<p>• Usuario: ${operador}</p>
<p>• Contraseña: ${password}</p>
<p style=" text-align: justify; text-justify: inter-word;">
El presente registro le permite tener un uso administrativo del sistema de préstamo de
dispositivos PC Puma para su entidad. Para acceder al sistema de préstamo es obligatorio y su
responsabilidad leer y conocer los lineamientos “PC Puma” de la entidad que administra el
servicio, dichos lineamientos deberán estar ubicados en el módulo de préstamo y en la página de
su entidad.
</p>
<p>Para más información o dudas, puede contactarnos por este medio.</p>
<p>Atte.</p>
<p>PC Puma</p>
<p style=" text-align: justify; text-justify: inter-word;">
<i>
Hacemos de su conocimiento que la presente información es confidencial y será
tratada como tal, es de uso exclusivo para el usuario al que fue asignada y se encuentra protegida
por la normatividad de la Universidad Nacional Autónoma de México en la materia, cualquier uso
no autorizado o indebido se hará del conocimiento de la autoridad universitaria. Puede consultar
nuestro aviso de privacidad en: <a href="https://sdi.unam.mx/avisospriv.php" target="_blank">https://sdi.unam.mx/avisospriv.php</a>
</i>
</p>
`;
}
correoPasswordOperador(operador, password: string) {
return `<h2>Estimad@ operador del servicio de préstamo PC Puma, su registro se realizó con éxito.</h2>
<p>Para acceder al servicio debe hacerlo con los siguientes datos:</p>
<p>• Usuario: ${operador}</p>
<p>• Contraseña: ${password}</p>
<p style=" text-align: justify; text-justify: inter-word;">
El presente registro le permite hacer uso del sistema de préstamo de dispositivos PC Puma.
Para acceder al servicio es obligatorio y su responsabilidad leer y conocer los “lineamientos
PC Puma” de la entidad en la que es solicitado el préstamo, ubicados en el módulo de
préstamo o la página de su entidad.
</p>
<p>Para más información o dudas, puede contactarnos por este medio.</p>
<p>Atte.</p>
<p>PC Puma</p>
<p style=" text-align: justify; text-justify: inter-word;">
<i>
Hacemos de su conocimiento que la presente
información es confidencial y será tratada como tal, es de uso exclusivo para el usuario al que
fue asignada y se encuentra protegida por la normatividad de la Universidad Nacional
Autónoma de México en la materia, cualquier uso no autorizado o indebido se hará del
conocimiento de la autoridad universitaria. Puede consultar nuestro aviso de privacidad en:
<a href="https://sdi.unam.mx/avisospriv.php" target="_blank">https://sdi.unam.mx/avisospriv.php</a>
</i>
</p>
`;
}
correoPasswordUsuario(password: string) {
return `<h2>Estimad@ usuari@ del servicio de préstamo PC Puma, su registro se realizó con éxito</h2>
<p>Para acceder al servicio debe hacerlo con los siguientes datos:</p>
<p>• Usuario: Número de cuenta o número de trabajador</p>
<p>• Contraseña: ${password}</p>
<p style=" text-align: justify; text-justify: inter-word;">
El presente registro le permite hacer uso del servicio de préstamo de dispositivos PC Puma
seleccionados, el cual puede ser utilizado únicamente al interior de la entidad donde se solicita el
servicio. Para acceder al servicio de préstamo es obligatorio y es su responsabilidad leer los
“lineamientos PC- Puma” de la entidad en la que es solicitado el préstamo, ubicados en el módulo
de préstamo y en la página de su entidad. El desconocimiento de dichos lineamientos no le exime
de su responsabilidad y aplicación efectiva de los mismos.
</p>
<p>Para mayor información o duda contáctenos por este medio.</p>
<p>Atte.</p>
<p>Pc Puma Solicita</p>
<p style=" text-align: justify; text-justify: inter-word;">
<i>
Hacemos de su conocimiento que la
presente información es confidencial y será tratada como tal, es de uso exclusivo para el usuario al
que fue asignada y se encuentra protegida por la normatividad de la Universidad Nacional
Autónoma de México en la materia, cualquier uso no autorizado o indebido se hará del
conocimiento de la autoridad universitaria. Puede consultar nuestro aviso de privacidad en:
<a href="https://sdi.unam.mx/avisospriv.php" target="_blank">https://sdi.unam.mx/avisospriv.php</a>
</i>
</p>
`;
}
sendEmail(message: NodemailerMessageDto) {
const transporter = nodemailer.createTransport({
service: this.configService.get<string>('NODEMAILER_SERVICE'),
+3 -3
View File
@@ -7,7 +7,7 @@ import {
PrimaryGeneratedColumn,
} from 'typeorm';
import { Institucion } from '../../institucion/entity/institucion.entity';
import { Motivo } from '../../motivo/entity/motivo.entity';
import { EquipoMotivo } from '../../equipo-motivo/entity/equipo-motivo.entity';
import { Multa } from '../../multa/entity/multa.entity';
import { Prestamo } from '../../prestamo/entity/prestamo.entity';
import { TipoUsuario } from '../../tipo-usuario/entity/tipo-usuario.entity';
@@ -44,8 +44,8 @@ export class Operador {
@JoinColumn({ name: 'id_tipo_usuario' })
tipoUsuario: TipoUsuario;
@OneToMany(() => Motivo, (motivo) => motivo.operador)
motivos: Motivo[];
@OneToMany(() => EquipoMotivo, (motivo) => motivo.operador)
motivos: EquipoMotivo[];
@OneToMany(() => Multa, (multa) => multa.opeardorMulta)
multas: Multa[];
+16 -72
View File
@@ -22,74 +22,6 @@ export class OperadorService {
private tipoUsuarioService: TipoUsuarioService,
) {}
private correoPasswordAdmin(operador, password: string) {
return `<h2>Estimad@ Responsable del programa PC Puma:</h2>
<p>Su registro se ha realizado exitosamente, para acceder al servicio del sistema de préstamo deberá hacerlo con los siguientes datos:</p>
<p>• Usuario: ${operador}</p>
<p>• Contraseña: ${password}</p>
<p style=" text-align: justify; text-justify: inter-word;">
El presente registro le permite tener un uso administrativo del sistema de préstamo de
dispositivos PC Puma para su entidad. Para acceder al sistema de préstamo es obligatorio y su
responsabilidad leer y conocer los lineamientos “PC Puma” de la entidad que administra el
servicio, dichos lineamientos deberán estar ubicados en el módulo de préstamo y en la página de
su entidad.
</p>
<p>Para más información o dudas, puede contactarnos por este medio.</p>
<p>Atte.</p>
<p>PC Puma</p>
<p style=" text-align: justify; text-justify: inter-word;">
<i>
Hacemos de su conocimiento que la presente información es confidencial y será
tratada como tal, es de uso exclusivo para el usuario al que fue asignada y se encuentra protegida
por la normatividad de la Universidad Nacional Autónoma de México en la materia, cualquier uso
no autorizado o indebido se hará del conocimiento de la autoridad universitaria. Puede consultar
nuestro aviso de privacidad en: <a href="https://sdi.unam.mx/avisospriv.php" target="_blank">https://sdi.unam.mx/avisospriv.php</a>
</i>
</p>
`;
}
private correoPasswordOperador(operador, password: string) {
return `<h2>Estimad@ operador del servicio de préstamo PC Puma, su registro se realizó con éxito.</h2>
<p>Para acceder al servicio debe hacerlo con los siguientes datos:</p>
<p>• Usuario: ${operador}</p>
<p>• Contraseña: ${password}</p>
<p style=" text-align: justify; text-justify: inter-word;">
El presente registro le permite hacer uso del sistema de préstamo de dispositivos PC Puma.
Para acceder al servicio es obligatorio y su responsabilidad leer y conocer los “lineamientos
PC Puma” de la entidad en la que es solicitado el préstamo, ubicados en el módulo de
préstamo o la página de su entidad.
</p>
<p>Para más información o dudas, puede contactarnos por este medio.</p>
<p>Atte.</p>
<p>PC Puma</p>
<p style=" text-align: justify; text-justify: inter-word;">
<i>
Hacemos de su conocimiento que la presente
información es confidencial y será tratada como tal, es de uso exclusivo para el usuario al que
fue asignada y se encuentra protegida por la normatividad de la Universidad Nacional
Autónoma de México en la materia, cualquier uso no autorizado o indebido se hará del
conocimiento de la autoridad universitaria. Puede consultar nuestro aviso de privacidad en:
<a href="https://sdi.unam.mx/avisospriv.php" target="_blank">https://sdi.unam.mx/avisospriv.php</a>
</i>
</p>
`;
}
async create(
id_institucion: number | Institucion,
id_tipo_usuario: number,
@@ -139,8 +71,14 @@ export class OperadorService {
subject: 'Credenciales Pc Puma',
html:
tipoUsuario.id_tipo_usuario === 3
? this.correoPasswordAdmin(operador.operador, password)
: this.correoPasswordOperador(operador.operador, password),
? this.nodemailerService.correoPasswordAdmin(
operador.operador,
password,
)
: this.nodemailerService.correoPasswordOperador(
operador.operador,
password,
),
}),
)
.then((_) => ({
@@ -241,8 +179,14 @@ export class OperadorService {
subject: 'Credenciales PC Puma',
html:
operador.tipoUsuario.id_tipo_usuario === 3
? this.correoPasswordAdmin(operador.operador, password)
: this.correoPasswordOperador(operador.operador, password),
? this.nodemailerService.correoPasswordAdmin(
operador.operador,
password,
)
: this.nodemailerService.correoPasswordOperador(
operador.operador,
password,
),
}),
)
.then((_) => ({
+3 -3
View File
@@ -1,6 +1,6 @@
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Equipo } from '../../equipo/entity/equipo.entity';
import { Motivo } from '../../motivo/entity/motivo.entity';
import { EquipoMotivo } from '../../equipo-motivo/entity/equipo-motivo.entity';
@Entity()
export class Status {
@@ -13,6 +13,6 @@ export class Status {
@OneToMany(() => Equipo, (equipo) => equipo.status)
equipos: Equipo[];
@OneToMany(() => Motivo, (motivo) => motivo.status)
motivos: Motivo[];
@OneToMany(() => EquipoMotivo, (motivo) => motivo.status)
motivos: EquipoMotivo[];
}
+4 -7
View File
@@ -1,4 +1,3 @@
import * as fs from 'fs';
import {
BadRequestException,
Controller,
@@ -43,12 +42,10 @@ export class UploadFileController {
const path = file ? `${file.destination}/${file.filename}` : null;
if (!file) throw new BadRequestException('No se mandó ningún archivo.');
return this.uploadFileService
.createUsuarios(path, parseInt(query.id_institucion))
.then((res) => {
fs.unlink(path, (err) => {});
return res;
});
return this.uploadFileService.createUsuarios(
path,
parseInt(query.id_institucion),
);
}
@Get('download-logo')
+183 -177
View File
@@ -3,6 +3,7 @@ import * as csvtojson from 'csvtojson';
import { BadRequestException, Injectable } from '@nestjs/common';
import { AppGateway } from '../app.gateway';
import { Equipo } from '../equipo/entity/equipo.entity';
import { Institucion } from '../institucion/entity/institucion.entity';
import { InstitucionUsuario } from '../institucion-usuario/entity/institucion-usuario.entity';
import { CarritoService } from '../carrito/carrito.service';
import { EquipoService } from '../equipo/equipo.service';
@@ -21,7 +22,6 @@ import { TipoUsuarioService } from '../tipo-usuario/tipo-usuario.service';
import { UsuarioService } from '../usuario/usuario.service';
import { UploadFileCargaMasivaUsuarioDto } from './dto/upload-file-carga-masiva-usuario.dto';
import { UploadFileCargaMasivaEquipoDto } from './dto/upload-file-carga-masiva-equipo.dto';
import { Institucion } from 'src/institucion/entity/institucion.entity';
@Injectable()
export class UploadFileService {
@@ -44,6 +44,85 @@ export class UploadFileService {
private usuarioService: UsuarioService,
) {}
async createEquipos(path: string, id_institucion: number) {
const institucion = await this.institucionService.findById(id_institucion);
let errores: string[] = [];
let mensajes: string[] = [];
let equiposNuevos: Equipo[] = [];
csvtojson()
.fromFile(path)
.then(async (equipos: UploadFileCargaMasivaEquipoDto[]) => {
for (let i = 0; i < equipos.length; i++) {
await this.evaluarEquipo(
institucion,
i,
equipos[i],
mensajes,
errores,
equiposNuevos,
);
if ((i + 1) % 90 === 0 || i + 1 === equipos.length) {
this.appGateway.equiposNuevos(institucion.id_institucion, {
errores,
mensajes,
equiposNuevos,
});
errores = [];
mensajes = [];
equiposNuevos = [];
}
}
fs.unlink(path, (err) => {});
});
return { message: 'Se subió y cargó correctamente su archivo csv.' };
}
async createUsuarios(path: string, id_institucion: number) {
const institucion = await this.institucionService.findById(id_institucion);
let errores: string[] = [];
let mensajes: string[] = [];
let usuariosNuevos: InstitucionUsuario[] = [];
csvtojson()
.fromFile(path)
.then(async (usuarios: UploadFileCargaMasivaUsuarioDto[]) => {
for (let i = 0; i < usuarios.length; i++) {
await this.evaluarUsuario(
institucion,
i,
usuarios[i],
mensajes,
errores,
usuariosNuevos,
);
if ((i + 1) % 100 === 0 || i + 1 === usuarios.length) {
this.appGateway.usuariosNuevos(institucion.id_institucion, {
errores,
mensajes,
usuariosNuevos,
});
errores = [];
mensajes = [];
usuariosNuevos = [];
}
}
});
return { message: 'Se subió y cargó correctamente tu archivo csv.' };
}
downloadLogo(id_institucion: number) {
return this.institucionService
.findById(id_institucion)
.then((institucion) => institucion.logo);
}
errorBase(index) {
return `Se saltó la linea ${
index + 2
} por el siguiente/los siguientes motivo(s):`;
}
async evaluarEquipo(
institucion: Institucion,
index: number,
@@ -251,188 +330,115 @@ export class UploadFileService {
}
}
async createEquipos(path: string, id_institucion: number) {
const institucion = await this.institucionService.findById(id_institucion);
let errores: string[] = [];
let mensajes: string[] = [];
let equiposNuevos: Equipo[] = [];
async evaluarUsuario(
institucion: Institucion,
index: number,
dataUsuario: UploadFileCargaMasivaUsuarioDto,
mensajes: string[],
errores: string[],
usuariosNuevos: InstitucionUsuario[],
) {
let error = this.errorBase(index);
csvtojson()
.fromFile(path)
.then(async (equipos: UploadFileCargaMasivaEquipoDto[]) => {
for (let i = 0; i < equipos.length; i++) {
await this.evaluarEquipo(
institucion,
i,
equipos[i],
mensajes,
errores,
equiposNuevos,
);
if ((i + 1) % 100 === 0) {
this.appGateway.equiposNuevos(institucion.id_institucion, {
errores,
mensajes,
equiposNuevos,
});
errores = [];
mensajes = [];
equiposNuevos = [];
}
}
fs.unlink(path, (err) => {});
});
return { message: 'Se subió y cargó correctamente su archivo csv.' };
}
if (!dataUsuario.numero_cuenta || !dataUsuario.tipo_usuario) {
if (!dataUsuario.numero_cuenta) error += ' falta del campo numero_cuenta';
if (error != this.errorBase(index)) error += ',';
if (!dataUsuario.tipo_usuario) error += ' falta del campo tipo_usuario';
errores.push(`${error}.`);
return;
}
async createUsuarios(path: string, id_institucion: number) {
const institucion = await this.institucionService.findById(id_institucion);
const errores: string[] = [];
const mensajes: string[] = [];
const usuariosActualizados: InstitucionUsuario[] = [];
const usuariosNuevos: InstitucionUsuario[] = [];
return csvtojson()
.fromFile(path)
.then(async (usuarios: UploadFileCargaMasivaUsuarioDto[]) => {
for (let i = 0; i < usuarios.length; i++) {
let error = this.errorBase(i);
if (!usuarios[i].numero_cuenta || !usuarios[i].tipo_usuario) {
if (!usuarios[i].numero_cuenta)
error += ' falta del campo numero_cuenta';
if (error != this.errorBase(i)) error += ',';
if (!usuarios[i].tipo_usuario)
error += ' falta del campo tipo_usuario';
errores.push(`${error}.`);
continue;
}
const tipoUsuario = await this.tipoUsuarioService.findByTipoUsuario(
usuarios[i].tipo_usuario,
const tipoUsuario = await this.tipoUsuarioService.findByTipoUsuario(
dataUsuario.tipo_usuario,
false,
);
const usuario = await this.usuarioService.findByUsuario(
dataUsuario.numero_cuenta,
false,
);
const carrera =
tipoUsuario && (tipoUsuario.id_tipo_usuario === 5 || dataUsuario.carrera)
? await this.institucionCarreraService.findCarreraByCarrera(
tipoUsuario.id_tipo_usuario === 5
? 'PROFESOR'
: dataUsuario.carrera,
false,
);
const usuario = await this.usuarioService.findByUsuario(
usuarios[i].numero_cuenta,
false,
);
const carrera =
tipoUsuario &&
(tipoUsuario.id_tipo_usuario === 5 || usuarios[i].carrera)
? await this.institucionCarreraService.findCarreraByCarrera(
tipoUsuario.id_tipo_usuario === 5
? 'PROFESOR'
: usuarios[i].carrera,
false,
)
: null;
const institucionCarrera = carrera
? await this.institucionCarreraService.findByIdInstitucionIdCarrera(
institucion,
carrera,
)
: null;
)
: null;
const institucionCarrera = carrera
? await this.institucionCarreraService.findByIdInstitucionIdCarrera(
institucion,
carrera,
)
: null;
if (
!tipoUsuario ||
tipoUsuario.id_tipo_usuario < 5 ||
!carrera ||
(carrera && !institucionCarrera)
) {
if (!tipoUsuario) error += ' no existe este tipo de usuario.';
else if (tipoUsuario.id_tipo_usuario < 5)
' no se puede asignar este tipo de usuario a este usuario.';
if (!carrera) error += ' falta del campo carrera para el alumno.';
if (carrera && !institucionCarrera)
error += ' la carrera no pertenece a la institución.';
errores.push(error);
continue;
}
if (usuario) {
let existeInstitucionUsuario: InstitucionUsuario = null;
if (
!tipoUsuario ||
tipoUsuario.id_tipo_usuario < 5 ||
!carrera ||
(carrera && !institucionCarrera)
) {
if (!tipoUsuario) error += ' no existe este tipo de usuario.';
else if (tipoUsuario.id_tipo_usuario < 5)
' no se puede asignar este tipo de usuario a este usuario.';
if (!carrera) error += ' falta del campo carrera para el alumno.';
if (carrera && !institucionCarrera)
error += ' la carrera no pertenece a la institución.';
errores.push(error);
return;
}
if (usuario) {
let existeInstitucionUsuario: InstitucionUsuario = null;
for (let i = 0; i < usuario.instituciones.length; i++)
if (
usuario.instituciones[i].institucionCarrera
.id_institucion_carrera ===
institucionCarrera.id_institucion_carrera
) {
existeInstitucionUsuario = usuario.instituciones[i];
break;
}
if (existeInstitucionUsuario && !existeInstitucionUsuario.activo) {
existeInstitucionUsuario.activo = true;
await this.institucionUsuarioService.update(
existeInstitucionUsuario,
);
} else if (!existeInstitucionUsuario)
await this.institucionUsuarioService
.create(institucionCarrera, usuario)
.then(({ message, institucionUsuario }) => {
usuariosActualizados.push(institucionUsuario);
mensajes.push(message);
});
if (
usuario.tipoUsuario.id_tipo_usuario != tipoUsuario.id_tipo_usuario
) {
usuario.tipoUsuario = tipoUsuario;
await this.usuarioService
.update(usuario, false)
.then(({ message }) => mensajes.push(message));
}
} else {
if (
!usuarios[i].nombres ||
!usuarios[i].apellido_m ||
!usuarios[i].apellido_p
) {
if (error != this.errorBase(i)) error += ',';
if (!usuarios[i].nombres) error += ' falta del campo nombres';
if (error != this.errorBase(i)) error += ',';
if (!usuarios[i].apellido_m)
error += ' falta del campo apellido_m';
if (error != this.errorBase(i)) error += ',';
if (!usuarios[i].apellido_p)
error += ' falta del campo apellido_p';
errores.push(`${error}.`);
continue;
}
await this.usuarioService
.create(
usuarios[i].numero_cuenta,
`${usuarios[i].apellido_p.trim()} ${usuarios[
i
].apellido_m.trim()} ${usuarios[i].nombres.trim()}`,
tipoUsuario,
institucionCarrera,
usuarios[i].correo ? usuarios[i].correo : '',
usuarios[i].rfc ? usuarios[i].rfc : '',
)
.then(({ message, institucionUsuario }) => {
usuariosNuevos.push(institucionUsuario);
mensajes.push(message);
});
}
for (let i = 0; i < usuario.instituciones.length; i++)
if (
usuario.instituciones[i].institucionCarrera.id_institucion_carrera ===
institucionCarrera.id_institucion_carrera
) {
existeInstitucionUsuario = usuario.instituciones[i];
break;
}
return {
message: 'Se subió y cargó correctamente tu archivo csv.',
mensajes,
usuariosNuevos,
errores,
};
});
}
downloadLogo(id_institucion: number) {
return this.institucionService
.findById(id_institucion)
.then((institucion) => institucion.logo);
}
errorBase(index) {
return `Se saltó la linea ${
index + 2
} por el siguiente/los siguientes motivo(s):`;
if (existeInstitucionUsuario && !existeInstitucionUsuario.activo) {
existeInstitucionUsuario.activo = true;
await this.institucionUsuarioService.update(existeInstitucionUsuario);
} else if (!existeInstitucionUsuario)
await this.institucionUsuarioService
.create(institucionCarrera, usuario)
.then(({ message }) => mensajes.push(message));
if (usuario.tipoUsuario.id_tipo_usuario != tipoUsuario.id_tipo_usuario) {
usuario.tipoUsuario = tipoUsuario;
await this.usuarioService
.update(usuario, false)
.then(({ message }) => mensajes.push(message));
}
} else {
if (
!dataUsuario.nombres ||
!dataUsuario.apellido_m ||
!dataUsuario.apellido_p
) {
if (!dataUsuario.nombres) error += ' falta del campo nombres';
if (error != this.errorBase(index)) error += ',';
if (!dataUsuario.apellido_m) error += ' falta del campo apellido_m';
if (error != this.errorBase(index)) error += ',';
if (!dataUsuario.apellido_p) error += ' falta del campo apellido_p';
errores.push(`${error}.`);
return;
}
await this.usuarioService
.create(
dataUsuario.numero_cuenta,
`${dataUsuario.apellido_p.trim()} ${dataUsuario.apellido_m.trim()} ${dataUsuario.nombres.trim()}`,
tipoUsuario,
institucionCarrera,
dataUsuario.correo ? dataUsuario.correo : '',
dataUsuario.rfc ? dataUsuario.rfc : '',
)
.then(({ message, institucionUsuario }) => {
usuariosNuevos.push(institucionUsuario);
mensajes.push(message);
});
}
}
async uploadLogo(file: Express.Multer.File, id_institucion: number) {
+2 -39
View File
@@ -15,7 +15,6 @@ import { InstitucionUsuarioService } from '../institucion-usuario/institucion-us
import { NodemailerService } from '../nodemailer/nodemailer.service';
import { SoapClientService } from '../soap-client/soap-client.service';
import { TipoUsuarioService } from '../tipo-usuario/tipo-usuario.service';
import { query } from 'express';
@Injectable()
export class UsuarioService {
@@ -53,42 +52,6 @@ export class UsuarioService {
);
}
correoPassword(password: string) {
return `<h2>Estimad@ usuari@ del servicio de préstamo PC Puma, su registro se realizó con éxito</h2>
<p>Para acceder al servicio debe hacerlo con los siguientes datos:</p>
<p>• Usuario: Número de cuenta o número de trabajador</p>
<p>• Contraseña: ${password}</p>
<p style=" text-align: justify; text-justify: inter-word;">
El presente registro le permite hacer uso del servicio de préstamo de dispositivos PC Puma
seleccionados, el cual puede ser utilizado únicamente al interior de la entidad donde se solicita el
servicio. Para acceder al servicio de préstamo es obligatorio y es su responsabilidad leer los
“lineamientos PC- Puma” de la entidad en la que es solicitado el préstamo, ubicados en el módulo
de préstamo y en la página de su entidad. El desconocimiento de dichos lineamientos no le exime
de su responsabilidad y aplicación efectiva de los mismos.
</p>
<p>Para mayor información o duda contáctenos por este medio.</p>
<p>Atte.</p>
<p>Pc Puma Solicita</p>
<p style=" text-align: justify; text-justify: inter-word;">
<i>
Hacemos de su conocimiento que la
presente información es confidencial y será tratada como tal, es de uso exclusivo para el usuario al
que fue asignada y se encuentra protegida por la normatividad de la Universidad Nacional
Autónoma de México en la materia, cualquier uso no autorizado o indebido se hará del
conocimiento de la autoridad universitaria. Puede consultar nuestro aviso de privacidad en:
<a href="https://sdi.unam.mx/avisospriv.php" target="_blank">https://sdi.unam.mx/avisospriv.php</a>
</i>
</p>
`;
}
async DGAE(usuario: string, id_institucion_carrera: number) {
const institucionCarrera = await this.institucionCarreraService.findById(
id_institucion_carrera,
@@ -250,7 +213,7 @@ export class UsuarioService {
this.nodemailerService.sendEmail({
email: `${usuario.usuario}@pcpuma.acatlan.unam.mx`,
subject: 'Credenciales Pc Puma',
html: this.correoPassword(password),
html: this.nodemailerService.correoPasswordUsuario(password),
}),
)
.then((_) => ({
@@ -290,7 +253,7 @@ export class UsuarioService {
this.nodemailerService.sendEmail({
email: usuario.correo,
subject: 'Credenciales Pc Puma',
html: this.correoPassword(password),
html: this.nodemailerService.correoPasswordUsuario(password),
}),
)
.then((_) => ({ message }));