Pular para o conteúdo principal

Appointments (Agendamentos)

Pilar: 02 — Funcionalidades Core / App Mobile
Status: 🟢 Implementado
Última Revisão Técnica: 2026-08-03
Módulos Conectados: gatein-server (app/api/mobile/activities.py, app/models.py), gatein-app (src/screens/Home, src/store)


1. Visão Geral e Arquitetura do Módulo

O módulo de Appointments (Agendamentos) é o eixo central do ecossistema Gatein para o motorista de frete. Um Appointment representa o compromisso operacional agendado entre um motorista (identificado obrigatoriamente por CPF/Tax ID) e um Terminal de Carga/Descarga (empresa cliente do ecossistema).

Toda a jornada do motorista no app mobile gravita em torno dos seus agendamentos: desde a recepção de notificações de novos agendamentos, passando pela visualização de cards dinâmicos customizados pelo terminal, verificação de pendências de integração de segurança, execução de check-in antecipado via Geofence, até a emissão do Ticket Digital e conclusão da carga ou descarga.

1.1 Diagrama do Ciclo de Vida e Atores

sequenceDiagram
autonumber
actor T as Terminal / Web App
participant S as Gatein Server (FastAPI)
participant DB as PostgreSQL
actor M as Motorista / Mobile App

T->>S: POST /web/appointments (Cria Agendamento p/ CPF)
S->>DB: Persiste Appointment (Status: PLANNED/ACTIVE)
S->>M: Envia Push Notification (FCM) "Novo Agendamento"
M->>S: GET /mobile/activities?status_filter=active
S->>DB: Fetch Appointments + Terminais + Layouts + SafetyIntegrations
S-->>M: Retorna Payload Unificado (JSON + Sqids + Layouts)
M->>M: Renderiza Card Dinâmico via Layout Engine
M->>S: POST /mobile/checkin (Entrada na Geofence)
S->>DB: Atualiza Status -> CHECKED-IN / IN_PROGRESS
T->>S: Aprova Check-in no Web App
S->>DB: Gera Ticket e marca status COMPLETED
S-->>M: Emite Ticket Digital no App Mobile

2. Estruturas de Dados e Schemas Explícitos

2.1 Modelo Relacional de Banco de Dados (appointments)

A tabela appointments no PostgreSQL armazena a entidade física do agendamento:

ColunaTipo SQLConstraintsDescrição / Regra
idUUIDPRIMARY KEY, Default gen_random_uuid()Identificador único interno no banco
refVARCHAR(64)NULLABLE, INDEXCódigo de referência externo (ex: número da Ordem de Carga/ERP)
terminal_idUUIDFOREIGN KEY (terminals.id), NOT NULLID do Terminal detentor do agendamento
user_tax_idVARCHAR(14)NOT NULL, INDEXCPF do motorista (apenas números). Chave primária de vínculo com o motorista
layout_refVARCHAR(64)NULLABLEReferência da versão do JSON de layout a ser utilizada na renderização
statusVARCHAR(32)NOT NULL, Default 'ACTIVE'Estado atual no ciclo de vida (ver Máquina de Estados)
summaryTEXTNULLABLEResumo descritivo da carga/operação
license_plateVARCHAR(10)NULLABLEPlaca do veículo associado (Cavalo/Carreta)
window_startTIMESTAMPTZNOT NULLInício da janela agendada de atendimento
window_endTIMESTAMPTZNOT NULLFim da janela agendada de atendimento
start_toleranceINTEGERNOT NULL, Default 0Tolerância em minutos permitida ANTES do window_start
end_toleranceINTEGERNOT NULL, Default 0Tolerância em minutos permitida DEPOIS do window_end
custom_dataJSONBNULLABLEObjeto de chave-valor com dados arbitrários enviados pelo Terminal para preenchimento de layouts dinâmicos
created_atTIMESTAMPTZNOT NULL, Default now()Timestamp de criação
updated_atTIMESTAMPTZNOT NULL, Default now()Timestamp de última atualização

2.2 Schema de Resposta da API Mobile (AppointmentResponseSchema)

Quando o aplicativo solicita a rota GET /mobile/activities, cada agendamento é serializado no formato Pydantic abaixo:

