diff --git a/src/modules/prova/dtos/create.dto.input.ts b/src/modules/prova/dtos/create.dto.input.ts index e05e88d..bce50a9 100644 --- a/src/modules/prova/dtos/create.dto.input.ts +++ b/src/modules/prova/dtos/create.dto.input.ts @@ -46,4 +46,12 @@ export class CreateProvaDTOInput { @ApiProperty() @IsString() criadorId: string; + + // Campo INTERNO: injetado pelo api-vcnafacul (Card 03) a partir do Collaborator + // do usuário. Não vem do cliente final. @IsOptional + @IsString para sobreviver + // ao whitelist do ValidationPipe e aceitar ausência/null. + @ApiProperty({ required: false, nullable: true }) + @IsOptional() + @IsString() + cursinhoId?: string | null; } diff --git a/src/modules/prova/factory/enem_2010_2016_factory.ts b/src/modules/prova/factory/enem_2010_2016_factory.ts index 48a1c3a..b98312a 100644 --- a/src/modules/prova/factory/enem_2010_2016_factory.ts +++ b/src/modules/prova/factory/enem_2010_2016_factory.ts @@ -57,16 +57,14 @@ export class Enem2010_2017Factory implements IProvaFactory { public async createSimuladoDia1(prova: Prova) { const mainName = `${prova.categoria.nome} ${prova.ano}`; - const exameId = prova.categoria.exame._id.toString(); prova.simulados.push( await this.enemService.createSimuladoArea( - mainName, + prova, EnemArea.CienciasHumanas, - exameId, ), ); prova.simulados.push( - await this.enemService.createSimuladoArea(mainName, EnemArea.BioExatas, exameId), + await this.enemService.createSimuladoArea(prova, EnemArea.BioExatas), ); prova.simulados.push( await this.simuladoRepository.create({ @@ -74,16 +72,16 @@ export class Enem2010_2017Factory implements IProvaFactory { categoria: prova.categoria, questoes: [], descricao: `${prova.categoria.exame.nome}`, + criadorId: prova.criadorId, + cursinhoId: prova.cursinhoId, }), ); } public async createSimuladoDia2(prova: Prova) { - const mainName = `${prova.categoria.nome} ${prova.ano}`; - const exameId = prova.categoria.exame._id.toString(); await this.enemService.createSimuladoIdiomatica(prova); prova.simulados.push( - await this.enemService.createSimuladoArea(mainName, EnemArea.Matematica, exameId), + await this.enemService.createSimuladoArea(prova, EnemArea.Matematica), ); } diff --git a/src/modules/prova/factory/enem_2017_plus_factory.ts b/src/modules/prova/factory/enem_2017_plus_factory.ts index dfa2576..2fd5a45 100644 --- a/src/modules/prova/factory/enem_2017_plus_factory.ts +++ b/src/modules/prova/factory/enem_2017_plus_factory.ts @@ -56,26 +56,22 @@ export class Enem2017PlusFactory implements IProvaFactory { } public async createSimuladoDia1(prova: Prova) { - const mainName = `${prova.categoria.nome} ${prova.ano}`; - const exameId = prova.categoria.exame._id.toString(); await this.enemService.createSimuladoIdiomatica(prova); prova.simulados.push( await this.enemService.createSimuladoArea( - mainName, + prova, EnemArea.CienciasHumanas, - exameId, ), ); } public async createSimuladoDia2(prova: Prova) { const mainName = `${prova.categoria.nome} ${prova.ano}`; - const exameId = prova.categoria.exame._id.toString(); prova.simulados.push( - await this.enemService.createSimuladoArea(mainName, EnemArea.Matematica, exameId), + await this.enemService.createSimuladoArea(prova, EnemArea.Matematica), ); prova.simulados.push( - await this.enemService.createSimuladoArea(mainName, EnemArea.BioExatas, exameId), + await this.enemService.createSimuladoArea(prova, EnemArea.BioExatas), ); prova.simulados.push( await this.simuladoRepository.create({ @@ -83,6 +79,8 @@ export class Enem2017PlusFactory implements IProvaFactory { categoria: prova.categoria, questoes: [], descricao: `${prova.categoria.exame.nome}`, + criadorId: prova.criadorId, + cursinhoId: prova.cursinhoId, }), ); } diff --git a/src/modules/prova/prova.controller.ts b/src/modules/prova/prova.controller.ts index c3bc661..2420fc6 100644 --- a/src/modules/prova/prova.controller.ts +++ b/src/modules/prova/prova.controller.ts @@ -49,6 +49,20 @@ export class ProvaController { return await this.service.getAll(query); } + @Get('cursinho/:cursinhoId') + @ApiResponse({ + status: 200, + description: 'busca provas de um cursinho', + type: Prova, + isArray: true, + }) + public async getAllByCursinho( + @Param('cursinhoId') cursinhoId: string, + @Query() query: GetAllDtoInput, + ): Promise> { + return await this.service.getAllByCursinho(cursinhoId, query); + } + @Get('summary') async getSummary() { return await this.service.getSummary(); diff --git a/src/modules/prova/prova.schema.spec.ts b/src/modules/prova/prova.schema.spec.ts index 2b0f828..72bc7a1 100644 --- a/src/modules/prova/prova.schema.spec.ts +++ b/src/modules/prova/prova.schema.spec.ts @@ -19,6 +19,19 @@ describe('Prova schema — criadorId / cursinhoId', () => { expect(prova.cursinhoId).toBeNull(); }); + it('constructor lê cursinhoId do item quando presente', () => { + const item = { + criadorId: 'user-1', + cursinhoId: 'curs-1', + ano: 2023, + filename: 'f.pdf', + } as unknown as CreateProvaDTOInput; + + const prova = new Prova(item, categoria); + + expect(prova.cursinhoId).toBe('curs-1'); + }); + describe('validação do schema Mongoose', () => { let Model: mongoose.Model; @@ -43,3 +56,13 @@ describe('Prova schema — criadorId / cursinhoId', () => { }); }); }); + +describe('ProvaSchema — índices', () => { + it('declara índice em cursinhoId', () => { + const indexes = ProvaSchema.indexes(); + const hasCursinhoIndex = indexes.some( + ([fields]) => (fields as Record).cursinhoId === 1, + ); + expect(hasCursinhoIndex).toBe(true); + }); +}); diff --git a/src/modules/prova/prova.schema.ts b/src/modules/prova/prova.schema.ts index 7c42a53..2116a1d 100644 --- a/src/modules/prova/prova.schema.ts +++ b/src/modules/prova/prova.schema.ts @@ -18,7 +18,7 @@ export class Prova extends BaseSchema { this.gabarito = item.gabarito; this.aplicacao = item.aplicacao; this.criadorId = item.criadorId; - this.cursinhoId = null; + this.cursinhoId = item.cursinhoId ?? null; this.simulados = []; this.questoes = []; } @@ -76,3 +76,5 @@ export class Prova extends BaseSchema { } export const ProvaSchema = SchemaFactory.createForClass(Prova); + +ProvaSchema.index({ cursinhoId: 1 }); diff --git a/src/modules/prova/prova.service.spec.ts b/src/modules/prova/prova.service.spec.ts index 0c3b7ec..ca17e79 100644 --- a/src/modules/prova/prova.service.spec.ts +++ b/src/modules/prova/prova.service.spec.ts @@ -105,3 +105,80 @@ describe('ProvaService.selectQuestionsForSimulado — branch custom', () => { expect(result).toHaveLength(2); }); }); + +describe('ProvaService.getAllByCursinho', () => { + function makeProva(id: string) { + return { + _id: id, + edicao: 'Regular', + aplicacao: 1, + ano: 2024, + categoria: { nome: 'Personalizado', exame: { nome: 'Personalizado' } }, + nome: `Prova ${id}`, + totalQuestao: 30, + gabarito: 'x', + totalQuestaoValidadas: 0, + filename: 'f.pdf', + enemAreas: [] as any[], + questoes: [] as any[], + createdAt: new Date('2024-01-01T00:00:00.000Z'), + }; + } + + function makeServiceWithGetAll(getAll: jest.Mock) { + const repository = { getAll }; + const service = new ProvaService( + {} as any, // provaFactory + repository as any, + {} as any, // categoriaRepository + {} as any, // simuladoRepository + {} as any, // questaoRepository + {} as any, // frenteRepository + ); + return { service, repository }; + } + + it('filtra por cursinhoId via where e mantém a paginação', async () => { + const getAll = jest.fn().mockResolvedValue({ + data: [makeProva('p1')], + page: 1, + limit: 40, + totalItems: 1, + }); + const { service } = makeServiceWithGetAll(getAll); + + const result = await service.getAllByCursinho('curs-1', { + page: 1, + limit: 40, + }); + + expect(getAll).toHaveBeenCalledWith({ + page: 1, + limit: 40, + where: { cursinhoId: 'curs-1' }, + }); + expect(result.totalItems).toBe(1); + expect(result.data[0]._id).toBe('p1'); + expect(result.data[0].categoria).toBe('Personalizado'); + expect(result.data[0].exame).toBe('Personalizado'); + expect(result.data[0].totalQuestaoCadastradas).toBe(0); + }); + + it('cursinho sem provas retorna data vazia (não lança)', async () => { + const getAll = jest.fn().mockResolvedValue({ + data: [], + page: 1, + limit: 40, + totalItems: 0, + }); + const { service } = makeServiceWithGetAll(getAll); + + const result = await service.getAllByCursinho('curs-x', { + page: 1, + limit: 40, + }); + + expect(result.data).toEqual([]); + expect(result.totalItems).toBe(0); + }); +}); diff --git a/src/modules/prova/prova.service.ts b/src/modules/prova/prova.service.ts index 2f27908..779d0b9 100644 --- a/src/modules/prova/prova.service.ts +++ b/src/modules/prova/prova.service.ts @@ -72,30 +72,46 @@ export class ProvaService { return prova; } + private toProvaDTO(prova: Prova): GetProvaDTOOutout { + return { + _id: prova._id, + edicao: prova.edicao, + aplicacao: prova.aplicacao, + ano: prova.ano, + categoria: prova.categoria.nome, + exame: prova.categoria.exame.nome, + nome: prova.nome, + totalQuestao: prova.totalQuestao, + gabarito: prova.gabarito, + totalQuestaoValidadas: prova.totalQuestaoValidadas, + filename: prova.filename, + enemAreas: prova.enemAreas, + totalQuestaoCadastradas: prova.questoes.length, + createdAt: prova.createdAt, + } as GetProvaDTOOutout; + } + public async getAll( param: GetAllInput, ): Promise> { const provas = await this.repository.getAll(param); return { ...provas, - data: provas.data.map((prova) => { - return { - _id: prova._id, - edicao: prova.edicao, - aplicacao: prova.aplicacao, - ano: prova.ano, - categoria: prova.categoria.nome, - exame: prova.categoria.exame.nome, - nome: prova.nome, - totalQuestao: prova.totalQuestao, - gabarito: prova.gabarito, - totalQuestaoValidadas: prova.totalQuestaoValidadas, - filename: prova.filename, - enemAreas: prova.enemAreas, - totalQuestaoCadastradas: prova.questoes.length, - createdAt: prova.createdAt, - }; - }), + data: provas.data.map((prova) => this.toProvaDTO(prova)), + }; + } + + public async getAllByCursinho( + cursinhoId: string, + param: GetAllInput, + ): Promise> { + const provas = await this.repository.getAll({ + ...param, + where: { cursinhoId }, + }); + return { + ...provas, + data: provas.data.map((prova) => this.toProvaDTO(prova)), }; } diff --git a/src/modules/prova/services/enem_service.spec.ts b/src/modules/prova/services/enem_service.spec.ts new file mode 100644 index 0000000..943c825 --- /dev/null +++ b/src/modules/prova/services/enem_service.spec.ts @@ -0,0 +1,72 @@ +import { EnemService } from './enem_service'; + +function makeService() { + const simuladoRepository = { + create: jest.fn().mockImplementation(async (s) => ({ ...s, _id: 's-new' })), + }; + const categoriaRepository = { + getByFilter: jest.fn().mockResolvedValue({ _id: 'cat-area' }), + }; + const provaRepository = {}; + const service = new EnemService( + simuladoRepository as any, + categoriaRepository as any, + provaRepository as any, + ); + return { service, simuladoRepository, categoriaRepository }; +} + +function makeProva() { + return { + ano: 2023, + criadorId: 'user-1', + cursinhoId: 'curs-1', + simulados: [] as any[], + categoria: { + nome: 'Enem Dia 1', + exame: { _id: 'e1', nome: 'ENEM' }, + }, + } as any; +} + +describe('EnemService.createSimuladoArea', () => { + it('propaga criadorId e cursinhoId da prova para o simulado de área', async () => { + const { service, simuladoRepository } = makeService(); + const prova = makeProva(); + + await service.createSimuladoArea(prova, 'Matemática'); + + const arg = simuladoRepository.create.mock.calls[0][0]; + expect(arg.nome).toBe('Enem Dia 1 2023 Matemática'); + expect(arg.criadorId).toBe('user-1'); + expect(arg.cursinhoId).toBe('curs-1'); + }); + + it('mantém cursinhoId null quando a prova não tem cursinho', async () => { + const { service, simuladoRepository } = makeService(); + const prova = makeProva(); + prova.cursinhoId = null; + + await service.createSimuladoArea(prova, 'Matemática'); + + const arg = simuladoRepository.create.mock.calls[0][0]; + expect(arg.criadorId).toBe('user-1'); + expect(arg.cursinhoId).toBeNull(); + }); +}); + +describe('EnemService.createSimuladoIdiomatica', () => { + it('propaga criadorId e cursinhoId nos simulados Inglês e Espanhol', async () => { + const { service, simuladoRepository } = makeService(); + const prova = makeProva(); + + await service.createSimuladoIdiomatica(prova); + + const args = simuladoRepository.create.mock.calls.map((c) => c[0]); + expect(args.length).toBeGreaterThanOrEqual(2); + for (const arg of args) { + expect(arg.criadorId).toBe('user-1'); + expect(arg.cursinhoId).toBe('curs-1'); + } + }); +}); diff --git a/src/modules/prova/services/enem_service.ts b/src/modules/prova/services/enem_service.ts index 6171e08..399f729 100644 --- a/src/modules/prova/services/enem_service.ts +++ b/src/modules/prova/services/enem_service.ts @@ -19,7 +19,6 @@ export class EnemService { public async createSimuladoIdiomatica(prova: Prova) { const mainName = `${prova.categoria.nome} ${prova.ano}`; - const exameId = prova.categoria.exame._id.toString(); await this.createSimuladoLinguagem(prova); prova.simulados.push( await this.simuladoRepository.create({ @@ -27,6 +26,8 @@ export class EnemService { categoria: prova.categoria, questoes: [], descricao: `${prova.categoria.exame.nome} Inglês`, + criadorId: prova.criadorId, + cursinhoId: prova.cursinhoId, }), ); prova.simulados.push( @@ -35,37 +36,40 @@ export class EnemService { categoria: prova.categoria, questoes: [], descricao: `${prova.categoria.exame.nome} Espanhol`, + criadorId: prova.criadorId, + cursinhoId: prova.cursinhoId, }), ); } public async createSimuladoLinguagem(prova: Prova) { - const mainName = `${prova.categoria.nome} ${prova.ano}`; - const exameId = prova.categoria.exame._id.toString(); prova.simulados.push( - await this.createSimuladoArea(mainName, EnemArea.Linguagens, exameId, 'Inglês'), + await this.createSimuladoArea(prova, EnemArea.Linguagens, 'Inglês'), ); prova.simulados.push( - await this.createSimuladoArea(mainName, EnemArea.Linguagens, exameId, 'Espanhol'), + await this.createSimuladoArea(prova, EnemArea.Linguagens, 'Espanhol'), ); } public async createSimuladoArea( - defaultName: string, + prova: Prova, nomeTipo: string, - exameId: string, complemento?: string, ) { + const mainName = `${prova.categoria.nome} ${prova.ano}`; + const exameId = prova.categoria.exame._id.toString(); const simuladoArea = new Simulado(); const categoria = await this.categoriaRepository.getByFilter({ nome: nomeTipo, exame: exameId, }); simuladoArea.categoria = categoria; - simuladoArea.nome = `${defaultName} ${nomeTipo}`; + simuladoArea.nome = `${mainName} ${nomeTipo}`; if (complemento) { simuladoArea.nome = `${simuladoArea.nome} ${complemento}`; } + simuladoArea.criadorId = prova.criadorId; + simuladoArea.cursinhoId = prova.cursinhoId; return await this.simuladoRepository.create(simuladoArea); }