3) Modo Off-line

O app foi projetado para operar em campo (fazenda/área rural), onde a internet pode ser instável ou inexistente. Por isso, o padrão obrigatório é offline-first: toda ação operacional deve ser registrada primeiro no aparelho, com geração de client_event_id, armazenamento local e envio posterior para sincronização com o ERP.

Offline-first TTL sessão: 72h Cache QR: 48h Obrigatório: client_event_id

Modo offline (sem internet) — padrão do app

No offline, o app não perde dados: ele registra os eventos no aparelho, mantém os itens em uma fila/outbox de sincronização e tenta enviar automaticamente quando houver internet. Mesmo com internet disponível, o app deve continuar seguindo esse padrão para evitar duplicidade, inconsistência e perda de dados em campo.

Regra principal (offline-first)

  • Toda ação operacional (entrega_kit, coleta_analise, fotos, aplicação etc.) deve primeiro ser salva localmente no aparelho.
  • No momento em que o evento local for criado, o app deve gerar um client_event_id único (UUID) e associá-lo permanentemente àquele evento.
  • Se houver internet, o app pode tentar sincronizar imediatamente; se falhar, o item deve permanecer salvo localmente para reenvio posterior.
  • O ERP continua sendo a fonte definitiva. O armazenamento no app é temporário (fila/cache), não é um “mini ERP”.

Validação offline (TTL 72h — sessão do app)

  • Quando o app consegue validar online (pareamento, valida_device_token ou qualquer endpoint administrativo com HTTP 200), ele atualiza ultima_validacao_em.
  • Se estiver sem internet, o app permite operar por até 72 horas desde a última validação online.
  • Após 72 horas sem validar online, o app bloqueia novas ações e solicita conexão para validar novamente.

Fila de sincronização (outbox)

  • Todo evento local deve entrar em uma fila/outbox com status de sincronização (pendente, erro, enviado, por exemplo).
  • Se um envio falhar por timeout, ausência de internet ou erro transitório, o evento deve continuar salvo localmente e permanecer pendente para novo envio.
  • Nos reenvios do mesmo evento, o app deve reutilizar exatamente o mesmo client_event_id.
  • Ao receber HTTP 200 com success=true, o app deve marcar o item como enviado e removê-lo da fila local.
  • Ao receber HTTP 200 com success=true e already_registered=true, o app também deve considerar o item como concluído, removendo-o da fila local.
  • Somente respostas com success=false devem manter o item pendente para nova tentativa, conforme a política de retry do aplicativo.

Idempotência (evitar duplicar no servidor)

  • Para todo evento enviado (entrega_kit, foto, coleta, aplicação etc.), o app deve gerar e enviar um identificador único client_event_id (UUID).
  • Obrigatório: sem client_event_id o servidor não consegue deduplicar reenvios (offline/timeout/conexão instável).
  • O ERP controla a idempotência consultando e registrando esse client_event_id para garantir que o mesmo evento não seja gravado duas vezes.
  • O simples fato de o client_event_id já existir no ERP significa que aquele evento já foi processado com sucesso.
  • Se o app reenviar um evento já registrado, o servidor retorna HTTP 200 (success=true) com already_registered=true para o app remover o item da fila.
Regra: endpoints que gravam evento exigem client_event_id (UUID)

Exemplo de resposta idempotente

