Programação Por Equipe DevBras

Capa do artigo: Integração com Google Agenda: guarde o id do evento e sobreviva ao acesso revogado

Integração com Google Agenda: guarde o id do evento e sobreviva ao acesso revogado

Modele compromissos no seu banco com o id do evento remoto e um status de sincronização, para que token expirado ou acesso revogado não apague a agenda da tela.

Segunda de manhã, a clínica abre o sistema e a semana está vazia. Ninguém desmarcou nada: a dona da conta Google trocou a senha no fim de semana, o token de acesso parou de valer e a tela, que lia os eventos direto da agenda dela, passou a mostrar zero compromissos. A recepção começa a ligar para os pacientes perguntando o horário. A resposta curta: o compromisso confirmado mora no seu banco, com o id do evento remoto guardado ao lado para atualizar ou apagar depois. A agenda externa é uma cópia sincronizada. Quando a sincronização falha, a tela mostra os seus dados e um aviso de falha, nunca uma semana vazia.

Provedor externo é integração, não disco. Ele pode sair do ar, revogar o acesso ou ser desconectado pela própria pessoa, e o seu sistema precisa continuar sabendo quem vem às 9h.

Por que a agenda some quando o acesso cai

Componentes de calendário como o FullCalendar conseguem ler eventos direto de uma agenda Google. Para uma página pública de eventos, isso basta. Para um sistema que marca consultas, é um atalho perigoso: o único lugar onde o compromisso existe passa a ser uma conta que você não controla.

O acesso de um app a uma conta Google depende de tokens OAuth. O token de acesso expira em pouco tempo e é renovado com um token de atualização. Esse token de atualização pode deixar de valer por vários motivos: a pessoa remove o acesso do app na conta, troca a senha em certas configurações, o app fica muito tempo sem usá-lo. Quando isso acontece, a renovação falha com o erro invalid_grant, e a única solução é a pessoa autorizar de novo.

Se a tela trata "não consegui ler" como "não há eventos", a falha vira um dado falso. É pior do que um erro, porque parece verdade.

Como modelar o compromisso com o evento remoto

O exemplo é a agenda da clínica: cada consulta nasce no sistema e é copiada para a agenda Google da profissional. A tabela guarda o compromisso inteiro, o id que o Google devolveu e o estado da última sincronização.

CREATE TABLE compromissos (
  id               BIGINT AUTO_INCREMENT PRIMARY KEY,
  titulo           VARCHAR(200) NOT NULL,
  inicio_utc       DATETIME NOT NULL,
  fim_utc          DATETIME NOT NULL,
  provedor         VARCHAR(20) NULL,
  evento_remoto_id VARCHAR(1024) CHARACTER SET ascii NULL,
  sync_status      VARCHAR(30) NOT NULL DEFAULT 'pendente',
  sync_erro        VARCHAR(500) NULL,
  sincronizado_em  DATETIME NULL,
  UNIQUE KEY uk_evento_remoto (provedor, evento_remoto_id)
);

Três escolhas importam aqui. Os horários ficam em UTC, e a conversão para o fuso da clínica acontece na exibição. O id remoto é texto em ASCII, o que mantém o índice único dentro do limite de tamanho do MySQL. E sync_status separa os estados que a tela precisa distinguir: pendente, ok, falhou, acesso_revogado e removido_no_provedor.

O passo a passo da sincronização

  1. Grave o compromisso no seu banco primeiro, com sync_status = 'pendente'. A consulta está marcada a partir daqui, aconteça o que acontecer com o Google.
  2. Envie para o provedor fora da requisição da tela, numa fila ou tarefa agendada. A recepção não espera a API do Google para ver a confirmação.
  3. Guarde o id devolvido e marque ok. Sem o id, você não consegue atualizar nem apagar o evento depois.
  4. Na alteração, use o id para atualizar o mesmo evento, em vez de criar outro.
  5. Classifique a falha: acesso revogado pede nova autorização; erro temporário pede nova tentativa; evento apagado do lado de lá pede uma decisão, não uma recriação automática.
  6. Peça só o escopo necessário. Para criar e editar eventos, o escopo https://www.googleapis.com/auth/calendar.events basta; não peça acesso completo à agenda se não vai gerenciar calendários.

O código que envia e registra o resultado

O exemplo usa a API REST do Google Calendar com fetch, em Node.js. A função obterAccessToken é a sua rotina de renovação OAuth; ela lança um erro com codigo = 'invalid_grant' quando a renovação é recusada.

const BASE = 'https://www.googleapis.com/calendar/v3/calendars/primary/events';