{
"id": "w8x9k2m1",
"type": "appointment",
"ref": "ORD-2026-9948",
"terminal_id": "p4k2n8m9",
"layout_ref": "v2_standard_card",
"status": "ACTIVE",
"summary": "Descarga de Grãos - Soja em Grão",
"license_plate": "ABC1D23",
"window_start": "2026-08-04T10:00:00Z",
"window_end": "2026-08-04T12:00:00Z",
"start_tolerance": 60,
"end_tolerance": 30,
"custom_data": {
"peso_estimado": "45.000 kg",
"tipo_carroceria": "Graneleiro",
"gate_entrada": "Gate 03"
},
"is_safety_integration_pending": false,
"tickets": [
{
"id": "z9y8x7w6",
"layout_ref": "v1_ticket_gate",
"content": {
"codigo_liberacao": "984210",
"doca": "Doca 12"
},
"created_at": "2026-08-04T10:15:22Z"
}
]
}

3. Máquina de Estados e Transições (State Machine)

O campo status de um agendamento é rigorosamente controlado por regras de negócio de transição.

3.1 Estados Válidos

  • PLANNED: Agendamento futuro criado pelo Terminal/Transportador, ainda fora da janela operacional imediata.
  • ACTIVE: Agendamento liberado e disponível no aplicativo do motorista para visualização e execução.
  • CHECKED_IN / CHECKED-IN: Motorista realizou o check-in remoto ou presencial. Aguardando chamada/liberação de doca.
  • IN_PROGRESS / ON_GOING: Motorista teve seu check-in aprovado e está fisicamente realizando a operação no Terminal.
  • PAUSED: Operação temporariamente paralisada por decisão operacional do Terminal.
  • COMPLETED: Carga/descarga concluída com sucesso. Ticket de saída emitido.
  • CANCELLED: Cancelado pelo Terminal ou Transportador antes do atendimento.
  • DELETED: Soft-delete lógico (ocultado de todas as rotas operacionais).

3.2 Matriz de Transições de Estado

Estado AtualEvento DisparadorEstado DestinoCondição de Guarda (Guards)Ações Secundárias
PLANNEDAbertura da janela temporalACTIVEnow() >= window_start - start_toleranceNotificação Push de Lembrete ao Motorista
ACTIVEMotorista faz Check-inCHECKED_INis_safety_integration_pending == False AND dentro da tolerância de janelaNotifica painel do Terminal em tempo real
ACTIVETerminal CancelaCANCELLEDNão possui Ticket emitido associadoInvalida jobs de lembrete no APScheduler
CHECKED_INTerminal Aprova Check-inIN_PROGRESSValidação de doca e liberação de gateGera registro na tabela tickets
CHECKED_INTerminal Rejeita Check-inACTIVEMotivo de rejeição informado no Web AppEnvia notificação Push com motivo de erro
IN_PROGRESSTerminal Conclui OperaçãoCOMPLETEDEmissão de comprovante de saídaNotifica motorista e gera Ticket Final
IN_PROGRESSTerminal ParalisaPAUSEDMotivo operacional registradoAtualiza status no app do motorista
PAUSEDTerminal RetomaIN_PROGRESSResolvida pendência no pátioNotifica motorista para retorno
QualquerAção de Exclusão AdminDELETEDPerfil Admin de Plataforma ou Tenant OwnerRemove agendamento das rotas do motorista

4. Regras de Negócio Explícitas (RN-APT-XXX)

As regras abaixo devem ser estritamente aplicadas por qualquer serviço que crie, modifique ou consulte agendamentos.

RN-APT-001: Isolamento por Tax ID (CPF) do Motorista

O aplicativo mobile exibe exclusivamente agendamentos em que o campo appointments.user_tax_id coincida exatamente com o CPF limpo (apenas dígitos) extraído do token JWT autenticado do motorista (current_user.tax_id). É proibida a consulta cruzada de agendamentos entre motoristas distintos.

RN-APT-002: Janela Temporal e Tolerâncias Operacionais

Um agendamento é considerado "Elegível para Check-in" se, e somente se, o horário atual do servidor (now()) satisfizer a seguinte regra:

(window_start - start_tolerance) <= now() <= (window_end + end_tolerance)