{
  "success": true,
  "msg": "Evento já registrado (idempotente).",
  "data": {
    "already_registered": true,
    "client_event_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}
Regra prática: success=true ou already_registered=true = remover da fila local

Cache de QR Codes (pré-sincronização) — TTL 48h

Alguns fluxos precisam de metadados para orientar o usuário em campo (ex.: tipo/descrição da análise). Para operar offline, o app deve manter um cache temporário de QR Codes e metadados mínimos.

  • Quando o app estiver online, ele deve sincronizar com o ERP os QR Codes e metadados necessários para operação offline (ex.: kit_entrega, coleta_analise).
  • O app salva localmente (cache) os QR Codes e os metadados mínimos necessários e define um TTL de 48 horas.
  • No momento do scan sem internet, o app pode consultar esse cache local para orientar a operação, desde que o item ainda esteja dentro do TTL.
  • Esse cache é apenas apoio operacional; a validação definitiva continua sendo feita pelo ERP no momento da sincronização.
  • Se o QR não estiver no cache (ou estiver expirado), o app ainda pode registrar o evento na fila, mas deve informar que a confirmação e/ou instruções específicas dependem de sincronização com o servidor.
  • O app não deve depender exclusivamente do cache para concluir a operação no ERP; o cache serve apenas para melhorar a experiência em campo durante a ausência de conexão.

Aplicação (GPS a cada 3 segundos) — recomendado

  • Durante a aplicação, a internet pode oscilar. O app não deve enviar ponto a ponto em tempo real.
  • O correto é salvar a trilha localmente e sincronizar depois (offline-first).
  • Modelo recomendado: salvar uma sessão (início/fim) + os pontos GPS (lat/lon) a cada 3 segundos.
  • Na sincronização, enviar em lotes (chunks) para evitar timeout e permitir retomar em caso de falha.

Armazenamento local (recomendação técnica)

  • SQLite: recomendado para a fila (outbox), cache de QR e trilhas/pontos de GPS (volume alto e consulta por status).
  • FileSystem: fotos/arquivos devem ser salvos localmente e enviados depois.
  • SecureStore: credenciais sensíveis (ex.: device_token).
  • AsyncStorage: apenas configurações/estado simples (ex.: account_type, ultima_validacao_em).

Exemplo prático — tabela local de sincronização (SQLite)

Para facilitar a implementação do app, recomenda-se que cada evento operacional seja salvo primeiro em uma tabela local de sincronização (outbox). Essa tabela pode ter um id interno autoincrement para uso local do app e um client_event_id (UUID) para identificação global do evento durante a sincronização com o ERP.

  • id: identificador local do SQLite, apenas para uso interno do app.
  • client_event_id: UUID único do evento, reutilizado em todos os reenvios do mesmo item.
  • tipo_evento: ex.: receber_kit_aplicacao, coleta_analise.
  • payload_json: dados do evento a serem enviados ao ERP.
  • status_sync: ex.: pendente, erro, enviado.
  • tentativas_envio: contador de tentativas.
  • criado_em: data/hora local de criação do evento.
  • enviado_em: data/hora da confirmação de envio, quando houver.
CREATE TABLE app_eventos_sync (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                client_event_id TEXT NOT NULL UNIQUE,
                tipo_evento TEXT NOT NULL,
                payload_json TEXT NOT NULL,
                status_sync TEXT NOT NULL DEFAULT 'pendente',
                tentativas_envio INTEGER NOT NULL DEFAULT 0,
                criado_em TEXT NOT NULL,
                enviado_em TEXT NULL
                );
Recomendação: manter id autoincrement local + client_event_id UUID para sincronização

Exemplo prático — criação de um evento local

Quando o usuário realizar uma ação no app, o evento deve nascer primeiro no banco local. Nesse momento, o app gera um client_event_id único e salva o item na fila de sincronização.

// Exemplo conceitual
                const novoEvento = {
                client_event_id: '550e8400-e29b-41d4-a716-446655440000',
                tipo_evento: 'coleta_analise',
                payload_json: JSON.stringify({
                    qrcode: 'CA_ABC123',
                    data: '2026-04-14',
                    hora: '10:30:00',
                    latitude: -20.9487,
                    longitude: -48.4790
                }),
                status_sync: 'pendente',
                tentativas_envio: 0,
                criado_em: '2026-04-14 10:30:00'
                };
  • O evento é salvo localmente antes de qualquer tentativa de envio.
  • Se o envio falhar, o mesmo client_event_id deve ser mantido no próximo reenvio.
  • Se a API responder success=true ou already_registered=true, o item deve ser marcado como enviado e removido da fila local.

Exemplo prático — geração do client_event_id

O client_event_id deve ser gerado no momento em que o evento local é criado. Ele não depende do ERP e não substitui o id autoincrement do banco local.

// Exemplo em JavaScript / React Native
                function gerarClientEventId() {
                return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
                    const r = Math.random() * 16 | 0;
                    const v = c === 'x' ? r : (r & 0x3 | 0x8);
                    return v.toString(16);
                });
                }

                const clientEventId = gerarClientEventId();
Regra: 1 evento local = 1 client_event_id; nunca reutilizar em eventos diferentes

Exemplo prático — sincronização da fila

// Fluxo conceitual
                1. Buscar eventos com status_sync = 'pendente' ou 'erro'
                2. Enviar o payload ao ERP com o mesmo client_event_id
                3. Se a resposta for success=true:
                - marcar status_sync = 'enviado'
                - preencher enviado_em
                - remover da fila local, se essa for a estratégia escolhida
                4. Se a resposta for already_registered=true:
                - tratar como sucesso
                - marcar como enviado
                - remover da fila local
                5. Se houver erro:
                - incrementar tentativas_envio
                - manter como pendente ou erro para nova tentativa
Padrão obrigatório: registrar offline → manter na fila → sincronizar → confirmar no ERP → remover da fila

O app pode continuar usando id autoincrement no SQLite para controle interno. O client_event_id é um identificador adicional, do tipo UUID, usado para garantir idempotência na comunicação com o ERP. Assim, o mesmo evento pode ser reenviado várias vezes sem risco de duplicidade no servidor.