💡Nota
Por que esse módulo é diferente. Os módulos 02 a 06 revisaram e aprofundaram o que as duas já dominam. Esse aqui é onde a gente sai da Regional e sobe o nível. O cronograma reserva as semanas 2 e 3 de setembro pra isso: funcionalidades que não existiam no que elas fizeram na Regional, ou que existiam mais simples. Cada funcionalidade aqui é um exercício de juntar banco + back + front + UX numa coisa só que funciona ponta a ponta. É onde a teoria dos outros módulos vira sistema.
Tem um detalhe de ritmo que muda tudo: no treino, cada funcionalidade dessas ganha um ou dois dias. Na prova, várias delas juntas cabem em poucas horas. A ordem certa é exatamente essa: primeiro fazer bem, com calma, entendendo cada camada. Depois, na reta final, fazer rápido. Ninguém comprime o que ainda não domina. Esse módulo é o "fazer bem"; a seção 10 já começa a treinar o "fazer rápido".
Deixando claro (pra qualquer pessoa entender): o que está aqui é 🆕 novo em relação à Regional. Não é revisar o que elas já sabem, é construir capacidade nova em cima da base que elas já têm.
1. O método: de enunciado a funcionalidade em quatro passos 🆕
Antes de qualquer código, esse módulo ensina um método. É a habilidade que a UC1 chama de "planejar o desenvolvimento": pegar um pedaço de enunciado e transformar em plano concreto. Na Seletiva, isso é literalmente o Módulo A (Prova Surpresa, 2 horas): entregam um requisito que ninguém viu antes e esperam a feature funcionando. Quem tem método começa a codar em 20 minutos sabendo pra onde vai. Quem não tem, improvisa e refaz.
Os quatro passos, sempre na mesma ordem:
| Passo |
Pergunta que ele responde |
O que sai dele |
| 1. Requisito |
O que o enunciado exige de verdade? |
Frases grifadas, separadas em obrigatórias (must) e desejáveis (should) |
| 2. Entidades |
Que dados isso precisa guardar? |
Um CREATE TABLE ou ALTER TABLE + dicionário rápido dos campos novos |
| 3. Endpoints |
Que conversas front ↔ back isso cria? |
Linhas novas na tabela de rotas: verbo, URL, quem pode, status possíveis |
| 4. Telas |
Onde o usuário faz isso e o que ele vê? |
Rascunho da tela + os quatro estados dela (módulo 05, seção 5.5) |
A ordem importa. Dados antes de endpoints (o endpoint serve dado, então o dado precisa existir primeiro). Endpoints antes de telas (a tela consome o endpoint). É a mesma ordem de construção que vocês treinam desde o módulo 03: banco → back → front.
1.1 O método aplicado num exemplo real
Pega esse pedaço de enunciado, do jeito que ele apareceria na prova:
💡Nota
"O convidado deve poder responder ao convite através de um link, informando se comparecerá e quantos acompanhantes levará (no máximo 5). O total de pessoas confirmadas não pode ultrapassar a capacidade do evento. Seria desejável que o convidado visualizasse o prazo de resposta."
Passo 1 — Requisito. Grifando os verbos de obrigação:
- "deve poder responder através de um link" → must (a página é pública, sem login: o Caio não tem senha no sistema)
- "informando se comparecerá e quantos acompanhantes (máximo 5)" → must (dois campos + uma validação)
- "o total não pode ultrapassar a capacidade" → must (regra de negócio, e regra de negócio mora no back, módulo 04 seção 11)
- "seria desejável visualizar o prazo" → should (faz depois que os must estiverem prontos)
Passo 2 — Entidades. O que já existe no banco do módulo 03: convite tem codigo e status, convidado tem acompanhantes com CHECK <= 5, casamento tem capacidade_total. Ou seja: nenhuma tabela nova. Só entra um ALTER TABLE pra datas e prazo (seção 3.2). Perceber que a modelagem já cobre o requisito é resultado do passo, não falta de trabalho: você acabou de economizar meia hora.
Passo 3 — Endpoints. Duas linhas novas na tabela de rotas:
| Verbo |
URL |
Quem pode |
Status possíveis |
| GET |
/convites/:codigo |
Público |
200, 404 |
| POST |
/convites/:codigo/resposta |
Público |
200, 404, 409, 422 |
Passo 4 — Telas. Uma página nova, rsvp.html, fora da área logada. Estados: carregando, convite não encontrado, formulário pronto, respondido com sucesso, erro de rede. Rascunho de 2 minutos no papel: nome do casal em cima, dados do convite, dois botões grandes ("Confirmo" / "Não vou"), seletor de acompanhantes.
Pronto. Isso é um plano. As seções 3 e 4 constroem exatamente essa feature, camada por camada.
🛠️Gambiarra boa
🛠️ Gambiarra boa: o ritual dos 10 minutos. Sempre que cair uma feature nova (no treino ou na prova), os quatro passos vão pro papel antes do VS Code abrir. Dez minutos, no máximo. Parece perda de tempo com o relógio correndo; é o contrário: o papel vira o mapa e elimina o "e agora?" no meio do caminho. Na Prova Surpresa, esse papel é o cronograma pessoal das 2 horas.
🎯 Como isso cai na prova. O Módulo A é esse método cronometrado. O enunciado da Surpresa vem curto e às vezes em inglês (módulo 09 treina a leitura). A banca avalia o resultado, mas o julgamento (0 a 3) enxerga o processo: uma feature com banco certo, regra no lugar certo e tela com estados é a assinatura de quem planejou. A seção 10 monta o timebox completo.
2. Os três perfis, agora de ponta a ponta 🆕
Na Regional eram dois perfis. O Wedding Pass tem três, e o módulo 03 já deixou isso pronto no banco: usuario.perfil ENUM('admin','organizador','recepcao'). O módulo 04 (seção 9) construiu a ferramenta (middleware de autorização). O que esse módulo faz é o trabalho de projeto: decidir, rota por rota, quem pode o quê, e blindar tudo.
Colocando as personas do módulo 06 nos papéis: a Ada é admin, a Olívia é organizadora, a Renata é da recepção, e o Caio é convidado (nem conta tem: ele só existe do lado público).
2.1 A matriz de autorização completa
Essa tabela é o coração do módulo. Ela é a verdade sobre permissões no sistema, e é o tipo de artefato que vale mostrar pra banca no Módulo D:
| Rota |
Público |
Renata (recepção) |
Olívia (organizadora) |
Ada (admin) |
POST /auth/login |
✅ |
✅ |
✅ |
✅ |
GET /convidados (+ ?busca=) |
❌ |
✅ |
✅ |
✅ |
POST /convidados |
❌ |
❌ |
✅ |
✅ |
PUT /convidados/:id |
❌ |
❌ |
✅ |
✅ |
DELETE /convidados/:id |
❌ |
❌ |
❌ |
✅ |
POST /convidados/:id/checkin |
❌ |
✅ |
❌ |
✅ |
POST /checkins (por código, seção 5) |
❌ |
✅ |
❌ |
✅ |
GET /casamentos/:id/capacidade |
❌ |
✅ |
✅ |
✅ |
GET /casamentos/:id/mesas |
❌ |
✅ |
✅ |
✅ |
GET /casamentos/:id/dashboard |
❌ |
❌ |
✅ |
✅ |
POST /convites (enviar convite, seção 3.5) |
❌ |
❌ |
✅ |
✅ |
GET /convites/:codigo |
✅ |
✅ |
✅ |
✅ |
POST /convites/:codigo/resposta |
✅ |
✅ |
✅ |
✅ |
GET/POST/PUT/DELETE /usuarios |
❌ |
❌ |
❌ |
✅ |
2.2 Escada como ponto de partida, matriz como verdade
O jeito natural de pensar três perfis é a escada: admin pode tudo do organizador, que pode tudo da recepção. A escada é ótima pra começar (organiza o raciocínio e evita esquecer rota sem proteção). Mas olha a matriz de novo: ela tem uma linha que quebra a escada.
O check-in é da Renata e da Ada. A Olívia não faz check-in. Se a escada fosse lei, organizadora poderia tudo que a recepção pode. Só que a operação da porta é um papel operacional do dia do evento, com responsabilidade própria (o registrado_por da tabela checkin diz quem deixou a pessoa entrar). A organizadora gerencia a lista; quem opera a porta é a recepção. Separar os dois é uma decisão de projeto defensável, e defensável é a palavra favorita do Módulo D.
A lição que fica: escada pra rascunhar, matriz pra decidir. E toda exceção da matriz precisa de um porquê escrito, porque é exatamente nas exceções que a banca pergunta "por que isso aqui é assim?".
⚠️Pega-ratão
⚠️ Pega-ratão: implementar a autorização como if (perfil !== 'admin') espalhado pelos controllers. Vira caça ao tesouro pra achar onde cada regra mora, e uma rota esquecida passa aberta. A matriz vive num lugar só: a definição das rotas.
2.3 A matriz virando código
Recapitulando a ferramenta do módulo 04 (seção 9): autenticar valida o token e põe o usuário no request (401 se não tem ou tá inválido); permitir(...) confere o perfil (403 se não pode). A matriz inteira vira uma linha por rota:
Node/Express:
// rotas/convidados.js — a matriz da seção 2.1 virando código, linha a linha
router.get('/', autenticar, listarConvidados); // qualquer perfil logado
router.post('/', autenticar, permitir('organizador', 'admin'), criarConvidado);
router.put('/:id', autenticar, permitir('organizador', 'admin'), editarConvidado);
router.delete('/:id', autenticar, permitir('admin'), removerConvidado);
router.post('/:id/checkin', autenticar, permitir('recepcao', 'admin'), fazerCheckin);
PHP:
// api/convidados.php — mesma matriz, roteamento manual
$usuario = exigir_login($pdo); // devolve 401 e encerra se o token nao vale
switch ("$metodo $recurso") {
case 'GET convidados':
// qualquer perfil logado passa
listar_convidados($pdo);
break;
case 'POST convidados':
exigir_perfil($usuario, ['organizador', 'admin']); // 403 se nao pode
criar_convidado($pdo, $dados);
break;
case 'DELETE convidados':
exigir_perfil($usuario, ['admin']);
remover_convidado($pdo, $id);
break;
}
Se os nomes das funções na API de vocês estiverem diferentes, sem problema. O contrato é o que importa: 401 pra quem não provou quem é, 403 pra quem provou mas não pode (módulo 04, seção 9 tem a distinção completa).
2.4 O front acompanha a matriz (mas não substitui ela)
O módulo 05 (seção 8) montou a guarda de página e o menu por perfil. Com três perfis, o mapa fica assim: login redireciona a Renata direto pro check-in (tela dela), a Olívia pro dashboard, a Ada pra onde quiser (menu completo). Botões que o perfil não pode usar nem aparecem (esconder é UX: menos ruído pra persona apressada do módulo 06).
E a frase que resume a divisão de trabalho continua valendo: front esconde, back barra. Esconder o botão de excluir não impede um DELETE disparado pelo Postman. Quem barra é o middleware.
2.5 O teste do token fraco (o favorito da banca)
O teste que a banca faz, e que vocês vão fazer antes dela:
- Loga com a Renata no Postman. Guarda o token dela.
- Chama
DELETE /convidados/1 com esse token no header.
- Resposta esperada: 403 com o corpo padrão de erro (
{ "erro": ... }, módulo 04 seção 12). Se vier 200, o controle é de fachada: o front escondia, mas o back não barrava.
- Repete pra cada ❌ da linha da Renata na matriz. Depois com o token da Olívia (o dela tem que levar 403 no check-in e no
DELETE).
São uns 2 minutos por rota com o Postman organizado (módulo 04, seção 15: salva essas requisições numa pasta "testes de autorização" da collection). Rodar essa bateria depois de qualquer mexida nas rotas vira hábito.
🎯Foco de prova
🎯 Como isso cai na prova. Cada perfil vendo e fazendo só o que pode é critério objetivo (sim/não, rota a rota). O julgamento entra na qualidade das exceções: a banca pergunta por que a organizadora não faz check-in, e a resposta pronta ("papel operacional da porta, com auditoria própria") é diferença entre 2 e 3. Guarda essa resposta no banco de porquês do Módulo D (módulo 10).
3. O fluxo convite → RSVP → follow-up 🆕
Essa é a jornada central do Wedding Pass, e é a funcionalidade mais completa do sistema: começa com a Olívia enviando um convite, passa pelo Caio respondendo por um link público, e volta pra Olívia acompanhar quem ainda não respondeu. Um fluxo, três atores, as três camadas envolvidas o tempo todo.
⚠️Pega-ratão
⚠️ Pega-ratão (o mesmo da v1 do sistema de vocês, e continua valendo): tratar convite, confirmação e acompanhamento como três coisas soltas. Elas são um fluxo: o mesmo convidado, o mesmo código, mudando de estado. Modelar como ciclo de estados, não como telas desconectadas, é o que faz o sistema parecer inteiro.
3.1 O ciclo de estados
Todo convite vive nesse ciclo:
┌──── sim ────▶ confirmado ──── check-in ────▶ presente
pendente ────┤ (linha na tabela checkin)
└──── não ────▶ recusado
Três decisões de projeto escondidas nesse desenho:
- Os estados moram no banco, não na tela.
convite.status é o ENUM do módulo 03 (pendente, confirmado, recusado). "Presente" não é um quarto valor do ENUM: é a existência de uma linha em checkin. Por quê? Porque check-in tem dados próprios (quando entrou, quem registrou) e uma constraint própria (a UNIQUE que bloqueia dupla entrada). Estado com dado junto vira tabela, não coluna.
- Transições são validadas no back. Responder um convite que não está
pendente é 409. Fazer check-in duas vezes é 409. O front mostra bonito, mas quem impede transição ilegal é a API.
- Cada transição carimba um horário:
respondido_em no convite, registrado_em no checkin. Auditoria barata que alimenta o follow-up e o dashboard de graça.
3.2 O código único (a "senha" do convidado)
Cada convite tem um código de 8 caracteres, único no sistema. Ele é o que o Caio recebe no link: quem tem o código, responde o convite. Isso o torna, na prática, a senha do convidado. Duas consequências:
Não pode ser adivinhável. Se o código fosse o id sequencial (/convites/1, /convites/2...), qualquer pessoa trocaria o número na URL e responderia o convite dos outros. Enumerável = quebrado.
Não pode ser ambíguo. O código vai ser lido em voz alta na porta do evento e digitado pela Renata. Então o alfabeto tira os caracteres que se confundem: sem 0/O, sem 1/I/l.
Node:
const crypto = require('crypto');
// 32 simbolos, sem os ambiguos (0/O, 1/I/l)
const ALFABETO = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
function gerarCodigo(tamanho = 8) {
let codigo = '';
for (const byte of crypto.randomBytes(tamanho)) {
codigo += ALFABETO[byte % ALFABETO.length];
}
return codigo;
}
PHP:
function gerar_codigo(int $tamanho = 8): string {
$alfabeto = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
$codigo = '';
foreach (str_split(random_bytes($tamanho)) as $byte) {
$codigo .= $alfabeto[ord($byte) % strlen($alfabeto)];
}
return $codigo;
}
Nos dois casos a fonte é o gerador criptográfico do sistema (crypto.randomBytes / random_bytes), não Math.random()/rand(), que são previsíveis.
E se dois códigos colidirem? Com 32 símbolos e 8 posições são 32⁸ combinações (mais de 1 trilhão): colisão é quase impossível, mas "quase" não é argumento de engenharia. A rede de segurança é a constraint UNIQUE no convite.codigo (módulo 03): se colidir, o INSERT falha, e o código tenta de novo:
// o UNIQUE do banco e a rede; o retry e o trapezista se levantando
for (let tentativa = 1; tentativa <= 3; tentativa++) {
try {
const codigo = gerarCodigo();
await conexao.execute(
'INSERT INTO convite (convidado_id, codigo) VALUES (?, ?)',
[convidadoId, codigo]
);
return codigo;
} catch (erro) {
if (erro.code !== 'ER_DUP_ENTRY') throw erro;
// colidiu (raro demais): gera outro e tenta de novo
}
}
throw new Error('Nao consegui gerar um codigo unico');
Detalhe fino: ER_DUP_ENTRY aqui também dispara se o convidado já tem convite (a UNIQUE de convidado_id). Por isso o fluxo real do POST /convites (seção 3.5) confere antes se já existe convite pro convidado: se existe, é reenvio, não convite novo.
A migração desse módulo. O fluxo precisa de três campos que o banco do módulo 03 ainda não tem:
ALTER TABLE convite
ADD COLUMN enviado_em DATETIME NULL,
ADD COLUMN respondido_em DATETIME NULL,
ADD COLUMN prazo_resposta DATE NULL;
🛠️Gambiarra boa
🛠️ Gambiarra boa: o arquivo migrations.sql. Banco evolui por ALTER, não por DROP + recriar (dropar mata os dados de teste que vocês cadastraram, e na prova mata o seed). Todo ALTER novo entra no fim de um arquivo migrations.sql versionado no Git. Além de ser prática real de mercado, é evidência linda pra banca: o histórico mostra o banco evoluindo junto com as features.
3.3 Os endpoints públicos do RSVP
Aqui aparece uma categoria nova de rota: pública de verdade. O Caio não tem login; o código no link é a credencial dele. As duas rotas ficam fora do middleware autenticar.
Buscar o convite (o que a página pública carrega):
// rotas/publico.js — sem autenticar: o codigo E a credencial
router.get('/convites/:codigo', async (req, res, next) => {
try {
const [linhas] = await pool.execute(
`SELECT c.codigo, c.status, c.prazo_resposta,
cv.nome AS convidado, cv.acompanhantes,
ca.nome AS casamento, ca.data_evento, ca.local
FROM convite c
JOIN convidado cv ON cv.id = c.convidado_id
JOIN casamento ca ON ca.id = cv.casamento_id
WHERE c.codigo = ?`,
[req.params.codigo]
);
if (linhas.length === 0) {
return res.status(404).json({ erro: 'Convite não encontrado' });
}
res.json(linhas[0]);
} catch (erro) { next(erro); }
});
Responder o convite (o endpoint mais denso do sistema). Ele valida entrada, confere estado, confere prazo, e aplica a regra de capacidade dentro de uma transação. Vale ler com calma porque ele reúne meio módulo 04 num lugar só:
router.post('/convites/:codigo/resposta', async (req, res, next) => {
const { resposta, acompanhantes = 0 } = req.body;
// 1. Validar a entrada (422 antes de tocar no banco)
const detalhes = [];
if (!['confirmado', 'recusado'].includes(resposta)) {
detalhes.push('resposta deve ser "confirmado" ou "recusado"');
}
if (!Number.isInteger(acompanhantes) || acompanhantes < 0 || acompanhantes > 5) {
detalhes.push('acompanhantes deve ser um número inteiro entre 0 e 5');
}
if (detalhes.length > 0) {
return res.status(422).json({ erro: 'Dados inválidos', detalhes });
}
const conexao = await pool.getConnection();
try {
await conexao.beginTransaction();
// 2. Travar o convite e conferir o estado atual
const [convites] = await conexao.execute(
`SELECT c.id, c.status, c.prazo_resposta,
cv.id AS convidado_id, cv.casamento_id
FROM convite c
JOIN convidado cv ON cv.id = c.convidado_id
WHERE c.codigo = ?
FOR UPDATE`,
[req.params.codigo]
);
if (convites.length === 0) {
await conexao.rollback();
return res.status(404).json({ erro: 'Convite não encontrado' });
}
const convite = convites[0];
if (convite.status !== 'pendente') {
await conexao.rollback();
return res.status(409).json({ erro: 'Convite já respondido' });
}
if (convite.prazo_resposta && new Date(convite.prazo_resposta) < new Date()) {
await conexao.rollback();
return res.status(409).json({ erro: 'Prazo de resposta encerrado' });
}
// 3. Se for "sim", conferir a capacidade com a linha do casamento travada
if (resposta === 'confirmado') {
const [casamentos] = await conexao.execute(
`SELECT ca.capacidade_total,
COALESCE((SELECT SUM(1 + cv2.acompanhantes)
FROM convidado cv2
JOIN convite c2 ON c2.convidado_id = cv2.id
WHERE cv2.casamento_id = ca.id
AND c2.status = 'confirmado'), 0) AS ja_confirmados
FROM casamento ca
WHERE ca.id = ?
FOR UPDATE`,
[convite.casamento_id]
);
const { capacidade_total, ja_confirmados } = casamentos[0];
if (Number(ja_confirmados) + 1 + acompanhantes > capacidade_total) {
await conexao.rollback();
return res.status(409).json({ erro: 'Capacidade do evento esgotada' });
}
}
// 4. Efetivar as duas escritas e commitar
await conexao.execute(
'UPDATE convite SET status = ?, respondido_em = NOW() WHERE id = ?',
[resposta, convite.id]
);
await conexao.execute(
'UPDATE convidado SET acompanhantes = ? WHERE id = ?',
[acompanhantes, convite.convidado_id]
);
await conexao.commit();
res.json({ mensagem: 'Resposta registrada', status: resposta });
} catch (erro) {
await conexao.rollback();
next(erro);
} finally {
conexao.release();
}
});
O que o FOR UPDATE está serializando aqui. Imagina o evento com um lugar sobrando e dois convidados clicando "Confirmo" no mesmo segundo. Sem trava, os dois SELECTs contam a mesma ocupação, os dois passam na regra, os dois confirmam: capacidade estourada. Com o FOR UPDATE na linha do casamento, o segundo request fica esperando o primeiro commitar; quando ele finalmente conta, a confirmação do primeiro já está lá, e ele leva o 409. É a mesma lógica de condição de corrida do módulo 04 (seção 11.2), com outra roupa: lá o UPDATE atômico resolvia numa instrução só; aqui a decisão precisa de conta no meio, então trava-se a linha-pai, conta, decide. Guarda esse molde: travar a linha-pai, contar, decidir. Ele volta duas vezes nesse módulo.
Sobre o prazo estourado: a resposta 409 com mensagem clara resolve. Existe um status mais fino pra "isso existia e expirou" (410 Gone); citar ele na apresentação mostra repertório, mas usar 409 padronizado é decisão perfeitamente defensável.
A versão PHP do mesmo endpoint (mesmo raciocínio, sintaxe PDO; as consultas SQL são idênticas):
// publico/resposta.php
$pdo->beginTransaction();
try {
$busca = $pdo->prepare(
'SELECT c.id, c.status, c.prazo_resposta,
cv.id AS convidado_id, cv.casamento_id
FROM convite c JOIN convidado cv ON cv.id = c.convidado_id
WHERE c.codigo = ? FOR UPDATE'
);
$busca->execute([$codigo]);
$convite = $busca->fetch();
if (!$convite) { $pdo->rollBack(); responder(404, ['erro' => 'Convite não encontrado']); }
if ($convite['status'] !== 'pendente') { $pdo->rollBack(); responder(409, ['erro' => 'Convite já respondido']); }
if ($resposta === 'confirmado') {
$cap = $pdo->prepare('...mesma consulta de capacidade do Node, com FOR UPDATE...');
$cap->execute([$convite['casamento_id']]);
$c = $cap->fetch();
if ($c['ja_confirmados'] + 1 + $acompanhantes > $c['capacidade_total']) {
$pdo->rollBack();
responder(409, ['erro' => 'Capacidade do evento esgotada']);
}
}
$pdo->prepare('UPDATE convite SET status = ?, respondido_em = NOW() WHERE id = ?')
->execute([$resposta, $convite['id']]);
$pdo->prepare('UPDATE convidado SET acompanhantes = ? WHERE id = ?')
->execute([$acompanhantes, $convite['convidado_id']]);
$pdo->commit();
responder(200, ['mensagem' => 'Resposta registrada', 'status' => $resposta]);
} catch (Throwable $e) {
$pdo->rollBack();
responder(500, ['erro' => 'Erro interno']);
}
(responder() é o helper de resposta JSON do módulo 04: seta o http_response_code, imprime o JSON e encerra.)
3.4 A página pública rsvp.html
A página que o Caio abre pelo link rsvp.html?codigo=A7XK2MQP. Primeira coisa: ler o código da URL.
// front/js/rsvp.js
const codigo = new URLSearchParams(location.search).get('codigo');
const API = 'http://localhost:3000';
⚠️Pega-ratão
⚠️ Pega-ratão importante: essa página NÃO usa o helper api() do módulo 05 (seção 5.6). O helper foi desenhado pra área logada: ele anexa o token e, no 401, limpa o storage e redireciona pro login. O Caio não tem login. Se a página pública usar o helper, um erro qualquer pode jogar o convidado numa tela de login que não é pra ele. Aqui é fetch direto, e esse porquê é ótimo pro Módulo D: saber quando não usar a própria ferramenta é maturidade de projeto.
O carregamento com os quatro estados do módulo 05 (seção 5.5), mais um quinto ("respondido"):
async function carregarConvite() {
mostrarEstado('carregando');
try {
const resposta = await fetch(`${API}/convites/${codigo}`);
if (resposta.status === 404) return mostrarEstado('nao-encontrado');
if (!resposta.ok) throw new Error();
const convite = await resposta.json();
preencherTela(convite);
// convite ja respondido? mostra o estado final, sem formulario
mostrarEstado(convite.status === 'pendente' ? 'formulario' : 'ja-respondido');
} catch {
mostrarEstado('erro'); // servidor fora, sem internet...
}
}
O envio da resposta, com os hábitos do módulo 05 (botão desabilitado durante o request, mensagens por status):
async function enviarResposta(valor) {
const botoes = document.querySelectorAll('.acoes button');
botoes.forEach(b => (b.disabled = true));
try {
const resposta = await fetch(`${API}/convites/${codigo}/resposta`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
resposta: valor,
acompanhantes: Number(document.querySelector('#acompanhantes').value)
})
});
const dados = await resposta.json();
if (resposta.status === 409) return avisar(dados.erro, 'alerta');
if (!resposta.ok) return avisar('Não deu pra registrar. Tenta de novo.', 'erro');
mostrarEstado('respondido'); // tela de sucesso, com o resumo da resposta
} catch {
avisar('Sem conexão com o servidor.', 'erro');
} finally {
botoes.forEach(b => (b.disabled = false));
}
}
Repara que o 409 chega com a mensagem específica do back ("Capacidade do evento esgotada", "Convite já respondido", "Prazo de resposta encerrado") e a página só exibe. Mensagem de regra de negócio nasce no back, que é onde a regra mora.
E os acompanhantes entram por um <select> de 0 a 5, não por campo livre:
<select id="acompanhantes">
<option value="0" selected>Sem acompanhantes</option>
<option value="1">1 acompanhante</option>
<option value="2">2 acompanhantes</option>
<option value="3">3 acompanhantes</option>
<option value="4">4 acompanhantes</option>
<option value="5">5 acompanhantes</option>
</select>
É a validação por design do módulo 06: o campo que só aceita valores válidos elimina a mensagem de erro. (O back valida mesmo assim, seção 3.3, porque request não nasce só de tela.)
3.5 Enviar o convite: e-mail de verdade e a gambiarra oficial
O POST /convites é da Olívia (matriz da seção 2.1). O fluxo dele: conferir se o convidado já tem convite (a UNIQUE de convidado_id garante no banco; reenvio reaproveita o mesmo código e só atualiza enviado_em), gerar o código com retry (seção 3.2), gravar com prazo_resposta padrão de 7 dias antes do evento (DATE_SUB(data_evento, INTERVAL 7 DAY)), e entregar o link pro convidado.
Entregar como? Duas respostas, e as duas precisam estar no bolso.
O envio de e-mail de verdade. Em Node, o padrão de mercado é o nodemailer:
const nodemailer = require('nodemailer');
const transporte = nodemailer.createTransport({
host: 'smtp.ethereal.email', // no treino: Ethereal/Mailtrap; na prova: o que a infra der
port: 587,
auth: { user: 'usuario-de-teste', pass: 'senha-de-teste' }
});
async function enviarConvitePorEmail(convidado, codigo) {
await transporte.sendMail({
from: '"Wedding Pass" <convites@weddingpass.com>',
to: convidado.email,
subject: 'Você foi convidado! Confirme sua presença',
html: `<p>Oi, ${convidado.nome}!</p>
<p><a href="http://localhost:5500/rsvp.html?codigo=${codigo}">
Clique aqui pra responder ao convite</a></p>`
});
}
Em PHP, o equivalente é o PHPMailer (mesma estrutura: configura SMTP, monta destinatário/assunto/corpo, send()). Existe o mail() nativo, mas ele depende de servidor de e-mail configurado na máquina e falha em silêncio quando não tem: em ambiente de prova, é armadilha. Se for PHP, PHPMailer com SMTP de teste.
No treino, Ethereal (Node, gera conta fake na hora e mostra o e-mail num painel web) e Mailtrap (caixa de entrada de teste, serve pras duas stacks) deixam vocês verem o e-mail chegando sem mandar nada pra ninguém de verdade.
🛠️Gambiarra boa
🛠️ Gambiarra boa (e oficial): o botão "copiar link". E-mail depende de infra que a prova pode não ter (SMTP liberado, internet). O fluxo à prova de ambiente: o botão "Enviar convite" gera o código, grava enviado_em, e abre um modal com o link rsvp.html?codigo=X e um botão que copia (navigator.clipboard.writeText(link)) pra Olívia colar no WhatsApp do convidado. O fluxo de convite fica completo e demonstrável sem depender de nada externo.
A regra de decisão: se o enunciado diz "enviar por e-mail", implementa o envio (e mantém o copiar-link como plano B visível). Se diz só "convite com link", o copiar-link é a solução, não um atalho.
async function copiarLink(codigo) {
const link = `${location.origin}/rsvp.html?codigo=${codigo}`;
await navigator.clipboard.writeText(link);
avisar('Link copiado! Cola no WhatsApp do convidado.', 'sucesso');
}
3.6 Follow-up: a lista de trabalho da Olívia
Depois dos convites na rua, a pergunta da Olívia é "quem falta?". Follow-up é isso: consultas por status bem apresentadas.
O panorama (alimenta o dashboard da seção 6 também):
-- LEFT JOIN pra contar tambem convidado que ainda nem tem convite
SELECT COALESCE(c.status, 'sem convite') AS situacao,
COUNT(*) AS total
FROM convidado cv
LEFT JOIN convite c ON c.convidado_id = cv.id
WHERE cv.casamento_id = ?
GROUP BY situacao;
A lista de pendentes com prazo, que é a tela de ação:
SELECT cv.nome, c.codigo, c.enviado_em, c.prazo_resposta,
DATEDIFF(c.prazo_resposta, CURDATE()) AS dias_restantes
FROM convite c
JOIN convidado cv ON cv.id = c.convidado_id
WHERE cv.casamento_id = ?
AND c.status = 'pendente'
ORDER BY c.prazo_resposta;
DATEDIFF devolve a diferença em dias: positivo é prazo correndo, negativo é prazo estourado. Essa conta vem pronta do banco; o front não fica fazendo matemática de data (fuso e formato de data em JS são um campo minado que não vale a pena atravessar na prova).
A tela: filtros por status (botõezinhos "Todos / Pendentes / Confirmados / Recusados" refazendo o render, padrão do módulo 05 seção 9), badge de dias restantes com a cor semântica dos tokens do módulo 06 (verde folgado, amarelo com 3 dias ou menos, vermelho estourado), e em cada linha os botões "copiar link" e "reenviar". Nada de tela nova do zero: é o render() de lista que vocês já dominam, com dados novos.
4. Capacidade por mesa 🆕
A tabela mesa existe desde o módulo 03 (com capacidade e a UNIQUE de casamento_id + numero), mas até agora era só modelo. Agora vira feature: a Olívia distribui os convidados nas mesas, e nenhuma mesa pode passar da capacidade dela.
4.1 A consulta de ocupação
SELECT m.id, m.numero, m.capacidade,
COALESCE(SUM(1 + cv.acompanhantes), 0) AS ocupados
FROM mesa m
LEFT JOIN convidado cv ON cv.mesa_id = m.id
WHERE m.casamento_id = ?
GROUP BY m.id, m.numero, m.capacidade
ORDER BY m.numero;
Cada convidado ocupa 1 + acompanhantes lugares. O LEFT JOIN + COALESCE garante que mesa vazia aparece com ocupados = 0 em vez de sumir do resultado (clássico do módulo 03).
Uma decisão de projeto escondida na régua: conta quem? Se a régua for só quem confirmou, a variante entra com CASE:
-- variante: so confirmados ocupam lugar (exige juntar o convite)
COALESCE(SUM(CASE WHEN c.status = 'confirmado'
THEN 1 + cv.acompanhantes END), 0) AS ocupados
Recomendação: todo convidado alocado reserva o lugar, confirmado ou não. Defesa: a mesa é planejamento físico; se a Olívia sentou o convidado na mesa 3, o lugar tá comprometido até ela tirar. Contar só confirmados faz a mesa "caber" no sistema e estourar na vida real se todo mundo responder sim. As duas réguas são defensáveis; o que não pode é não saber qual você escolheu. (Se o enunciado definir, o enunciado manda.)
4.2 A regra no back: terceira aparição do molde
Alocar convidado em mesa é um PUT/PATCH no convidado com mesa_id. E aí, de novo, dois requests simultâneos podem disputar o último lugar da mesa. A esta altura vocês já sabem o molde: travar a linha-pai, contar, decidir.
// dentro de transacao, no PUT /convidados/:id quando vem mesa_id
const [mesas] = await conexao.execute(
`SELECT m.numero, m.capacidade,
COALESCE((SELECT SUM(1 + cv.acompanhantes)
FROM convidado cv
WHERE cv.mesa_id = m.id
AND cv.id <> ?), 0) AS ocupados
FROM mesa m
WHERE m.id = ?
FOR UPDATE`,
[convidadoId, mesaId] // cv.id <> ? : nao conta o proprio convidado trocando de mesa
);
const mesa = mesas[0];
if (Number(mesa.ocupados) + 1 + acompanhantes > mesa.capacidade) {
await conexao.rollback();
return res.status(409).json({
erro: `Mesa ${mesa.numero} cheia (${mesa.ocupados}/${mesa.capacidade})`
});
}
// cabe: segue o UPDATE do convidado e commita
Contando as aparições do molde: capacidade do evento no RSVP (travou casamento), capacidade da mesa aqui (travou mesa). A terceira "capacidade" do sistema, o check-in, resolve diferente de propósito, e a seção 5 explica por quê. Saber os dois jeitos e quando usar cada um é conversa de nota 3 no Módulo D.
4.3 A tela de mesas
Dois pedaços de front, os dois reaproveitando o módulo 05:
O select de alocação conta a ocupação pra Olívia na hora da escolha, e desabilita mesa cheia (validação por design de novo):
select.innerHTML = mesas.map(m => {
const livres = m.capacidade - m.ocupados;
return `<option value="${m.id}" ${livres <= 0 ? 'disabled' : ''}>
Mesa ${m.numero} — ${livres} de ${m.capacidade} livres
</option>`;
}).join('');
O painel de mesas: um grid repeat(auto-fit, minmax(220px, 1fr)) (módulo 05, seção 3.6) com um card por mesa e uma barrinha de ocupação (uma div com width em porcentagem, cor semântica virando âmbar quando encosta na lotação). É visual, é rápido de fazer com os tokens prontos, e é o tipo de tela que segura o olhar da banca na demo.
5. Check-in por código, blindado nas três camadas 🆕
No dia do casamento, a Renata na porta. Fila andando. O fluxo dela: o convidado mostra o código (impresso, no celular), ela digita ou escaneia, o sistema mostra quem é pra conferência (nome, mesa, acompanhantes, situação do convite), ela confirma a entrada. Verde grande de "bem-vindo". E se o código entrar de novo: bloqueio, com horário da primeira entrada.
A Regional tinha check-in; a versão da Seletiva é por código e blindada de ponta a ponta. Esse é o assunto que mais concentra pontos por linha de código do sistema inteiro.
5.1 As duas portas da mesma sala
O módulo 04 (seção 2.4) já tinha o POST /convidados/:id/checkin (check-in pelo id, usado na listagem de convidados). Entra agora o POST /checkins com corpo { "codigo": "A7XK2MQP" }: a porta rápida da Renata. Duas rotas, a mesma tabela checkin, a mesma constraint por trás. Nenhuma regra duplicada: as duas portas dão na mesma sala.
5.2 As três defesas
Defesa 1, o banco (a verdade final). A constraint do módulo 03:
CONSTRAINT uq_checkin_convidado UNIQUE (convidado_id)
Aconteça o que acontecer nas outras camadas, o banco não aceita duas linhas de check-in pro mesmo convidado. Essa é a garantia que não depende de ninguém se comportar.
Defesa 2, o back: INSERT direto e captura do erro. O jeito ingênuo é "SELECT pra ver se já tem, depois INSERT". Tem uma janela de corrida aí: dois requests fazem o SELECT juntos, ninguém acha nada, os dois INSERTam... e o segundo estoura na constraint com um erro 500 feio. O jeito robusto abraça a constraint: manda o INSERT direto e trata a duplicidade como resposta esperada.
Node:
router.post('/checkins', autenticar, permitir('recepcao', 'admin'), async (req, res, next) => {
try {
const { codigo } = req.body;
// 1. Achar o convidado pelo codigo (e trazer o contexto pra tela de conferencia)
const [convidados] = await pool.execute(
`SELECT cv.id, cv.nome, cv.acompanhantes, m.numero AS mesa, c.status
FROM convite c
JOIN convidado cv ON cv.id = c.convidado_id
LEFT JOIN mesa m ON m.id = cv.mesa_id
WHERE c.codigo = ?`,
[codigo]
);
if (convidados.length === 0) {
return res.status(404).json({ erro: 'Código não encontrado' });
}
const convidado = convidados[0];
// 2. INSERT direto: o banco decide quem chegou primeiro
try {
await pool.execute(
'INSERT INTO checkin (convidado_id, registrado_por) VALUES (?, ?)',
[convidado.id, req.usuario.id]
);
} catch (erro) {
if (erro.code === 'ER_DUP_ENTRY') {
const [[registro]] = await pool.execute(
'SELECT registrado_em FROM checkin WHERE convidado_id = ?',
[convidado.id]
);
const hora = new Date(registro.registrado_em)
.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
return res.status(409).json({
erro: 'Convidado já fez check-in',
detalhes: [`Entrou às ${hora}`]
});
}
throw erro; // qualquer outro erro segue pro handler padrao
}
res.status(201).json({ mensagem: `Bem-vindo, ${convidado.nome}!`, convidado });
} catch (erro) { next(erro); }
});
PHP (o miolo da captura):
try {
$pdo->prepare('INSERT INTO checkin (convidado_id, registrado_por) VALUES (?, ?)')
->execute([$convidadoId, $usuario['id']]);
} catch (PDOException $e) {
if ($e->errorInfo[1] == 1062) { // 1062 = codigo MySQL de entrada duplicada
$busca = $pdo->prepare('SELECT registrado_em FROM checkin WHERE convidado_id = ?');
$busca->execute([$convidadoId]);
$registro = $busca->fetch();
responder(409, [
'erro' => 'Convidado já fez check-in',
'detalhes' => ['Entrou às ' . date('H:i', strtotime($registro['registrado_em']))]
]);
}
throw $e;
}
Repara numa coisa: sem transação. É um INSERT só, e a constraint é o árbitro: se dois requests chegarem colados, o banco aceita um e rejeita o outro, atomicamente. Compara com os dois usos do FOR UPDATE (RSVP e mesa): lá a decisão precisava de uma conta antes da escrita, então travava-se pra contar. Aqui a regra é "só pode existir um", que é exatamente o que uma UNIQUE declara. Duas ferramentas pra mesma família de problema: quando a regra é contável, transação com trava; quando a regra é unicidade, constraint e captura. Essa frase, dita com as próprias palavras no Módulo D, é diferença entre 2 e 3.
Defesa 3, o front. O painel da Renata (módulo 05, seções 6 e 7.3): resposta 201 pinta o painel verde com nome e mesa; 409 pinta vermelho com a mensagem e o horário que veio em detalhes. O campo de código com autofocus e limpo após cada operação, porque a persona da Renata (módulo 06) opera com fila na frente: cada segundo de fricção é fila parada.
5.3 Provar a corrida em 30 segundos
A banca testa dupla entrada abrindo duas abas e mandando o mesmo código quase junto. Vocês vão provar isso antes, direto no console do navegador:
const opts = {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ codigo: 'A7XK2MQP' })
};
const respostas = await Promise.all([
fetch('http://localhost:3000/checkins', opts),
fetch('http://localhost:3000/checkins', opts)
]);
console.log(respostas.map(r => r.status));
// esperado: [201, 409] (em qualquer ordem). NUNCA [201, 201].
Dois requests disparados no mesmo tick. Se voltar [201, 409], a blindagem tá de pé. Se voltar [201, 201], tem duas linhas no banco e um problema pra resolver agora (provavelmente a constraint não existe: SHOW CREATE TABLE checkin pra conferir). Esse teste entra no hábito, junto com a bateria do token fraco.
5.4 E se o convite não estiver confirmado?
O convidado chega na porta com convite pendente (nunca respondeu) ou recusado (disse que não ia e apareceu). Entra? Três respostas possíveis: barrar, permitir direto, ou permitir com aviso (painel amarelo: "Convite não confirmado. Registrar entrada mesmo assim?", e a Renata decide na hora com um clique).
Recomendação: permitir com aviso. Defesa: na porta de um casamento de verdade, ninguém barra a tia que esqueceu de responder; o sistema informa e a pessoa decide. É a melhor história pro Módulo D porque mostra que vocês pensaram no contexto de uso, não só na regra. Como sempre: se o enunciado definir o comportamento, o enunciado manda.
🛠️Gambiarra boa
🛠️ Gambiarra boa: o QR code. O código impresso no convite pode virar QR. Custo mínimo, efeito grande na demo. Dois caminhos: a lib qrcode do npm, local (QRCode.toDataURL(link) devolve a imagem pronta pra pôr num <img>), ou o serviço https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=... numa linha de HTML (depende de internet: confirmar se a prova tem antes de contar com isso). Ler QR pela câmera (API BarcodeDetector, uns 15 linhas) só se sobrar tempo de verdade: é cereja, não bolo. No Módulo D, o QR é o tipo de "a mais" que mostra visão de produto real.
E o registrado_por gravado em cada check-in fecha a história: é auditoria ("quem deixou essa pessoa entrar, e quando?"), e é mais um porquê pronto pro banco do Módulo D: é por causa dele que check-in é da recepção logada, e não um endpoint aberto.
6. Dashboard: agregações reais, gráfico e alerta de 90% 🆕
A tela da Olívia. Ela abre e enxerga o evento de relance: quantos convidados, quantos confirmaram, quantos já chegaram, e o alerta quando a ocupação passa de 90% da capacidade.
6.1 Um endpoint, uma viagem
A tentação é o front fazer cinco fetches (convidados, confirmados, presentes, mesas...). O desenho certo é um endpoint que devolve o dashboard inteiro: GET /casamentos/:id/dashboard. Uma viagem ao servidor, uma tela montada. Os números nascem de duas ou três consultas de agregação (módulo 03), e a técnica central é o SUM(CASE WHEN ...), que conta várias coisas numa passada só:
SELECT COUNT(*) AS convidados,
SUM(CASE WHEN c.status = 'confirmado' THEN 1 ELSE 0 END) AS confirmados,
SUM(CASE WHEN c.status = 'recusado' THEN 1 ELSE 0 END) AS recusados,
SUM(CASE WHEN c.status = 'pendente' OR c.status IS NULL THEN 1 ELSE 0 END) AS pendentes,
COALESCE(SUM(CASE WHEN c.status = 'confirmado'
THEN 1 + cv.acompanhantes END), 0) AS pessoas_esperadas
FROM convidado cv
LEFT JOIN convite c ON c.convidado_id = cv.id
WHERE cv.casamento_id = ?;
(pendentes inclui o IS NULL: convidado sem convite ainda também "falta responder" do ponto de vista da Olívia. E pessoas_esperadas conta acompanhantes, porque lugar sentado não sabe a diferença.)
Mais uma pros presentes:
SELECT COUNT(*) AS presentes
FROM checkin ch
JOIN convidado cv ON cv.id = ch.convidado_id
WHERE cv.casamento_id = ?;
E o endpoint monta a resposta, com a regra do alerta calculada aqui:
const ocupacaoPct = Math.round((pessoas_esperadas / capacidade_total) * 100);
res.json({
convidados, confirmados, recusados, pendentes,
pessoas_esperadas,
presentes,
capacidade_total,
ocupacao_pct: ocupacaoPct,
alerta_capacidade: ocupacaoPct >= 90,
mesas // o array da consulta de ocupacao da secao 4.1
});
⚠️Pega-ratão
⚠️ Pega-ratão: calcular o alerta no front. Se o >= 90 morar no JS da tela, a banca abre o endpoint no Postman e vê um JSON sem a regra: e ela desconta onde a regra deveria morar (back, módulo 04 seção 11.1). O front recebe alerta_capacidade: true e só decide a cor. Regra no back, cosmética no front, sempre.
⚠️ E o pega-ratão clássico continua valendo: chumbar números bonitos na tela pra "ficar apresentável". A banca faz um check-in ao vivo e olha se o contador de presentes subiu. Se não reagiu, o "tempo real" era mentira e cai nota. Todo número do dashboard nasce de consulta.
6.2 Gráfico sem biblioteca (o caminho que sempre funciona)
Enunciado pode permitir biblioteca, pode não permitir, e a internet pode não existir na hora. O gráfico que sempre pode: HTML + CSS puros.
Barras (confirmados × recusados × pendentes), ~15 linhas:
<div class="grafico">
<div class="barra" style="--v: 62%"><span>Confirmados</span></div>
<div class="barra recusados" style="--v: 10%"><span>Recusados</span></div>
<div class="barra pendentes" style="--v: 28%"><span>Pendentes</span></div>
</div>
.grafico { display: flex; align-items: flex-end; gap: var(--espaco-2); height: 160px; }
.barra {
flex: 1;
height: var(--v); /* o JS seta --v com o percentual real */
background: var(--cor-sucesso);
border-radius: var(--raio) var(--raio) 0 0;
transition: height 300ms ease; /* o grafico "cresce" no refresh */
}
.barra.recusados { background: var(--cor-erro); }
.barra.pendentes { background: var(--cor-alerta); }
O JS só faz barra.style.setProperty('--v', pct + '%'). Com os tokens do módulo 06 já definidos, isso é bonito por padrão.
Donut de ocupação, 3 linhas de CSS com conic-gradient:
.donut {
width: 140px; aspect-ratio: 1; border-radius: 50%;
background: conic-gradient(var(--cor-acao) 0 var(--pct), #e5e7eb var(--pct) 100%);
/* furo do donut: um circulo interno com a cor do fundo, ou mask */
}
Com biblioteca, se o enunciado permitir e valer o tempo: Chart.js, arquivo local no projeto (front/lib/chart.umd.js, baixado antes). CDN como única via é aposta na internet da prova, e essa aposta já custou demo de muita gente. A regra: o caminho sem lib no músculo, a lib como upgrade.
6.3 "Tempo real" que é real
Duas pontas: o dashboard recarrega sozinho (setInterval de 15 segundos chamando o fetch de novo, módulo 05 seção 7.2) e recarrega após ação própria (fez check-in pelo painel? chama a atualização na sequência). O alerta de 90% é uma classe CSS trocada conforme alerta_capacidade, com a cor de status dos tokens. Quando a banca fizer o check-in ao vivo dela, o contador sobe em até 15 segundos sem ninguém apertar F5: esse momento, sozinho, conta a história de sistema vivo.
7. Comprovante em PDF e JPEG 🆕
Saída de documento: o comprovante da confirmação (ou do check-in) que o convidado recebe ou guarda. O enunciado da Seletiva pede PDF e JPEG. Três caminhos pra ter no bolso, em ordem de investimento:
7.1 O caminho 1 (o primeiro a fazer): uma página bem formatada + impressão do navegador
Monta comprovante.html?codigo=X: uma página caprichada com os tokens do módulo 06 (nome do casal, nome do convidado, código grande, QR se tiver, mesa, status). Aí:
PDF de graça, com CSS de impressão:
@media print {
.nao-imprime { display: none; } /* botoes, menu: some na impressao */
body { background: white; }
}
@page { size: A5; margin: 12mm; }
document.querySelector('#btn-pdf').addEventListener('click', () => window.print());
// o dialogo do navegador oferece "Salvar como PDF": documento pronto, tamanho A5
JPEG com html2canvas (biblioteca de um arquivo só, baixada local):
const area = document.querySelector('#comprovante');
const canvas = await html2canvas(area); // fotografa o elemento
const link = document.createElement('a');
link.href = canvas.toDataURL('image/jpeg', 0.92); // qualidade 92%
link.download = `comprovante-${codigo}.jpg`;
link.click(); // dispara o download
🛠️Gambiarra boa
🛠️ Por que esse caminho primeiro: UMA página cobre os DOIS formatos, e a mesma página ainda aparece na demo. O esforço vai todo pro que pontua (o capricho visual), e a "geração de documento" custa um window.print() e dez linhas de JS. É exatamente o atalho honesto que dev de produto usa.
7.2 O caminho 2 (Node): PDF gerado no servidor com pdfkit
Se o enunciado pedir download gerado pelo servidor (ou pontos de back nessa feature):
const PDFDocument = require('pdfkit');
router.get('/convites/:codigo/comprovante.pdf', async (req, res, next) => {
try {
// ...busca convite + convidado + casamento; 404 se nao existir...
const doc = new PDFDocument({ size: 'A5', margin: 40 });
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename=comprovante.pdf');
doc.pipe(res); // o PDF escorre direto pra resposta HTTP
doc.fontSize(20).text('Wedding Pass', { align: 'center' });
doc.moveDown();
doc.fontSize(14).text(`Convidado: ${convidado.nome}`);
doc.text(`Código: ${convite.codigo}`);
doc.text(`Mesa: ${convidado.mesa ?? 'a definir'}`);
doc.text(`Status: ${convite.status}`);
doc.end();
} catch (erro) { next(erro); }
});
7.3 O caminho 3 (PHP): dompdf, reaproveitando o HTML
O dompdf converte HTML em PDF, então o mesmo HTML do comprovante da seção 7.1 serve de template:
require 'vendor/autoload.php';
use Dompdf\Dompdf;
$dompdf = new Dompdf();
$dompdf->loadHtml($htmlDoComprovante); // o mesmo HTML da pagina, com CSS embutido
$dompdf->setPaper('A5');
$dompdf->render();
$dompdf->stream('comprovante.pdf'); // dispara o download
A decisão, resumida: caminho 1 sempre (cobre PDF + JPEG com uma página e serve pra demo); caminhos 2/3 quando o enunciado cobrar geração no servidor. E em qualquer caminho, o investimento certo é na página bonita primeiro, porque ela alimenta os três.
⚠️Pega-ratão
⚠️ Pega-ratão: comprovante feio (dados desalinhados, sem identidade visual). A banca abre o arquivo. Um comprovante com os tokens do evento, tipografia decente e o QR alinhado é acabamento nível 3; um <pre> com os dados jogados é nível 1 com a mesma lógica por trás. Mesma regra do módulo 06: o que o usuário final recebe conta história sobre o sistema inteiro.
8. Branch e merge no ritmo de features 🔄
Essas duas semanas são o cenário perfeito pra rodar o fluxo do módulo 02 de verdade: uma branch por feature.
git switch -c feat/rsvp # nasce a branch da feature
# ...commits pequenos enquanto constroi: banco, endpoint, tela...
git switch main
git merge feat/rsvp # feature pronta entra na main
Commits pequenos e frequentes, mensagens no padrão que vocês já usam (feat:, fix:, chore:, sem acentos). No treino, cada feature deste módulo nasce numa branch e entra na main por merge, inclusive com um conflito ensaiado de propósito perto do fim (o guia de aula marca o dia): conflito resolvido com calma no treino é conflito que não assusta na prova.
E na prova? Regra prática: branch só se estiver no músculo, sem custo mental. O inegociável é o resto: commits frequentes na main já são backup contra desastre e evidência de processo pra banca. Branch bem usada é ponto extra de organização; branch mal usada (código preso fora da main na hora da entrega) é risco. Conforto decide.
9. Como o CIS enxerga as funcionalidades novas 🎯
Cada feature deste módulo espalha pontos por mais de um critério. O mapa:
| Funcionalidade |
Onde pontua |
Como a banca testa |
| RSVP público |
Back (regra de capacidade, validação) + front (4 estados da página) |
Código inválido na URL, responder duas vezes, confirmar com o evento quase cheio |
| Três perfis |
Back (403 rota a rota) + UX (menu e tela por perfil) |
Token fraco em rota forte, direto na API |
| Check-in por código |
Back (corrida bloqueada) + front (painel com feedback) + UX (fluxo da fila) |
Duas abas com o mesmo código quase juntas; espera o 409 com o horário |
| Dashboard |
Banco (agregações) + back (alerta no lugar certo) + front (hierarquia visual) |
Check-in ao vivo olhando se o contador sobe sozinho |
| Mesas |
Banco (modelo + consulta de ocupação) + back (regra de lotação) |
Alocar convidado em mesa cheia |
| Comprovante |
Front + UX (acabamento nível 3) |
Abre o PDF e o JPEG e olha o capricho |
Lendo a coluna da direita de uma vez, dá pra ver quem é a banca: um usuário apressado e malicioso ao mesmo tempo. Ela usa o sistema como a Renata numa fila (rápido, sem ler manual) e como alguém tentando quebrar (URL trocada, ação repetida, token errado). Todo teste de vocês de agora em diante imita esses dois humores.
10. O ritual da Prova Surpresa (Módulo A) 🆕
O Módulo A da Seletiva entrega um requisito desconhecido e 2 horas. É a compressão máxima do que este módulo treina. O ritual, com o relógio na mesa:
| Relógio |
O que tá acontecendo |
| 0:00 – 0:10 |
Ler o enunciado grifando: must vs should (módulo 09 se vier em inglês) |
| 0:10 – 0:20 |
O método da seção 1 no papel: entidades, endpoints, telas. O papel é o plano das 2h |
| 0:20 – 0:50 |
Banco (ALTER/CREATE no migrations.sql) + endpoint com a regra de negócio + prova no Postman |
| 0:50 – 1:40 |
A tela, integrada de verdade, com os 4 estados |
| 1:40 – 2:00 |
Testes de sabotador (os dois humores da banca) + commits finais + respiro |
A ordem de corte quando o tempo aperta (e ele vai apertar): primeiro cai o enfeite visual extra, depois o gráfico, depois o segundo formato de saída (JPEG). O que nunca cai: a regra de negócio no back e a validação. Uma feature crua com a regra certa pontua; uma feature linda que aceita dado inválido conta a história errada pra banca inteira.
E commit a cada peça que funciona (banco pronto: commit; endpoint provado no Postman: commit; tela integrada: commit). Se algo quebrar feio aos 1:50, o último commit é a rede.
🎯Foco de prova
🎯 Esse ritual se treina. O guia de aula fecha as duas semanas rodando ele completo, e os treinos avaliativos repetem. Na Seletiva, o ritual tem que estar tão automático quanto o SELECT.
11. Como estudar esse módulo
Esse módulo não se estuda decorando, se estuda construindo. Pra cada funcionalidade, a trilha completa tem que sair sem consulta:
- Dados: que entidades e campos isso exige? Que constraint protege a regra no último nível? (módulo 03)
- Regra: que regra de negócio a API garante, com que status ela responde, e onde entra trava ou constraint? (módulo 04 + seções 3 a 5 daqui)
- Tela: como o usuário faz isso, com que estados e que feedback? (módulos 05 e 06)
- Prova: o que aqui é objetivo (sim/não) e o que é julgamento (0 a 3)? Onde mora a nota? (seção 9)
Quem percorre essa trilha pra cada feature entendeu o sistema como um todo, e é isso que a prova mede: não pedaços soltos, um sistema inteiro que aguenta uso apressado e malicioso.
12. Autoavaliação do módulo
- Quais são os quatro passos do método e o que sai de cada um?
- Por que a matriz de rotas vale mais que a "escada" de permissões? Qual exceção do Wedding Pass a escada erraria, e qual a defesa dela?
- Descreve o roteiro do teste do token fraco no Postman, do login ao status esperado.
- Desenha o ciclo de estados do convite. Onde cada estado mora no banco, e por que "presente" não é um valor do ENUM?
- Por que o código do convite não pode ser o id sequencial? E como o sistema lida com colisão de códigos?
- O que o
FOR UPDATE serializa no RSVP? Conta a história dos dois "sim" simultâneos no último lugar.
- Por que a página pública do RSVP não usa o helper
api() do módulo 05?
- Quando o botão "copiar link" resolve o requisito do convite, e quando precisa de envio de e-mail de verdade?
- Escreve de cabeça a consulta dos pendentes com dias restantes (
DATEDIFF).
- Quais são as três defesas do check-in? Por que o INSERT-e-captura dispensa transação nesse caso?
- Como provar em 30 segundos que a dupla entrada tá bloqueada?
- Por que o dashboard é UM endpoint, e por que o alerta de 90% tem que nascer no back?
- Como fazer o gráfico de barras e o donut sem biblioteca nenhuma? E qual a condição pra usar Chart.js?
- Quais os três caminhos do comprovante e por que a página bem formatada vem primeiro?
- Na capacidade da mesa: alocados ou só confirmados? Tua escolha e tua defesa.
- Recita o timebox do Módulo A e a ordem de corte quando o tempo aperta.
Referências do módulo
Do plano de curso (UC1):
- BEZERRA, Eduardo. Princípios de análise e projeto de sistemas com UML. Rio de Janeiro: Elsevier. (requisitos → projeto: a base do método da seção 1)
- MARTIN, Robert C. Código limpo: habilidades práticas do Agile Software. Rio de Janeiro: Alta Books. (funções curtas e nomes honestos valem dobrado em código de feature escrito sob pressão)
Documentação oficial (o "mercado" deste módulo):
- Nodemailer (e-mail em Node): https://nodemailer.com/
- PHPMailer (e-mail em PHP): https://github.com/PHPMailer/PHPMailer
- PDFKit (PDF no servidor, Node): https://pdfkit.org/
- Dompdf (HTML → PDF, PHP): https://github.com/dompdf/dompdf
- html2canvas (elemento → imagem): https://html2canvas.hertzen.com/
- Chart.js (gráficos, usar local): https://www.chartjs.org/docs/latest/
qrcode (QR em Node): https://www.npmjs.com/package/qrcode
- MDN,
window.print: https://developer.mozilla.org/en-US/docs/Web/API/Window/print
- MDN,
URLSearchParams: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
- MDN,
conic-gradient: https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/conic-gradient
- MySQL 8.0, funções de data (
DATEDIFF, DATE_SUB): https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html
- MySQL 8.0, locking reads (
FOR UPDATE): https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html