async function enviarParaGoogle(token, c) {
  const url = c.evento_remoto_id ? BASE + '/' + encodeURIComponent(c.evento_remoto_id) : BASE;
  const resposta = await fetch(url, {
    method: c.evento_remoto_id ? 'PATCH' : 'POST',
    headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      summary: c.titulo,
      start: { dateTime: c.inicio_utc.toISOString() },
      end: { dateTime: c.fim_utc.toISOString() }
    })
  });
  if (!resposta.ok) {
    const erro = new Error('Google respondeu ' + resposta.status);
    erro.status = resposta.status;
    throw erro;
  }
  return resposta.json();
}

async function sincronizar(db, c) {
  try {
    const token = await obterAccessToken(c.usuario_id);
    const evento = await enviarParaGoogle(token, c);
    await db.query(
      'UPDATE compromissos SET evento_remoto_id = ?, sync_status = ?, sync_erro = NULL, sincronizado_em = UTC_TIMESTAMP() WHERE id = ?',
      [evento.id, 'ok', c.id]
    );
  } catch (erro) {
    let status = 'falhou';
    if (erro.codigo === 'invalid_grant') status = 'acesso_revogado';
    else if (erro.status === 404 || erro.status === 410) status = 'removido_no_provedor';
    await db.query(
      'UPDATE compromissos SET sync_status = ?, sync_erro = ? WHERE id = ?',
      [status, String(erro.message).slice(0, 500), c.id]
    );
  }
}

Repare que nenhum caminho de erro apaga o compromisso. O pior que acontece é ele ficar marcado como não sincronizado, e a tela saber disso.

Existe um buraco clássico nessa sequência: o Google cria o evento, mas o UPDATE que guarda o id falha. Na próxima tentativa, o sistema cria um segundo evento. A API do Google Calendar permite que você informe o id do evento na criação, seguindo as regras de formato da documentação dela. Gerar esse id a partir do id local torna a criação repetível sem duplicar, a mesma ideia de idempotência usada em pagamentos.

O que a tela mostra quando a sincronização falha

A grade da clínica lê a sua API, não a agenda Google. A resposta traz os compromissos do banco e um resumo do estado da integração:

{
  "compromissos": [
    { "id": 812, "titulo": "Consulta - Ana P.", "inicio": "2026-10-19T12:00:00Z", "sync": "acesso_revogado" }
  ],
  "integracao": {
    "ok": false,
    "mensagem": "A agenda Google de Dra. Lúcia está desconectada. Os horários abaixo continuam valendo. Reconecte para voltar a sincronizar."
  }
}

A tela desenha a semana normalmente e mostra o aviso no topo, com um botão para reconectar. A recepção trabalha; a profissional resolve a conexão quando puder. O post sobre a grade de calendário que lê a sua API mostra o outro lado: como a tela pede alterações sem gravar direto no provedor. E se a dúvida é onde o compromisso deve nascer, o guia agenda do Google não é o banco do pedido trata disso.

Erros comuns

  • Ler os eventos direto do provedor como fonte única: no dia em que o token cai, a agenda some e ninguém sabe quem está marcado.
  • Não guardar o id do evento remoto: cada alteração cria um evento novo e a agenda da profissional fica cheia de duplicatas.
  • Tratar falha de leitura como lista vazia: a tela afirma que não há compromissos, e a recepção acredita.
  • Recriar automaticamente o evento apagado do lado de lá: se a profissional apagou de propósito, o sistema desfaz a decisão dela em silêncio.
  • Pedir o escopo mais amplo "por garantia": a tela de consentimento assusta, e um vazamento do token expõe mais do que o necessário.
  • Sincronizar dentro da requisição de quem marcou: uma lentidão do provedor vira lentidão da sua tela, e um erro dele vira erro ao marcar.

Checklist

  • Todo compromisso confirmado existe no seu banco, independente do provedor.
  • O id do evento remoto fica gravado ao lado do compromisso.
  • Existe um status de sincronização com estados distintos para revogado, falha e removido.
  • A falha de sincronização aparece como aviso, com os compromissos ainda visíveis.
  • A sincronização roda fora da requisição da tela, com nova tentativa para erro temporário.
  • O escopo OAuth pedido é o mínimo para o que o sistema faz.
  • Horários são gravados em UTC e convertidos na exibição.

Num ambiente de teste, remova o acesso do seu app nas configurações de segurança da conta Google e abra a agenda do sistema. O que a sua tela ainda mostra é o que você realmente possui.

Avalie este artigo

Referência: https://fullcalendar.io/docs/google-calendar