[Janela Simulado] Card 02 · ms-simulado · PATCH /disponibilidade + filtro em getAvailable + gate em getToAnswer
Projeto: ms-simulado
Etapa: 5 (Janela Simulado — docs/simulado-janela-disponibilidade.md)
Estimativa: M
Bloqueia: Card 03
Bloqueado por: Card 01
Descrição
Ativa o comportamento: novo endpoint PATCH /v1/simulado/:id/disponibilidade pra admin setar as datas, filtro adicional em getAvailable respeitando a janela, gate em getToAnswer recusando com 403 se fora. POST /answer continua sem checagem — grace period intencional pra não cortar aluno que começou dentro da janela.
Escopo
Novo DTO UpdateDisponibilidadeDTO
export class UpdateDisponibilidadeDTO {
@IsOptional() @IsDate() @Type(() => Date)
disponivelDe?: Date | null;
@IsOptional() @IsDate() @Type(() => Date)
disponivelAte?: Date | null;
}
Aceita null explícito em qualquer um dos dois campos (pra "limpar" uma data setada).
Método SimuladoService.updateDisponibilidade
public async updateDisponibilidade(
id: string,
dto: UpdateDisponibilidadeDTO,
): Promise<Simulado> {
const simulado = await this.simuladoRepository.getById(id);
if (!simulado) throw new NotFoundException();
// Validação cross-field
if (dto.disponivelDe && dto.disponivelAte && dto.disponivelDe >= dto.disponivelAte) {
throw new BadRequestException('disponivelDe deve ser anterior a disponivelAte');
}
await this.simuladoRepository.model.updateOne(
{ _id: id },
{ $set: { disponivelDe: dto.disponivelDe, disponivelAte: dto.disponivelAte } },
);
return this.simuladoRepository.getById(id);
}
Endpoint no SimuladoController
@Patch(':id/disponibilidade')
@ApiResponse({ status: 200, type: Simulado })
public async updateDisponibilidade(
@Param('id') id: string,
@Body() dto: UpdateDisponibilidadeDTO,
): Promise<Simulado> {
return this.service.updateDisponibilidade(id, dto);
}
Filtro em SimuladoRepository.getAvailable
Alterar pra:
async getAvailable(categoriaId: string) {
const now = new Date();
return await this.model
.find({
categoria: categoriaId,
bloqueado: false,
$and: [
{ $or: [{ disponivelDe: null }, { disponivelDe: { $lte: now } }] },
{ $or: [{ disponivelAte: null }, { disponivelAte: { $gte: now } }] },
],
})
.select(['nome', '_id']);
}
O índice composto do Card 01 ({ categoria, bloqueado, disponivelDe, disponivelAte }) suporta essa query.
Gate em SimuladoService.getToAnswer
Antes de retornar o simulado, chamar isSimuladoAvailable. Se false, lançar:
import { isSimuladoAvailable, getAvailabilityStatus } from './availability';
public async getToAnswer(simuladoId: string): Promise<SimuladoAnswerDTOOutput> {
const simulado = await this.simuladoRepository.getById(simuladoId);
if (!simulado) return null;
if (!isSimuladoAvailable(simulado)) {
throw new HttpException(
{
message: 'Simulado fora da janela de disponibilidade',
status: getAvailabilityStatus(simulado),
},
HttpStatus.FORBIDDEN,
);
}
return this.GetSimulado(simuladoId);
}
O status detalhado no payload (bloqueado / antes_da_janela / depois_da_janela) permite ao frontend exibir mensagem específica.
POST /v1/simulado/answer — NÃO modificar
Decisão deliberada do doc §4. Aluno que carregou o simulado dentro da janela pode submeter fora. Grace period implícito. Confirmar no PR que nenhuma checagem foi adicionada nesse endpoint.
Critérios de aceitação
Risco
Médio. Pontos de atenção:
- Cross-field validation em
updateDisponibilidade: se malfeita, admin consegue setar de > ate e o isSimuladoAvailable sempre retorna false. Testar caso limite.
- Timezone: sempre gravar UTC no banco. Frontend converte de local pra UTC ao enviar; leitura vem UTC e frontend renderiza local. Sem
moment.tz ou similar, cuidado com Date naïve.
- Race entre listar e abrir: aluno vê simulado na lista (dentro da janela), clica, e nesse meio-tempo a janela fecha.
getToAnswer revalida com isSimuladoAvailable(now) → 403. UI trata (Card 04).
Observações
- O gate em
getToAnswer usa new Date() implicitamente (default do parâmetro now). Se algum teste precisa fixar tempo, chamar isSimuladoAvailable(simulado, fakeNow).
- Aluno que abriu simulado e ficou 30min respondendo — envio funciona (grace period). Se sair da tela e voltar 1h depois,
getToAnswer refaz o gate — cai em 403 se janela fechou. Comportamento correto.
Referências
- Doc principal:
docs/simulado-janela-disponibilidade.md §6 (endpoints) e §9 passos 3-6
- Grace period: doc §4 (tabela decisões) e §7.4
[Janela Simulado] Card 02 · ms-simulado ·
PATCH /disponibilidade+ filtro emgetAvailable+ gate emgetToAnswerProjeto: ms-simulado
Etapa: 5 (Janela Simulado —
docs/simulado-janela-disponibilidade.md)Estimativa: M
Bloqueia: Card 03
Bloqueado por: Card 01
Descrição
Ativa o comportamento: novo endpoint
PATCH /v1/simulado/:id/disponibilidadepra admin setar as datas, filtro adicional emgetAvailablerespeitando a janela, gate emgetToAnswerrecusando com 403 se fora.POST /answercontinua sem checagem — grace period intencional pra não cortar aluno que começou dentro da janela.Escopo
Novo DTO
UpdateDisponibilidadeDTOAceita
nullexplícito em qualquer um dos dois campos (pra "limpar" uma data setada).Método
SimuladoService.updateDisponibilidadeEndpoint no
SimuladoControllerFiltro em
SimuladoRepository.getAvailableAlterar pra:
O índice composto do Card 01 (
{ categoria, bloqueado, disponivelDe, disponivelAte }) suporta essa query.Gate em
SimuladoService.getToAnswerAntes de retornar o simulado, chamar
isSimuladoAvailable. Sefalse, lançar:O status detalhado no payload (
bloqueado/antes_da_janela/depois_da_janela) permite ao frontend exibir mensagem específica.POST /v1/simulado/answer— NÃO modificarDecisão deliberada do doc §4. Aluno que carregou o simulado dentro da janela pode submeter fora. Grace period implícito. Confirmar no PR que nenhuma checagem foi adicionada nesse endpoint.
Critérios de aceitação
PATCH /v1/simulado/:id/disponibilidadefunciona:null)nullexplícito → limpadisponivelDe >= disponivelAte) → 400GET /v1/simulado/available?categoria=Xnão retorna simulados fora da janelaGET /v1/simulado/available?categoria=Xretorna simulado comnull/null(compatibilidade)GET /v1/simulado/toanswer/:idretorna 403 com payload{ message, status }pra:bloqueado: true)disponivelDe > now)disponivelAte < now)POST /v1/simulado/answercontinua aceitando submissões independente da janelaRisco
Médio. Pontos de atenção:
updateDisponibilidade: se malfeita, admin consegue setarde > atee oisSimuladoAvailablesempre retornafalse. Testar caso limite.moment.tzou similar, cuidado comDatenaïve.getToAnswerrevalida comisSimuladoAvailable(now)→ 403. UI trata (Card 04).Observações
getToAnswerusanew Date()implicitamente (default do parâmetronow). Se algum teste precisa fixar tempo, chamarisSimuladoAvailable(simulado, fakeNow).getToAnswerrefaz o gate — cai em 403 se janela fechou. Comportamento correto.Referências
docs/simulado-janela-disponibilidade.md§6 (endpoints) e §9 passos 3-6