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:
| Coluna | Tipo SQL | Constraints | Descrição / Regra |
|---|---|---|---|
id | UUID | PRIMARY KEY, Default gen_random_uuid() | Identificador único interno no banco |
ref | VARCHAR(64) | NULLABLE, INDEX | Código de referência externo (ex: número da Ordem de Carga/ERP) |
terminal_id | UUID | FOREIGN KEY (terminals.id), NOT NULL | ID do Terminal detentor do agendamento |
user_tax_id | VARCHAR(14) | NOT NULL, INDEX | CPF do motorista (apenas números). Chave primária de vínculo com o motorista |
layout_ref | VARCHAR(64) | NULLABLE | Referência da versão do JSON de layout a ser utilizada na renderização |
status | VARCHAR(32) | NOT NULL, Default 'ACTIVE' | Estado atual no ciclo de vida (ver Máquina de Estados) |
summary | TEXT | NULLABLE | Resumo descritivo da carga/operação |
license_plate | VARCHAR(10) | NULLABLE | Placa do veículo associado (Cavalo/Carreta) |
window_start | TIMESTAMPTZ | NOT NULL | Início da janela agendada de atendimento |
window_end | TIMESTAMPTZ | NOT NULL | Fim da janela agendada de atendimento |
start_tolerance | INTEGER | NOT NULL, Default 0 | Tolerância em minutos permitida ANTES do window_start |
end_tolerance | INTEGER | NOT NULL, Default 0 | Tolerância em minutos permitida DEPOIS do window_end |
custom_data | JSONB | NULLABLE | Objeto de chave-valor com dados arbitrários enviados pelo Terminal para preenchimento de layouts dinâmicos |
created_at | TIMESTAMPTZ | NOT NULL, Default now() | Timestamp de criação |
updated_at | TIMESTAMPTZ | NOT 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 Atual | Evento Disparador | Estado Destino | Condição de Guarda (Guards) | Ações Secundárias |
|---|---|---|---|---|
PLANNED | Abertura da janela temporal | ACTIVE | now() >= window_start - start_tolerance | Notificação Push de Lembrete ao Motorista |
ACTIVE | Motorista faz Check-in | CHECKED_IN | is_safety_integration_pending == False AND dentro da tolerância de janela | Notifica painel do Terminal em tempo real |
ACTIVE | Terminal Cancela | CANCELLED | Não possui Ticket emitido associado | Invalida jobs de lembrete no APScheduler |
CHECKED_IN | Terminal Aprova Check-in | IN_PROGRESS | Validação de doca e liberação de gate | Gera registro na tabela tickets |
CHECKED_IN | Terminal Rejeita Check-in | ACTIVE | Motivo de rejeição informado no Web App | Envia notificação Push com motivo de erro |
IN_PROGRESS | Terminal Conclui Operação | COMPLETED | Emissão de comprovante de saída | Notifica motorista e gera Ticket Final |
IN_PROGRESS | Terminal Paralisa | PAUSED | Motivo operacional registrado | Atualiza status no app do motorista |
PAUSED | Terminal Retoma | IN_PROGRESS | Resolvida pendência no pátio | Notifica motorista para retorno |
| Qualquer | Ação de Exclusão Admin | DELETED | Perfil Admin de Plataforma ou Tenant Owner | Remove 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_pendingserá retornado comoTrue. - Toda tentativa de efetuar check-in em agendamentos com
is_safety_integration_pending == Trueserá 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:
active: Filtra por status pertencentes a["ACTIVE", "ON_GOING", "CHECKED-IN", "CHECKED_IN", "PAUSED", "PLANNED", "IN_PROGRESS"].history: Filtra por status pertencentes aos estados não-ativos/finalizados (COMPLETED,CANCELLED, etc.).all: Retorna todos os agendamentos independente do estado (excetoDELETED).
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 comwindow_start >= start_date.end_date(datetime, opcional): Filtra agendamentos comwindow_start <= end_date.limit(int, opcional): Padrão50, min1, max100.offset(int, opcional): Padrão0, min0.
Lógica de Execução no Servidor (Step-by-Step)
- Autenticação: Extrai e valida o Firebase Bearer Token via
get_current_user. Obtém ocurrent_user.tax_id. - 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_ounotin_paraactive_statuses). - Aplica filtros opcionais de data.
- Adiciona filtro base:
- Execução Paginada: Ordena por
Appointment.window_start ASCcom.limit(limit + 1).offset(offset). - 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.
- Coleta os
- Busca de Dados Concorrentes/Agrupados:
- Busca os dados dos Terminais (
Terminal.id.in_(terminal_ids)). - Busca os layouts configurados (
AppointmentLayout,TicketLayout). - Busca registros de
SafetyIntegrationativos para o CPF do motorista nesses terminais.
- Busca os dados dos Terminais (
- 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.
- Para cada agendamento, verifica se o terminal exige integração de segurança (
- 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
- Decodifica o
activity_idviadecode_id(). - Valida no banco se o agendamento existe e pertence ao
current_user.tax_id. - Insere um registro de log auditável na tabela
appointment_logscom timestamp atual e metadados. - Retorna
{ "success": true, "message": "Events logged successfully" }.
6. Regras de Segurança e Controle de Acesso (RBAC & Tenant)
- Validação de Identidade (Auth Guard):
- O endpoint rejeita qualquer requisição sem token Bearer válido com HTTP 401 (
Unauthorized).
- O endpoint rejeita qualquer requisição sem token Bearer válido com HTTP 401 (
- Tenant Guard Estrito:
- O motorista não possui poder de informar o
tax_idna requisição; o servidor obrigatoriamente força o uso dotax_idatrelado à conta autenticada.
- O motorista não possui poder de informar o
- 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.
- 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
7. Tratamento de Erros e Caminhos Alternativos
7.1 Tabela de Erros Comuns da API
| Código HTTP | Código de Erro Interno | Causa Raiz | Ação Recomendada no App Mobile |
|---|---|---|---|
401 Unauthorized | TOKEN_EXPIRED / INVALID_TOKEN | Token do Firebase expirou ou foi revogado | Redirecionar motorista para a tela de Login/OTP |
400 Bad Request | OUT_OF_WINDOW_TOLERANCE | Tentativa de check-in fora do horário permitido | Exibir modal explicativo com a janela permitida |
403 Forbidden | SAFETY_INTEGRATION_REQUIRED | Tentativa de check-in com pendência de segurança | Abrir modal com link para o vídeo/form de segurança |
404 Not Found | APPOINTMENT_NOT_FOUND | ID de agendamento inválido ou pertencente a outro CPF | Atualizar a lista de agendamentos via pull-to-refresh |
422 Unprocessable | INVALID_SQID | Hash de ID malformado ou corrompido | Notificar 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.