Caso o motorista tente realizar check-in fora dessa janela calculada, o servidor deve retornar erro HTTP 400 (OUT_OF_WINDOW_TOLERANCE).

RN-APT-003: Bloqueio por Integração de Segurança Pendente

Se a empresa Terminal associada ao agendamento possuir a configuração safety_integration_active = True, o servidor verificará a presença de um registro na tabela safety_integrations para aquele user_tax_id e company_id com expires_at > now().

  • Se não houver integração válida, o campo computado is_safety_integration_pending será retornado como True.
  • Toda tentativa de efetuar check-in em agendamentos com is_safety_integration_pending == True será bloqueada no backend, exigindo que o motorista conclua a integração de segurança (ex: vídeo/formulário) previamente.

RN-APT-004: Limite de Check-in Ativo por Agendamento

Um agendamento pode possuir no máximo 1 (um) check-in em estado PENDING ou PROCESSING simultaneamente. Múltiplas submissões concorrentes devem ser travadas por lock pessimista ou constraint de banco de dados.

RN-APT-005: Imutabilidade de Appointments Finalizados ou Cancelados

Agendamentos nos estados COMPLETED, CANCELLED ou DELETED são imutáveis. Nenhuma alteração em campos de janela, placa, carga ou dados customizados é permitida após a transição para esses estados.

RN-APT-006: Resolução Fallback de Layout Dinâmico

Se um agendamento possuir layout_ref nulo ou se a combinação de (terminal_id, layout_ref) não for encontrada no dicionário de layouts do Terminal, o aplicativo mobile utilizará obrigatoriamente o layout default configurado para a empresa Terminal.

RN-APT-007: Rastreabilidade de Interações do Motorista (Log Events)

Qualquer visualização detalhada ou clique no card de um agendamento no aplicativo mobile deve gerar um evento de telemetria enviado via POST /mobile/activities/log-events. O backend deve validar se o activity_id enviado pertence ao motorista autenticado antes de gravar em appointment_logs.

RN-APT-008: Estratégia de Paginação e Meta-dados

A listagem de agendamentos na rota GET /mobile/activities utiliza busca paginada com técnica de N+1 (busca limit + 1 itens para determinar com exatidão se o atributo has_more na resposta será True ou False). O limite padrão é de 50 registros, configurável até 100.

RN-APT-009: Ofuscação Mandatória de IDs via Sqids

Nenhum ID numérico sequencial ou UUID de banco de dados pode ser retornado em formato bruto no JSON da API pública/mobile. Todos os identificadores de agendamento (id), terminal (terminal_id), tickets e empresas devem ser obrigatoriamente convertidos via o utilitário encode_id() (Sqids).

RN-APT-010: Tratamento de Filtro por Categoria (status_filter)

A API de atividades suporta três visões de filtro:

  1. active: Filtra por status pertencentes a ["ACTIVE", "ON_GOING", "CHECKED-IN", "CHECKED_IN", "PAUSED", "PLANNED", "IN_PROGRESS"].
  2. history: Filtra por status pertencentes aos estados não-ativos/finalizados (COMPLETED, CANCELLED, etc.).
  3. all: Retorna todos os agendamentos independente do estado (exceto DELETED).

5. Fluxos Lógicos e Detalhamento de Endpoints

5.1 Endpoint GET /mobile/activities

Este é o endpoint primário consumido pela tela principal (Home/Activities) do app mobile.

Parâmetros de Requisição (Query Parameters)

  • status_filter (string, opcional): Pode ser 'active' (padrão), 'history' ou 'all'.
  • start_date (datetime, opcional): Filtra agendamentos com window_start >= start_date.
  • end_date (datetime, opcional): Filtra agendamentos com window_start <= end_date.
  • limit (int, opcional): Padrão 50, min 1, max 100.
  • offset (int, opcional): Padrão 0, min 0.

