Agendamento com Google Agenda: a reserva nasce no seu banco, não no calendário
Modele a reserva com estado no seu banco, impeça conflito com uma restrição no PostgreSQL e sincronize com o Google Agenda por uma fila que tolera falha.
Segunda, 8h. A paciente chega à clínica para a consulta das 8h com a Dra. Ana e a recepção não encontra nada. Na sexta, ela marcou pelo site, viu a tela de sucesso e recebeu o convite. Só que a chamada à API do Google Agenda deu tempo esgotado no meio do caminho, o código tratou isso como "marcado" e o sistema da clínica não guardou nenhum registro próprio. A agenda externa é um espelho para a profissional consultar; a reserva confirmada é um registro do seu sistema, com estado, no seu banco.
A resposta curta: grave a reserva primeiro, numa transação que também cria uma tarefa de sincronização. Um processo separado cria o evento no Google, guarda o id e tenta de novo quando falha. O conflito de horário é barrado pelo seu banco, não pela agenda, e o acesso pede só o escopo de eventos, com token guardado no servidor.
Por que o calendário externo não pode ser a fonte da verdade
A integração ingênua faz tudo numa requisição: recebe o formulário, chama a API do Google e responde sucesso. Isso amarra o seu compromisso com o cliente a uma rede que você não controla. Quando a chamada falha depois de o Google ter gravado, você fica sem saber se o evento existe. Quando falha antes, a pessoa recebe "marcado" e nada existe.
Há um segundo problema: o Google Agenda permite eventos sobrepostos. Ele não sabe que a Dra. Ana atende uma pessoa por vez. Se duas pessoas clicam no mesmo horário ao mesmo tempo, as duas reservas entram. E um terceiro: a profissional pode apagar ou mover o evento na própria agenda, e aí o "registro" do seu negócio mudou sem passar pelas suas regras.
A solução é separar responsabilidades. O seu banco guarda a reserva e decide o que vale. O Google recebe uma cópia, e a cópia pode atrasar, falhar e ser refeita sem que a reserva mude de estado.
Como modelar a reserva e barrar conflito no banco
No PostgreSQL, uma restrição de exclusão impede dois períodos sobrepostos para a mesma profissional, mesmo com requisições simultâneas. Ela precisa da extensão btree_gist para combinar a igualdade do id com a sobreposição do intervalo:
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE reservas (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
profissional_id int NOT NULL,
periodo tstzrange NOT NULL,
status text NOT NULL CHECK (status IN ('confirmada', 'cancelada')),
google_event_id text,
criado_em timestamptz NOT NULL DEFAULT now(),
EXCLUDE USING gist (profissional_id WITH =, periodo WITH &&)
WHERE (status <> 'cancelada')
);
CREATE TABLE sync_agenda (
id bigserial PRIMARY KEY,
reserva_id uuid NOT NULL REFERENCES reservas(id),
acao text NOT NULL CHECK (acao IN ('criar', 'remover')),
tentativas int NOT NULL DEFAULT 0,
proxima_tentativa timestamptz NOT NULL DEFAULT now(),
ultimo_erro text,
feito_em timestamptz
);
O WHERE no fim da restrição faz uma reserva cancelada liberar o horário. O tipo tstzrange guarda início e fim como instantes com fuso, e o intervalo padrão inclui o início e exclui o fim, então a consulta das 8h às 8h30 não conflita com a das 8h30 às 9h. Se o seu banco não é PostgreSQL, o equivalente é verificar sobreposição dentro de uma transação com bloqueio da linha da profissional.
O fluxo da reserva, passo a passo
- Grave reserva e tarefa na mesma transação. Insira a linha em
reservascom statusconfirmadae uma linha emsync_agendacom açãocriar. Se a restrição barrar, responda que o horário acabou de ser ocupado. - Responda à pessoa com o seu código de reserva. A confirmação vem do seu banco. O e-mail de confirmação sai do seu sistema, não depende do convite do Google.
- Um processo separado consome a fila. Ele pega tarefas pendentes com
SELECT ... FOR UPDATE SKIP LOCKED, para dois processos não pegarem a mesma, e chama a API. - Guarde o id do evento. Com sucesso, grave
google_event_idna reserva efeito_emna tarefa. - Em falha, agende nova tentativa. Incremente
tentativas, grave o erro e empurreproxima_tentativacom espera crescente. Depois de um limite, alerte alguém em vez de tentar para sempre. - Cancelar é mudar o estado e enfileirar a remoção. A reserva vira
canceladana hora; a remoção do evento segue o mesmo caminho da fila.
Como chamar a API sem duplicar evento
O risco de qualquer nova tentativa é criar o mesmo evento duas vezes: a primeira chamada gravou, mas a resposta se perdeu. A API do Google Agenda permite que você proponha o id do evento na criação, desde que siga as regras de caracteres da documentação (letras minúsculas de a até v e dígitos). Derivando o id da sua reserva, a segunda tentativa encontra o evento já existente em vez de criar outro:
async function criarEvento(reserva, calendarId, accessToken) {
const url = 'https://www.googleapis.com/calendar/v3/calendars/' +
encodeURIComponent(calendarId) + '/events';
const resp = await fetch(url, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + accessToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: 'reserva' + reserva.id.replace(/-/g, ''),
summary: 'Consulta - reserva ' + reserva.codigo,
start: { dateTime: reserva.inicio, timeZone: 'America/Sao_Paulo' },
end: { dateTime: reserva.fim, timeZone: 'America/Sao_Paulo' }
}),
signal: AbortSignal.timeout(10000)
});
if (resp.status === 409) return { jaExistia: true };
if (!resp.ok) throw new Error('Google Agenda respondeu ' + resp.status);
return resp.json();
}
Um UUID sem hífens só tem dígitos e letras de a até f, e o prefixo "reserva" só usa letras do intervalo permitido. A resposta 409 indica que o id já existe, ou seja, uma tentativa anterior funcionou. É o mesmo raciocínio da idempotência no pagamento: a operação pode ser repetida sem efeito duplicado.
Repare no summary: ele leva o código da reserva, não o nome e o telefone da paciente. A agenda da profissional pode estar compartilhada com outras pessoas e sincronizada no celular. Mande para lá só o que ela precisa ver, e mantenha o dado pessoal no seu sistema.
Escopo e token: peça o mínimo
Se o sistema só cria e remove eventos, peça o escopo de eventos (https://www.googleapis.com/auth/calendar.events) em vez do acesso completo ao calendário. A tela de consentimento fica menos assustadora e, se o token vazar, o estrago é menor. O token de acesso e o de atualização ficam no servidor, cifrados no banco ou num cofre de segredos, nunca no JavaScript da página. O fluxo de autorização segue o que está no guia de OAuth de servidor web.
Erros comuns
- Responder "marcado" com base na resposta do Google. Quando a API falha depois de gravar, você não sabe o estado; quando falha antes, prometeu algo que não existe.
- Confiar no calendário para impedir conflito. Ele aceita sobreposição, e duas pessoas ficam com o mesmo horário.
- Tentar de novo sem id estável. Cada nova tentativa cria um evento duplicado na agenda da profissional.
- Pedir acesso total à agenda. Mais escopo que o necessário afasta quem autoriza e amplia o dano de um vazamento.
- Mandar dado pessoal no título do evento. Ele aparece em telas de bloqueio, agendas compartilhadas e notificações.
- Tentar para sempre em silêncio. Uma fila sem limite nem alerta esconde que a integração parou há dias.
Checklist
- A reserva existe no seu banco antes de qualquer chamada externa.
- Uma restrição no banco impede dois horários sobrepostos para a mesma profissional.
- A sincronização roda fora da requisição do usuário, com tentativas registradas.
- O id do evento é derivado da reserva e guardado em
google_event_id. - Cancelar muda o estado na hora e enfileira a remoção do evento.
- O escopo pedido é só o de eventos e o token fica no servidor.
- O título do evento não leva dado pessoal do cliente.
- Tarefas que passaram do limite de tentativas geram alerta.
Para os detalhes de guardar o id e lidar com a falha do lado do evento, veja evento de calendário externo. E faça o teste que resolve a dúvida: se o Google Agenda ficasse fora do ar a manhã inteira, a sua recepção ainda saberia quem vem às 8h?
Referência: https://www.upinside.com.br/blog/integracao-com-google-calendar-e-agendamento-de-horarios