Lógica de Execução no Servidor (Step-by-Step)

  1. Autenticação: Extrai e valida o Firebase Bearer Token via get_current_user. Obtém o current_user.tax_id.
  2. Construção da Query de Appointments:
    • Adiciona filtro base: Appointment.user_tax_id == current_user.tax_id AND Appointment.status != 'DELETED'.
    • Aplica condições de status_filter (in_ ou notin_ para active_statuses).
    • Aplica filtros opcionais de data.
  3. Execução Paginada: Ordena por Appointment.window_start ASC com .limit(limit + 1).offset(offset).
  4. Agregação de Chaves Relacionadas:
    • Coleta os terminal_id únicos de todos os agendamentos retornados.
    • Coleta os pares de (terminal_id, layout_ref) necessários para appointments e tickets.
  5. Busca de Dados Concorrentes/Agrupados:
    • Busca os dados dos Terminais (Terminal.id.in_(terminal_ids)).
    • Busca os layouts configurados (AppointmentLayout, TicketLayout).
    • Busca registros de SafetyIntegration ativos para o CPF do motorista nesses terminais.
  6. Cálculo Dinâmico de Pendência de Segurança:
    • Para cada agendamento, verifica se o terminal exige integração de segurança (safety_integration_active).
    • Se exigido e não houver registro válido não-expirado, seta is_safety_integration_pending = True.
  7. Serialização com Sqids: Retorna o payload estruturado contendo a lista de agendamentos, o mapa de terminais e o dicionário de layouts indexado por "{encoded_terminal_id}_{layout_ref}".

5.2 Endpoint POST /mobile/activities/log-events

Endpoint utilizado para registrar ações de engajamento do motorista com os cards de agendamento.

Payload de Requisição (MobileLogEventsPayload)

{
"events": [
{
"activity_type": "appointment",
"activity_id": "w8x9k2m1",
"event": "viewed",
"message": "Card expandido pelo motorista",
"json_data": {
"source_screen": "HomeScreen",
"battery_level": 85
}
}
]
}

Lógica de Processamento

  1. Decodifica o activity_id via decode_id().
  2. Valida no banco se o agendamento existe e pertence ao current_user.tax_id.
  3. Insere um registro de log auditável na tabela appointment_logs com timestamp atual e metadados.
  4. Retorna { "success": true, "message": "Events logged successfully" }.

6. Regras de Segurança e Controle de Acesso (RBAC & Tenant)

  1. Validação de Identidade (Auth Guard):
    • O endpoint rejeita qualquer requisição sem token Bearer válido com HTTP 401 (Unauthorized).
  2. Tenant Guard Estrito:
    • O motorista não possui poder de informar o tax_id na requisição; o servidor obrigatoriamente força o uso do tax_id atrelado à conta autenticada.
  3. Sanitização de Saída:
    • Dados internos de banco (ex: strings de conexão, chaves de API do terminal, metadados internos de configuração de infraestrutura) são filtrados na serialização do TerminalResponseSchema.

7. Tratamento de Erros e Caminhos Alternativos

7.1 Tabela de Erros Comuns da API

Código HTTPCódigo de Erro InternoCausa RaizAção Recomendada no App Mobile
401 UnauthorizedTOKEN_EXPIRED / INVALID_TOKENToken do Firebase expirou ou foi revogadoRedirecionar motorista para a tela de Login/OTP
400 Bad RequestOUT_OF_WINDOW_TOLERANCETentativa de check-in fora do horário permitidoExibir modal explicativo com a janela permitida
403 ForbiddenSAFETY_INTEGRATION_REQUIREDTentativa de check-in com pendência de segurançaAbrir modal com link para o vídeo/form de segurança
404 Not FoundAPPOINTMENT_NOT_FOUNDID de agendamento inválido ou pertencente a outro CPFAtualizar a lista de agendamentos via pull-to-refresh
422 UnprocessableINVALID_SQIDHash de ID malformado ou corrompidoNotificar o usuário e relatar log silencioso

7.2 Estratégia de Fallback e Cache Offline no App Mobile

  • Cache Persistente: Ao obter resposta com sucesso de GET /mobile/activities, o aplicativo armazena a lista e o mapa de layouts em armazenamento local seguro (AsyncStorage / Zustand Store).
  • Modo Offline: Caso haja falha de conexão (Network Error / Timeout), o app renderiza a última versão cached dos agendamentos exibindo um banner superior informativo: "Modo Offline — Exibindo dados salvos localmente".
  • Ações Desabilitadas em Offline: O botão de submissão de Check-in antecipado permanece desabilitado enquanto o dispositivo estiver sem conexão à internet.