Skip to content
avilaopsPublic

About

TMS: sistema de gestão de transportes terrestres, carga fracionada e dedicada. Coletas, manifestos, frota, financeiro, fiscal/CT-e, app do motorista (PWA) e portal do cliente. Next.js 16 + Prisma + PostgreSQL.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

171 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TMS

Sistema de gestão de transportes para operação terrestre, de carga fracionada e dedicada: coletas, manifestos, motoristas, veículos, financeiro, notas fiscais (leitura de XML de NF-e), app do motorista e portal do cliente.

Nasceu como o sistema da Mello Transportes Rio Preto (este repositório se chamava Mello) e ainda roda a operação dela. Em 06/10/2026 o site institucional saiu daqui para avilaops/mellotransportesriopreto.com.br; o que ficou é só o sistema.

O que tem aqui

Área Rota Quem acessa O que faz
Gestão /dashboard Equipe interna: ADMIN, DIRECTOR, OPERATION, FINANCE, COMMERCIAL, EXPEDITION, WAREHOUSE (cada um vê a sua parte: Perfis de acesso) Clientes, cotações (funil comercial), coletas, manifestos, motoristas, veículos e frota (manutenção, abastecimento, documentos, pneus, checklist, custos), equipe (ajudantes, ausências, adiantamentos e produtividade), ocorrências (chamados de clientes e da equipe), financeiro, notas fiscais (importação de XML de NF-e) e CT-e (emissão pela SEFAZ, pronta para homologação, e registro manual), mensageria (histórico dos avisos para sistemas de fora), auditoria, usuários
Motorista /driver DRIVER PWA com viagens, mapa, baixa de entrega com comprovante (fotos por tipo, ressalva) e fila offline, comprovantes devolvidos para refazer, tentativa de entrega sem sucesso, checklist do veículo da viagem e registro de ocorrência na entrega
Cliente /portal CLIENT Coletas (pedido, acompanhamento com rastreio e comprovante de entrega), faturas, minutas e atendimento (chamados) da própria empresa
API pública /api/cotacoes, /api/leads, /api/rastreio Site do transportador Recebe cotação e lead, responde o rastreio por CNPJ/CPF + código

Não há página pública: a raiz / leva quem já entrou para a própria área e todo o resto para /login.

O controle de acesso por perfil fica em src/proxy.ts e é conferido de novo no servidor em cada rota interna. Perfis são estritos: um ADMIN não entra em /driver nem em /portal, porque não tem motorista nem empresa vinculados.

Perfis de acesso

São nove perfis (enum Role): os sete da equipe interna, que entram em /dashboard, mais DRIVER (motorista, /driver) e CLIENT (cliente, /portal). O que cada perfil interno pode fazer está num arquivo só, src/lib/permissoes.ts: uma lista de capacidades (financeiro, clientesVer, deposito…) e os perfis que têm cada uma. Cada rota interna pede uma capacidade (requireStaff({ pode: "financeiro" })), o menu mostra só as telas que o perfil pode abrir (src/app/dashboard/menu.ts) e as respostas que escondem dinheiro decidem pela mesma lista. O perfil é lido do banco a cada chamada, não do token: troca de perfil vale na hora.

Área Administrador ADMIN Diretoria DIRECTOR Operação OPERATION Financeiro FINANCE Comercial COMMERCIAL Expedição EXPEDITION Conferência WAREHOUSE
Visão geral (contadores) sim, com receita sim, com receita sim sim, com receita sim sim sim
Clientes tudo tudo tudo lê; altera só condição de pagamento e limite de crédito tudo não não
Cotações (funil comercial) tudo tudo tudo não tudo não não
Tabelas de frete tudo lê e calcula lê e calcula lê e calcula tudo não não
Minutas (cargas) tudo tudo tudo lê; informa frete à mão lê lê; muda status lê
Depósito, conferência e etiquetas tudo tudo tudo não não não tudo
Manifestos e viagens tudo tudo tudo lê não tudo não
Despesas de viagem lança e aprova lança lança lê e aprova não lança não
Acerto e resultado da viagem lê lê não lê não não não
Comprovantes de entrega tudo tudo tudo não não tudo não
Ocorrências (chamados) tudo tudo tudo não tudo tudo tudo
Notas fiscais, CT-e, MDF-e e documentos da subcontratada tudo tudo tudo lê não lê não
Faturamento tudo lê não tudo não não não
Cobrança (painel) lê lê não lê não não não
Financeiro (lançamentos, fluxo, recibo) tudo lê não tudo não não não
Conciliação bancária (extrato OFX) tudo não não tudo não não não
Motoristas tudo tudo, sem alterar comissão tudo, sem ver comissão lê, com comissão não lê, sem comissão não
Veículos e frota tudo tudo tudo lê não lê; registra checklist não
Custos da frota lê lê não lê não não não
Equipe (ajudantes, ausências, produtividade) tudo tudo, com valores tudo, sem valores lê, com valores não lê, sem valores não
Adiantamentos da equipe tudo lê não tudo não não não
Remuneração variável: formas no cadastro (diária, km, entrega, fixo) tudo lê não vê lê; altera só a do ajudante não não não
Remuneração variável: fechamento mensal e demonstrativo lê, fecha e reabre lê não lê não não não
Parceiros de transporte (cadastro de agregados e terceiros) tudo tudo, sem alterar a contratação padrão tudo, sem ver valores lê, e altera só a contratação padrão não não não
Frete contratado (contrato da viagem, pagamentos, recibo e fechamento) tudo lê não tudo não não não
Relatórios lê lê não lê não não não
Mensageria lê e reenvia lê não não não não não
Auditoria lê lê não não não não não
Usuários tudo não não não não não não
Empresa, integração e parâmetros de cobrança tudo não não não não não não
  • Administrador e Operação não mudaram com a chegada dos outros cinco: toda rota que era "equipe interna" continua com os dois, e toda rota que era "só administrador" continua sem a Operação. Nas seções abaixo, "equipe interna" e "administrador" descrevem esses dois; os demais seguem a tabela.
  • Diretoria faz tudo o que a Operação faz e lê o que é dinheiro; não lança, não baixa, não fatura, não mexe em preço, usuário, empresa nem integração.
  • Financeiro não opera carga: não cria nem altera minuta, manifesto, veículo ou motorista. O percentual de comissão do motorista ele lê, mas só o administrador altera (mudar exige também o cadastro de motoristas).
  • Remuneração variável (Equipe): as formas do motorista (diária, km, entrega e fixo por viagem) seguem a regra da comissão, com as capacidades que já existiam (equipeValoresVer para ler, equipeValores mais motoristas para alterar); a do ajudante se altera com equipeValores. Fechar e reabrir o mês é a capacidade nova equipeFechamento, só do administrador: é o ato que congela os valores e lança (ou desfaz) a despesa no Financeiro. O motorista, no aplicativo, só vê o que as viagens dele renderam, e só se a empresa ligar.
  • Atribuir perfil é do administrador, em /dashboard/usuarios. No celular o cadastro de usuário novo fica recolhido atrás do botão "Novo usuário", para a lista caber na primeira tela. Tela aberta por quem não tem a capacidade mostra "Seu perfil não tem acesso a esta área.", sem citar perfil. Motorista continua nascendo pelo cadastro de motoristas, e cliente continua exigindo a empresa vinculada. A trava do último administrador vale para qualquer destino, e toda troca fica na auditoria (usuario.perfil, com o perfil de antes e o de depois).
  • Responsável por chamado pode ser qualquer usuário de um perfil que atende chamados (todos os internos menos o Financeiro).
  • Rota nova precisa de uma capacidade: tests/perfis.test.ts lê todas as rotas de src/app/api, recusa requireStaff sem capacidade e confere, perfil por perfil, o 403 e a passagem em cada uma.

Os valores novos do enum entram por prisma/sql/022-perfis.sql (só ALTER TYPE ... ADD VALUE IF NOT EXISTS; roda em transação única, psql -1 -f, e pode rodar de novo). Nenhum usuário troca de perfil sozinho.

Ainda não existe: perfil sob medida por empresa (a matriz é do código, igual para todas), mais de um perfil por usuário, restrição por filial ou por cliente dentro de um perfil, e tela própria de leitura para quem só lê (a Diretoria no Financeiro e o Financeiro em Clientes veem os mesmos botões; quem recusa a gravação é a API, com 403).

Quem consome a API pública

O site do transportador não chama o TMS do navegador: o servidor do site repassa as três rotas acima (rewrite), então não há CORS. O limite de tentativas do rastreio usa o x-forwarded-for, que precisa chegar até aqui com o IP do visitante.

Stack

  • Next.js 16 (App Router, output: "standalone"), React 19, TypeScript
  • Tailwind CSS 4 e componentes shadcn/Base UI
  • Prisma 7 com @prisma/adapter-pg e PostgreSQL 18
  • NextAuth 4 com login por e-mail e senha (bcrypt) e sessão JWT
  • Vitest contra Postgres real
  • Docker, GitHub Actions (GHCR) e Caddy no Hetzner

Esta versão do Next.js tem mudanças de API em relação a versões anteriores (por exemplo, middleware.ts virou proxy.ts). Antes de mexer, consulte node_modules/next/dist/docs/.

Rodando localmente

Requisitos: Node 22+ e um Postgres acessível. Tudo aqui precisa do banco.

npm install
cp .env.example .env          # preencha DATABASE_URL e NEXTAUTH_SECRET
npx prisma generate
npm run db:push               # cria as tabelas e aplica o isolamento por empresa
TENANT_SLUG=minha-transportadora TENANT_NAME="Minha Transportadora" \
  ADMIN_EMAIL=voce@exemplo.com ADMIN_PASSWORD=troque-por-12-caracteres npx tsx prisma/seed.ts
npm run dev

Para subir só o banco com Docker:

docker compose -f docker-compose.dev.yml up -d   # expõe em 127.0.0.1:5436 (usuário, senha e banco: tms)

Comandos

Comando O que faz
npm run dev Servidor de desenvolvimento
npm run build Build de produção (standalone)
npm start Sobe o build
npm run lint ESLint
npm run typecheck tsc --noEmit
npm test Vitest; exige DATABASE_URL apontando para um Postgres de teste

Variáveis de ambiente

Todas estão documentadas em .env.example. As essenciais:

Variável Uso
DATABASE_URL Conexão Postgres
NEXTAUTH_URL URL pública do sistema (https://tms.avilaops.com em produção)
NEXTAUTH_SECRET Assinatura do JWT de sessão
AVILAOPS_CLIENT_ID, AVILAOPS_CLIENT_SECRET Cliente OIDC do TMS no auth (o segredo é o OIDC_SEGREDO_TMS de lá)
TMS_EMPRESA_PADRAO Slug da empresa das rotas públicas de cotação e lead quando a requisição não informa empresa
FISCAL_MCP_URL Endereço do MCP único da empresa (https://mcp.avilaops.com/mcp), por onde o serviço fiscal gera o DANFE, o DACTE e o DAMDFE em PDF. Opcional: sem ela o recurso fica desligado e o botão não aparece
FISCAL_MCP_TOKEN Token de serviço do TMS no MCP único, enviado como Authorization: Bearer; só abre as ferramentas fiscal_*. Necessário quando FISCAL_MCP_URL está definida
RESPTEC_CSRT_ID, RESPTEC_CSRT Opcionais, as duas juntas: o código de segurança do responsável técnico, quando o fisco o fornece. Com elas o infRespTec leva idCSRT e hashCSRT; o CSRT é segredo e não sai do servidor (só o hash vai no XML)
VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT Opcionais: chaves do push no navegador (npx web-push generate-vapid-keys) e o contato de quem opera (mailto:). Sem as três o push fica desligado e só o sininho funciona: Notificações
TMS_CHAVE_DE_DADOS Opcional: chave (32 caracteres ou mais, openssl rand -base64 48) que cifra as credenciais do Mercado Pago e o certificado A1 de cada empresa. Sem ela a cobrança pelo Mercado Pago e o envio do certificado (emissão de CT-e) ficam desligados: Cobrança pelo Mercado Pago
MERCADO_PAGO_API Opcional: endereço da API do Mercado Pago (padrão https://api.mercadopago.com). Só os testes mudam
GEO_CONTATO Opcional: e-mail ou site de quem responde pelo uso do Nominatim (vai no User-Agent). Sem ela os endereços de entrega não são localizados e a rota segue pela cidade: Mapa, endereço e GPS
GEO_URL Opcional: servidor do Nominatim (padrão https://nominatim.openstreetmap.org). Troque por uma instância própria quando o volume crescer
ROTA_URL Opcional: servidor OSRM próprio para a distância por estrada na ordem sugerida. Sem ela a distância é em linha reta. O servidor de demonstração público do OSRM é recusado
NEXT_PUBLIC_MAPA_TILES Opcional: endereço https dos blocos de imagem do mapa, com {z}, {x} e {y} (padrão https://tile.openstreetmap.org/{z}/{x}/{y}.png). Lida na hora do build
TENANT_SLUG, TENANT_NAME, ADMIN_EMAIL, ADMIN_PASSWORD Seed: cria a empresa e o administrador dela

Várias empresas no mesmo sistema (multi-tenant)

Cada transportadora é uma linha de Tenant, e toda tabela de negócio tem tenantId. Todas dividem o mesmo banco; quem separa uma da outra é o Postgres, não o código de cada rota:

  • Políticas de segurança por linha (prisma/sql/010-rls.sql). Consulta feita em nome de uma empresa roda numa transação que troca para o papel tms_app e grava a empresa em app.tenant_id. Para esse papel, só existem as linhas daquela empresa: um findMany sem filtro, um UPDATE sem WHERE ou um id de outra empresa na URL não alcançam dado alheio.
  • tenantId preenchido pelo banco. O valor padrão da coluna lê app.tenant_id. O código não informa a empresa ao gravar, e gravar fora de uma transação de empresa falha em vez de cair na empresa errada.
  • Referência entre empresas é recusada por gatilho (tms_mesmo_tenant): chave estrangeira não passa por política, e sem isso daria para apontar uma coleta para o cliente de outra empresa sabendo o id.
  • Unicidade por empresa: CNPJ do cliente, CPF do motorista, placa, e-mail do usuário e chave de NF-e importada são únicos dentro da empresa. Código de rastreio e chave de CT-e continuam únicos no sistema inteiro.

No código (src/lib/prisma.ts):

Uso O que é
prisma (export padrão) Empresa tirada da sessão de quem fez a requisição. Sem sessão, lança erro: não há consulta sem empresa
transacao(fn) Transação interativa na empresa da sessão. O cliente padrão não tem $transaction de propósito
paraEmpresa(id) Empresa informada por quem chama: rota pública que recebe a empresa por parâmetro
sistema Sem empresa e sem política. Só para o que acontece antes de haver empresa: login, saúde, rastreio público por código, seed

Login. O TMS não tem senha própria. A tela /login manda direto para o login único da Ávila Ops (auth.avilaops.com), por OIDC (authorization code + PKCE; provedor avilaops em src/lib/auth.ts). É o auth que autentica (senha, Google, Microsoft, Facebook), confere se a conta foi liberada para o app tms e cobra o segundo fator; o TMS recebe só quem passou. Por ser OIDC e não cookie de .avilaops.com, funciona também em domínio próprio de cliente: basta registrar o endereço de retorno do domínio novo em src/lib/oidc.ts do auth. Sair encerra também a sessão do auth (src/lib/sair.ts).

Em que empresa a pessoa entra (resolverEntrada). O e-mail é único por empresa, não no sistema. Com cadastro em uma empresa só, entra direto. Com cadastro em mais de uma, escolhe em /empresa. Sem cadastro em nenhuma, não entra. A empresa escolhida vai no token da sessão (tenantId), que dura 8 horas, como a sessão do auth.

Equipe da Ávila Ops (papel ADMIN no auth). Opera a plataforma: em /plataforma/empresas cria empresa com o primeiro administrador, renomeia, desativa e reativa (desativar impede a entrada e as rotas públicas, sem apagar nada). Em /empresa entra em qualquer empresa ativa; na primeira entrada nasce um administrador "Nome (Ávila Ops)", visível na lista de usuários daquela empresa. Administrador de transportadora não é equipe e não vê outras empresas.

Cadastrar alguém já libera o acesso (src/lib/acessos.ts). Criar usuário, motorista ou empresa (com o primeiro administrador) chama a API de provisionamento do auth (POST /api/provisionamento/acessos, autenticada com o mesmo cliente OIDC do login), que garante a conta e libera o app tms. O TMS não tem caixa de e-mail: pede ao auth (enviarConvite) que escreva para a pessoa. Conta nova, ou que a pessoa ainda não assumiu, recebe o convite: endereço de uso único, válido por 7 dias, para criar a senha; ao salvar, ela já cai em /login do TMS, que abre a sessão. Conta que já tinha senha recebe só o endereço do sistema. O endereço da senha não passa pelo TMS; só volta na resposta (e aí a tela o mostra uma vez, com "Copiar" e "Enviar por WhatsApp") quando o e-mail não saiu. Se o auth não responder, o cadastro vale mesmo assim. "Liberar acesso" (POST /api/usuarios/[id]/acesso) refaz tudo e serve de "enviar convite de novo". O resultado de cada pedido fica no cadastro (User.inviteStatus, inviteDetail, inviteAt; script prisma/sql/013-invite-status.sql) e aparece na lista de usuários, sob o e-mail. Trocar o e-mail de um motorista libera o novo e revoga o antigo, se nenhum outro cadastro o usa.

Nenhuma tela pede senha. A coluna password do usuário continua no banco por enquanto, com um valor sem-senha:… que não é hash de nada e que nenhum código lê.

Tabela nova. Declare tenantId igual ao das outras (com o @default(dbgenerated(...)) e a relação com Tenant), rode npm run db:push e, se ela referencia outra tabela, acrescente o par em referencias, no 010-rls.sql. A política e as permissões são criadas sozinhas para toda tabela que tenha tenantId.

Rotas públicas. POST /api/leads e POST /api/cotacoes aceitam empresa (slug) no corpo; sem ele vale TMS_EMPRESA_PADRAO. Empresa inexistente ou desativada responde 404. GET /api/rastreio não precisa de empresa: o código de rastreio é único no sistema. A página pública /rastreio usa essa rota: pede o CNPJ ou CPF de quem contratou e o código, mostra a transportadora, a situação e a linha do tempo, e aceita link já preenchido (/rastreio?cnpj=…&codigo=…).

Os testes em tests/multi-tenant.test.ts provam o isolamento contra o banco, com duas empresas.

Tabelas de frete e cotação

O preço do frete sai de tabelas cadastradas no painel (/dashboard/tabelas-frete), nunca de valor fixo no código.

  • Tabela (FreightTable): peso coberto pelo frete mínimo, valor do kg excedente, fator de cubagem, valor de nota coberto e percentual sobre o que passa dele, máximo de volumes, percentuais de reentrega e devolução, e validade. Uma por empresa é a padrão; marcar outra como padrão desmarca a anterior.
  • Cidades (FreightTableCity): frete mínimo e prazo de cada cidade atendida. A tela aceita a lista colada da planilha (Cidade;Mínimo;Prazo, com vírgula decimal e prazos como Até 24h) e grava a lista inteira de uma vez.
  • Cliente: pode ter uma tabela negociada (Client.freightTableId). Sem ela, vale a padrão.
  • A conta (src/lib/frete.ts, calcularFrete): mínimo da cidade + excedente sobre o maior entre peso real e cubado + percentual sobre a nota acima do limite. Volumes acima do combinado, nota acima do limite sem percentual definido e cidade só com veículo dedicado saem como aviso, sem impedir o cálculo. O nome da cidade casa sem acento, caixa, UF ou apóstrofo.
  • Qual tabela vale (tabelaVigente): a do cliente, se ativa e dentro da validade; senão a padrão; senão nenhuma.
  • Cotação do site (POST /api/leads e /api/cotacoes, src/lib/cotacao.ts): o valor estimado vem da tabela padrão da empresa. Sem tabela em vigor, ou com a cidade fora dela, o pedido é gravado sem valor para o comercial responder.
  • Permissão: criar e alterar tabela e cidades é do administrador; a operação consulta e usa o simulador (POST /api/tabelas-frete/calcular).
  • Cotações (/dashboard/crm, menu "Cotações"; src/lib/crm.ts): tudo o que chegou pedindo preço, em duas visões sobre a mesma lista. O funil é o quadro por etapa (Novos, Em contato, Convertidos, Perdidos); a lista é a tabela para achar, cobrar e exportar. Os filtros do topo (busca por número, empresa, cidade ou telefone; período; etapa; responsável) valem para as duas e vão para a rota (GET /api/dashboard/crm?status=&dias=&assigneeId=&busca=); o resumo (cotações, conversão, fechado × cotado, paradas) é do que está filtrado. "Exportar CSV" baixa a lista filtrada, com ponto e vírgula e vírgula decimal.
    • Número: cada cotação ganha um número sequencial por empresa na criação, venha do site ou do painel (QuoteLead.number, src/lib/crm-db.ts, com a mesma trava por empresa do chamado e da fatura). É por ele que a equipe fala da cotação com o cliente.
    • Canal: SITE para o formulário público; WHATSAPP, TELEFONE ou EMAIL para o pedido que a equipe registra no painel ("Registrar cotação", POST /api/dashboard/crm): empresa, telefone ou e-mail (ao menos um), origem, destino, volumes, peso e valor da nota. O valor estimado sai da tabela padrão, como no site; sem tabela para o destino, fica em branco para o comercial preencher. Sem responsável informado, fica com quem registrou. Vai para a auditoria (cotacao.registrar).
    • Responsável: qualquer usuário da empresa com a capacidade crm (GET /api/dashboard/crm/equipe); usuário de outro perfil ou de outra empresa é recusado (400).
    • Parada: cotação em aberto (Novos ou Em contato) sem atividade há DIAS_PARA_COBRAR (2) dias ou mais. Aparece com o selo "parada há N dias", no contador do topo (que também filtra) e no filtro de etapa "Paradas". Troca de status e "Registrar contato" (PATCH { contato: true }, auditoria cotacao.contato) renovam a atividade; mudar valor ou responsável, não.
    • Motivo da perda: ao marcar Perdida, a cotação pede o motivo (lista fechada: preço, prazo, desistência, sem retorno, outra transportadora, outro). Sair de Perdidos apaga o motivo.
  • Cotação aprovada vira coleta: no funil ou na lista, "Converter em coleta" pede cliente, remetente e destinatário e cria a coleta confirmada, com código de rastreio e a carga do pedido (POST /api/dashboard/crm/[id]/converter). O lead convertido fica ligado à coleta e travado: não muda mais de status, valor, responsável nem motivo. "Convertido" só existe pela conversão, e lead perdido precisa voltar para "Em contato" antes de converter. A coleta nasce com o frete da tabela, não com o valor estimado da cotação: quando os dois diferem, a confirmação mostra os dois, e quem quiser cobrar o valor da cotação edita o frete na coleta. O valor da nota vem do pedido; apagar o campo na conversão grava a coleta sem valor de nota.
  • Banco em produção: as colunas novas de QuoteLead e o backfill do número entram por prisma/sql/039-cotacoes.sql, antes do código, seguido de npm run db:rls (o gatilho passa a conferir que o responsável é da mesma empresa).

Proposta comercial. Em uma cotação aberta, “Preparar proposta” pede o prazo de entrega e a validade, monta uma mensagem com número, rota, carga e valor estimado e a copia para colar no WhatsApp. Prazo e validade são informados pelo comercial, não presumidos pela tabela; copiar não registra contato nem muda a etapa, porque não prova que a mensagem foi enviada. Se a área de transferência falhar, a prévia continua selecionável para cópia manual. Sem preço estimado, a tela orienta preencher o valor antes.

Ainda não existe nas cotações: a cobrança automática da cotação parada, o histórico de cada cotação na própria tela (as mudanças estão na auditoria), a cotação por faixa de peso e região (a tabela é por cidade, com mínimo e excedente por kg) e o robô de atendimento que monta a cotação a partir da conversa.

  • Frete na coleta (src/lib/frete-coleta.ts): toda coleta, do painel ou do portal, nasce com o frete calculado pela tabela do cliente (ou pela padrão), o prazo e a composição da conta (freightValue, freightDeadlineHours, freightDetails). Mudar destino, peso, volumes ou valor da nota refaz o cálculo. O operador pode fixar o valor à mão na edição (freightManual); apagar o campo devolve a conta para a tabela. Sem tabela em vigor, ou com o destino fora dela, a coleta fica "a cotar". O cliente vê no portal só o valor e o prazo.

Faturamento

Em /dashboard/faturamento o administrador cobra de um cliente o frete das cargas entregues.

  • Faturável é a carga entregue, com frete definido e fora de fatura. Carga entregue "a cotar" aparece à parte, com o campo para informar o frete (PATCH /api/coletas/[id]/frete, que vale em qualquer status enquanto a carga não foi faturada).
  • Emitir (POST /api/faturas) soma os fretes, dá o próximo número da empresa, prende as cargas na fatura e cria o lançamento a receber no financeiro. É esse lançamento que o cliente vê em "Faturas" no portal. A numeração é por empresa e uma emissão por vez (trava do Postgres), então não repete nem pula.
  • Pagar, reabrir, cancelar (PATCH /api/faturas/[id]): o lançamento acompanha a fatura na mesma transação. Pagar abre a baixa e aceita juros, multa e desconto, que vão para o lançamento da fatura (o total da fatura é o emitido e não muda); ver Financeiro. Cancelar solta as cargas, que voltam a ser faturáveis; o número cancelado não é reaproveitado, e fatura paga precisa ser reaberta antes de cancelar.
  • Depois de faturada, a carga não muda mais de frete: a fatura já saiu com ele.
  • /dashboard/faturamento/[id] é a fatura pronta para imprimir ou salvar em PDF.

Ainda não há boleto nem envio automático do aviso de cobrança, e o Pix é estático: a baixa é manual, ou pela conciliação do extrato bancário. A posição do que está em aberto, o aviso para copiar e o recibo ficam em Cobrança.

Financeiro: contas a pagar, a receber e fluxo de caixa

Em /dashboard/financeiro, só para o administrador. As regras e as contas ficam em src/lib/financeiro.ts.

  • Lançamento (FinancialTransaction): receita ou despesa, valor, vencimento, cliente ou fornecedor, categoria, centro de custo e observação. Quando é pago guarda a data (paidAt) e a forma de pagamento.
  • Baixa com juros, multa e desconto: receber um título abre a baixa, com os três campos (valores em reais, maiores ou iguais a zero). O valor original (amount) não muda; o que entrou de fato fica em paidAmount (original + juros + multa − desconto, nunca negativo), junto de interest, fine e discount. Baixa sem encargo, ou com os três zerados, é a de sempre e não guarda nada; reabrir apaga os quatro. Vale só para título a receber: despesa é paga pelo valor. O fluxo de caixa realizado, o "recebido no mês", os relatórios, a receita do painel e o recibo usam o valor recebido quando ele existe; o previsto e o que está em aberto continuam no valor original.
  • Sugestão de encargos: título vencido abre a baixa com a multa e os juros já calculados (encargosSugeridos em src/lib/cobranca.ts): multa é o percentual sobre o valor, uma vez; juros são simples e proporcionais aos dias de atraso (o percentual do mês dividido por 30). É só sugestão: o operador altera ou apaga. Os percentuais são da empresa (padrão de 2% e 1% ao mês) e ficam em Empresa, seção Cobrança.
  • Centro de custo: texto livre e opcional (filial, rota, veículo…), com sugestão dos já usados; a lista filtra por ele (?centro=, sem diferenciar maiúsculas) e o relatório financeiro soma as despesas pagas no período por centro.
  • Situação é calculada: pago, em aberto ou vencido. Vence no fim do dia do vencimento, no relógio do Brasil; o vencimento em si é um dia do calendário e é lido em UTC, para não aparecer um dia antes.
  • Rotas: GET /api/financeiro (filtros tipo, situacao, de, ate, centro), POST com validação, PATCH /api/financeiro/[id] (editar, action: "pagar" — com juros, multa e desconto opcionais — ou "reabrir") e DELETE.
  • Lançamento que veio de fatura não é pago, editado nem excluído por aqui: responde 409 e manda para o Faturamento, que mantém fatura e lançamento em sincronia. Pagar a fatura grava o paidAt do lançamento.
  • Fluxo de caixa (GET /api/financeiro/fluxo?de=AAAA-MM&ate=AAAA-MM, padrão de três meses para trás e três para a frente, no máximo 36): por mês, o previsto (o que vence no mês) e o realizado (o que foi pago ou recebido no mês), com saldo e acumulado. O resumo dos cartões é sempre de todos os lançamentos: conta vencida há um ano continua vencida.

Conciliação bancária por extrato (OFX)

Em /dashboard/financeiro/conciliacao (botão Conciliação no Financeiro), para quem lança no financeiro (ADMIN e FINANCE). Não há ligação com banco: o administrador exporta o extrato no site do banco, em OFX (o formato "Money", que todo banco brasileiro oferece), e envia o arquivo. As regras ficam em src/lib/conciliacao.ts e o leitor em src/lib/ofx.ts, os dois em funções puras e sem dependência nova.

  • Leitor de OFX: lê OFX 1.x (SGML, campos sem fechamento) e 2.x (XML), em UTF-8 ou Windows-1252; tira a conta (banco, agência e o número mascarado: só o fim, ••••1234), o período e cada movimentação (identificador FITID, data, valor com sinal, tipo e descrição). Data no formato AAAAMMDD[HHMMSS][.XXX][fuso], valendo o dia escrito; valor com ponto ou vírgula. Recusa, com mensagem clara, arquivo vazio, que não é OFX, sem movimentação, acima de 2 MB, ou com movimentação sem data ou valor legível. Movimentação de valor zero ("saldo anterior") é descartada.
  • O que fica guardado (BankStatementLine): uma linha por movimentação, única por empresa + conta + FITID. Reenviar o mesmo extrato, ou um período que se sobrepõe, não duplica: a resposta diz quantas eram novas e quantas já estavam. O número inteiro da conta não é gravado (fica o fim e um resumo criptográfico, que é o que separa uma conta da outra). FITID repetido no mesmo arquivo ganha #2, #3; movimentação sem FITID recebe um identificador montado com dia, valor e descrição.
  • Sugestão: cada linha pendente vem com os lançamentos candidatos e o motivo. Mesmo sinal (crédito com a receber, débito com a pagar); valor igual ao do título em aberto, ou ao valor recebido de fato no título já pago; vencimento (título em aberto) ou pagamento (título pago) a até 5 dias da data do extrato; e identificação na descrição: o txid do Pix (FAT000123), o número da fatura ("fatura 123") ou parte do nome do cliente ou fornecedor. Candidato forte tem valor igual e data na janela ou identificação por txid ou fatura. A linha é certeira quando tem exatamente um candidato forte (o identificado passa na frente do que só casa pela data) e esse lançamento não é o certeiro de outra linha. Título em aberto de valor igual e vencimento fora da janela (cliente que pagou atrasado) e título com o txid na descrição mas valor diferente aparecem como sugestão fraca, nunca certeira.
  • Por linha (PATCH /api/financeiro/conciliacao/[id]): Conciliar com o candidato; Outro (escolher outro lançamento do mesmo lado, com busca); Criar um lançamento já pago a partir da linha, com categoria (tarifa bancária, por exemplo); ou Ignorar. Certeiros (n) concilia em lote as linhas certeiras (POST /api/financeiro/conciliacao/certeiros), cada uma na sua transação. Desfazer devolve a linha a pendente.
  • Conciliar dá a baixa pela regra de sempre. Lançamento em aberto recebe a baixa do Financeiro (situacaoDaBaixa, em src/lib/financeiro-db.ts, a mesma da tela), com a data do extrato e a forma de pagamento inferida da descrição (Pix, boleto, transferência, cartão, dinheiro; em branco quando o extrato não diz). Em título a receber de valor diferente, a diferença a mais entra como juros e a diferença a menos como desconto (o extrato não separa multa de juros; quem chama a rota pode mandar juros, multa e desconto, que têm de fechar com o valor do extrato). Em título a pagar, valor diferente é recusado: corrige-se o lançamento antes. Lançamento já pago só é ligado à linha, sem mexer nele.
  • Lançamento de fatura: conciliar paga a fatura, pelo mesmo caminho do Faturamento (alterarFatura, em src/lib/faturas-db.ts), e o lançamento a acompanha; juros e desconto vão para o lançamento e o total da fatura não muda. Desfazer reabre a fatura.
  • Desfazer: se a baixa veio da conciliação, o título (ou a fatura) é reaberto; se o lançamento já estava pago, ou nasceu da linha, ele continua pago e só perde a ligação (volta a aparecer como candidato da mesma linha). Enquanto está conciliado, o lançamento não é reaberto nem excluído pelo Financeiro nem pelo Faturamento (409): o caminho é desfazer a conciliação.
  • Rotas: GET /api/financeiro/conciliacao?situacao=pendentes|conciliadas|ignoradas (até 300 linhas por vez, com a contagem de cada situação), POST /api/financeiro/conciliacao (o corpo é o arquivo, como o banco exportou), e as duas acima. Tudo fica na Auditoria (conciliacao.importar, .conciliar, .criar, .ignorar, .desfazer, além da baixa e da reabertura do lançamento ou da fatura). Linha e lançamento de outra empresa respondem 404, e o banco recusa ligar uma linha a lançamento de outra empresa.

A tabela é criada por prisma/sql/024-conciliacao.sql; rode npm run db:rls depois dela.

Ainda não existe na conciliação: ligação direta com o banco (Open Finance, API ou arquivo de retorno CNAB: o extrato é sempre exportado e enviado à mão), extrato em CSV ou PDF, uma linha do extrato para vários lançamentos (cliente que paga três faturas num Pix só) ou vários créditos para um título (recebimento parcial), saldo da conta e conferência de saldo, cadastro de contas bancárias (a conta aparece só como veio no arquivo), regra automática por descrição ("toda TARIFA vira despesa de tarifa") e exclusão de linha importada (a que não interessa é ignorada). A janela de 5 dias é fixa no código (JANELA_PADRAO).

Ainda não existe: boleto, Pix dinâmico (com confirmação automática do pagamento), ligação direta com banco (a conciliação é por extrato enviado à mão), parcelamento, lançamento recorrente e acordo de dívida. A baixa é sempre do título inteiro (não há recebimento parcial), os encargos não entram no lançamento já criado como pago (só na baixa), e o centro de custo é um texto no lançamento, sem cadastro nem rateio.

Cobrança

Em /dashboard/cobranca, só para o administrador. As contas ficam em src/lib/cobranca.ts e não mudam o banco: a tela só lê.

  • Posição por cliente (GET /api/financeiro/cobranca): os lançamentos a receber em aberto, agrupados por quem deve (o cliente; sem cliente, o pagador digitado; sem nenhum, "Sem cliente informado"), com total, vencido e o maior atraso em dias. Quem mais deve em atraso aparece primeiro.
  • Faixas de atraso: a vencer, 1 a 30, 31 a 60, 61 a 90 e mais de 90 dias, contados do vencimento até hoje no relógio do Brasil. Título sem vencimento conta em "a vencer".
  • Aviso de cobrança: o texto já redigido com os títulos do cliente (lembrete quando nada venceu, atraso quando algo venceu), para copiar e mandar pelo canal de costume. O sistema não envia nada. Com a chave Pix cadastrada (abaixo), o aviso leva o Pix Copia e Cola de cada título; sem chave, o texto não traz dado de pagamento.
  • Recibo (GET /api/financeiro/[id]/recibo, tela /dashboard/financeiro/recibo/[id]): só de receita já recebida, para imprimir ou salvar em PDF. Chega-se a ele pelo link "Recibo" no Financeiro e na fatura paga.

A baixa continua no Faturamento e no Financeiro (com juros, multa e desconto); título pago sai da posição, e o recibo sai pelo valor recebido, com a composição quando houve encargo. A posição e o aviso mostram o valor original do título, sem juros nem multa. Parcelas e o registro de que o aviso foi mandado não existem.

Cobrança por Pix (Copia e Cola estático)

O administrador cadastra em Empresa > Cobrança a chave Pix da transportadora (CPF, CNPJ, e-mail, telefone ou aleatória, conferida pelo formato e, em CPF e CNPJ, pelo dígito verificador), o nome do recebedor (até 25 letras) e a cidade (até 15), que são os limites do padrão. A partir daí o sistema monta o Pix Copia e Cola de cada título a receber em aberto, com o valor do título e um identificador (FAT000123 para a fatura nº 123; TIT + o começo do id para lançamento sem fatura). O código é o BR Code do Banco Central (campos 00, 26, 52, 53, 54, 58, 59, 60, 62 e o CRC16 no 63), gerado por função pura em src/lib/pix.ts, sem banco e sem dependência; nome e cidade saem sem acento.

Onde aparece, com botão de copiar: na fatura do painel (/dashboard/faturamento/[id], enquanto em aberto), nas faturas do portal do cliente ("Pagar com Pix" em cada título em aberto), no texto do aviso de cobrança, e nos avisos fatura.emitida e cobranca.vencida da integração (campo pixCopiaECola).

É Pix estático: o pagamento NÃO dá baixa sozinho. O sistema não fala com banco nenhum e não sabe se o cliente pagou; quem recebe confere o extrato (o identificador aparece nele) e dá a baixa à mão, ou envia o extrato em OFX para a conciliação, que reconhece o identificador e sugere a fatura. As telas dizem isso ao lado do código. O valor é o original do título, sem juros nem multa.

Ainda não existe no Pix estático: QR Code (o projeto não tem gerador de QR e não se acrescentou dependência: sai só o código para copiar e colar) e mais de uma chave por empresa. QR Code, boleto e baixa automática existem pela cobrança do Mercado Pago, abaixo.

Cobrança pelo Mercado Pago

Pix dinâmico (com QR Code) e boleto, com baixa automática: cada transportadora liga a própria conta do Mercado Pago, o TMS cria a cobrança lá e paga a fatura sozinho quando o Mercado Pago avisa. Regras em src/lib/cobranca-gateway.ts, gravação em src/lib/cobranca-gateway-db.ts, cliente HTTP (só fetch) em src/lib/mercado-pago.ts, cifra em src/lib/cifra.ts.

Passo a passo para a transportadora ligar a conta (só o administrador):

  1. No painel de desenvolvedores do Mercado Pago, crie uma aplicação e copie o Access Token de produção (começa com APP_USR-).
  2. No TMS, abra Empresa > Cobrança > Mercado Pago e copie o endereço de webhook mostrado ali (<NEXTAUTH_URL>/api/pagamentos/mercado-pago/<slug da empresa>).
  3. Na aplicação do Mercado Pago, em Webhooks, cadastre esse endereço em modo produção, marque o evento Pagamentos e copie a assinatura secreta que o painel gera.
  4. De volta ao TMS, cole o Access Token e a assinatura secreta e clique em Ligar conta. Depois, Testar conexão: a tela mostra o nome da conta.
  5. Para o boleto, a conta precisa ter o meio de pagamento habilitado, e o cadastro do cliente precisa de e-mail, CNPJ ou CPF e o endereço escrito como Rua, número - Bairro, Cidade - UF, CEP 00000-000 (é o formato que a busca por CNPJ preenche). O Pix pede só e-mail e CNPJ ou CPF. Dado que falta vira erro dizendo o que completar.

Credenciais. O Access Token e a assinatura secreta ficam cifrados no banco (AES-256-GCM, chave derivada de TMS_CHAVE_DE_DADOS, amarrada à empresa e ao campo) e não voltam para a tela, para log nem para a auditoria: a leitura devolve só "configurado" e os 4 últimos caracteres de cada um. Sem TMS_CHAVE_DE_DADOS o recurso fica desligado e a tela explica. Trocar a variável torna ilegível o que já foi gravado: cada empresa cadastra de novo.

Gerar a cobrança. Na fatura em aberto do painel, Gerar Pix e Gerar boleto (quem tem faturamento): o valor é o total da fatura, a referência (external_reference) é o id da fatura, e o vencimento é o fim do dia do vencimento da fatura, dentro do que o Mercado Pago aceita (Pix: no mínimo 1 dia; boleto: no mínimo 3; os dois: no máximo 29 dias à frente). A fatura pode ter várias cobranças (gerou de novo depois de vencer), mas só uma em aberto por tipo. Cada tentativa tem chave de idempotência: se o Mercado Pago não responder, a cobrança fica "em criação" e gerar de novo repete o mesmo pedido, sem nascer pagamento em dobro.

Onde aparece. Na fatura do painel e nas faturas do portal do cliente: o Pix Copia e Cola com botão de copiar, a imagem do QR Code que o Mercado Pago devolve, e o boleto com link e linha digitável (quando ele a devolve). Com Pix dinâmico em aberto, ele entra no lugar do estático. O aviso fatura.emitida da integração leva o copia-e-cola (em pixCopiaECola) e cobranca (pix e boleto, com link e venceEm) quando a cobrança já existe na hora da entrega.

Baixa automática. O webhook (POST /api/pagamentos/mercado-pago/[empresa], público) acha a empresa pelo slug, confere a assinatura do aviso com o segredo dela (x-signature: HMAC-SHA256 de id:<data.id>;request-id:<x-request-id>;ts:<ts>;, comparado em tempo constante; não confere, 401) e não confia no corpo: busca o pagamento no Mercado Pago com o token da empresa e usa o que a API responde. Aprovado, em reais, com a referência da fatura, do valor da cobrança, e a fatura em aberto: paga pelo mesmo caminho do Faturamento, com a data do pagamento do gateway e a forma Pix ou Boleto; a diferença entre o valor e o que foi pago vira juros ou desconto, como na conciliação. O ator na auditoria é "Mercado Pago", e quem tem financeiro recebe o aviso no sininho. Aviso repetido não paga duas vezes. Cancelado e vencido encerram a cobrança.

Na dúvida não paga. Pagamento de outro valor, em outra moeda, com outra referência, de fatura já paga por fora (possível recebimento em dobro) ou cancelada, estorno depois da baixa, e pagamento aprovado sem cobrança correspondente: a fatura não é mexida, a cobrança fica "a conferir" com o motivo, e o financeiro é avisado. Fatura paga pelo Mercado Pago não é reaberta pelo sistema.

Quando o aviso não chega. Atualizar situação, na cobrança, consulta o Mercado Pago na hora; e o servidor confere a cada 10 minutos as cobranças em aberto criadas nas últimas 72 horas.

Rotas: GET, PUT e DELETE /api/empresa/gateway e POST /api/empresa/gateway/teste (empresa); POST /api/faturas/[id]/cobrancas e POST /api/faturas/[id]/cobrancas/[cobrancaId] (faturamento); GET /api/faturas/[id] passa a trazer cobrancas e gateway; POST /api/pagamentos/mercado-pago/[empresa] (público, com assinatura e limite de 120 avisos por minuto por empresa). Tabelas PaymentGateway e PaymentCharge, criadas por prisma/sql/025-gateway.sql; rode npm run db:rls depois dela.

Limites. O limite de avisos é em memória, por processo. A conferência periódica olha 50 cobranças por volta. O boleto sai como bolbradesco, o meio de boleto do Mercado Pago no Brasil. O endereço do pagador é lido do texto do cadastro: fora do formato, o boleto é recusado com o que falta. O notification_url de cada cobrança só é enviado quando NEXTAUTH_URL é https público; o endereço cadastrado no painel do Mercado Pago vale de qualquer forma. Nenhum teste automatizado chama o Mercado Pago: eles apontam MERCADO_PAGO_API para um servidor local que imita a API.

Ainda não existe: cartão, assinatura ou recorrência, split, estorno pelo TMS, outros gateways, cancelar no Mercado Pago a cobrança de uma fatura paga por fora ou cancelada (ela segue pagável lá até vencer; se for paga, fica "a conferir"), o cliente gerar o próprio Pix pelo portal (quem gera é a transportadora) e juros e multa embutidos na cobrança de fatura vencida (o valor é o total da fatura).

Relatórios

Em /dashboard/relatorios, só para o administrador. Um período em meses (GET /api/relatorios?de=AAAA-MM&ate=AAAA-MM, padrão do mês corrente e os dois anteriores, no máximo 36), no relógio do Brasil. As contas ficam em src/lib/relatorios.ts e não mudam o banco: a tela só lê.

  • Operação: cargas criadas no período, por status; e as entregas feitas no período, com quantas chegaram no prazo, o tempo médio e a conta por motorista. O prazo é o da tabela de frete e corre de "Coletado" a "Entregue" no histórico da carga. Entrega sem prazo ou sem as duas datas fica como "sem medição", fora da taxa.

  • Comercial: cotações recebidas no período por status, a conversão (as que viraram coleta sobre todas) e o frete das cargas criadas no período, por cliente. Carga cancelada ou recusada não entra no frete; carga a cotar conta à parte.

  • Financeiro: recebido, pago e resultado pela data do pagamento (o recebido é o que entrou de fato, com juros, multa e desconto da baixa), despesas pagas por categoria e por centro de custo, e a inadimplência (quanto do que há a receber em aberto já venceu), que é a posição de hoje e não a do período.

  • Resultado (campo resultado da mesma resposta; aba própria na tela, e o DRE também na aba Financeiro):

    • DRE básico: a receita recebida e as despesas pagas no período, por categoria do lançamento, e o que sobrou. É regime de caixa: vale a data do pagamento e o valor que entrou de fato; o que foi faturado e não recebido não aparece.
    • Margem por cliente: o frete das cargas entregues no período menos a parte do custo das viagens que as levaram. O custo da viagem (despesas aprovadas mais combustível) é rateado pelo peso: cada carga leva a fração do custo que o peso dela representa no peso total da viagem. Viagem sem peso divide por igual; carga entregue fora de viagem não leva custo.
    • Resultado por viagem: as viagens finalizadas no período, com frete, custo, resultado e margem (a mesma conta do acerto da viagem).

Ainda não há exportação para planilha ou PDF, DRE por competência (o que há é de caixa), nem margem por rota ou por veículo.

Frota

O que se controla de cada veículo, além do cadastro. As regras e as contas ficam em src/lib/frota.ts; as contas são funções puras e não gravam nada (consumo, custo por km e situação de documento são calculados na leitura).

Dados para o MDF-e (no cadastro do veículo, bloco fechado por padrão, todos opcionais): RENAVAM, tara, tipo de rodado, tipo de carroceria, UF de licenciamento e, para veículo de terceiro, o proprietário (CPF ou CNPJ, nome, RNTRC, IE, UF e tipo). A emissão do MDF-e lista o que falta em vez de emitir incompleto. Veículo de agregado ou de terceiro com cadastro em Parceiros aponta para ele (campo "Dono do veículo"): aí o proprietário do MDF-e é o cadastro do parceiro, e os campos de proprietário daqui ficam escondidos.

Capacidade e ocupação. O cadastro do veículo tem a capacidade em kg (Vehicle.capacity) e a cubagem útil em m³ (Vehicle.cubicCapacity, campo "Cubagem (m³)", opcional). São as duas medidas que a viagem confere contra as cargas: a soma do peso (Collection.weight) e da cubagem (Collection.cubicMeters) das cargas da viagem. As contas são funções puras em src/lib/ocupacao.ts: carga sem cubagem informada não soma (e a conta diz quantas ficaram de fora), a comparação arredonda o peso em 0,1 kg e a cubagem em 0,001 m³ para a soma de frações não acusar excesso que não existe, e o percentual é inteiro (acima da capacidade ele sobe para o inteiro seguinte: nunca aparece "100%" ao lado de um aviso). Veículo sem capacidade cadastrada não gera aviso: aparece "capacidade não cadastrada". Onde isso aparece e o que acontece na saída está em Viagem. O tipo do veículo (type), o peso máximo (maxWeight) e os dados do MDF-e não entram em regra nenhuma, e o tipo do veículo não muda o cálculo do frete (isso é da tabela de frete).

Tela do veículo (/dashboard/veiculos/[id], pelo link "Manutenção e frota" da lista de veículos), com uma aba por assunto. O endereço antigo da manutenção (/dashboard/veiculos/[id]/manutencao) leva para lá.

  • Manutenção (GET e POST /api/veiculos/[id]/manutencao): serviço, custo, data e, agora, tipo (preventiva ou corretiva) e hodômetro, os dois opcionais. Registrar continua lançando a despesa no Financeiro.
  • Abastecimento (GET e POST /api/veiculos/[id]/abastecimentos, DELETE .../[registroId]): data, litros, valor total, hodômetro, posto e motorista (os dois últimos opcionais). O consumo (km/l) e o custo por km são medidos de um abastecimento para o seguinte, pelo hodômetro (conta do tanque cheio). Fica sem medição, em vez de sair com número errado: o primeiro abastecimento, o que tem hodômetro igual ou menor que o anterior, e o que tem litros zerados.
  • Documentos (GET e POST /api/veiculos/[id]/documentos, PATCH e DELETE .../[registroId]): licenciamento (CRLV), seguro, ANTT, tacógrafo ou outro, com número, vencimento e observação. A situação é calculada: em dia, a vencer (hoje ou nos próximos 30 dias) ou vencido. Vence no fim do dia do vencimento, no relógio do Brasil; o vencimento é um dia do calendário, lido em UTC, como no financeiro. Renovar é editar o vencimento.
  • Pneus (GET e POST /api/veiculos/[id]/pneus, PATCH e DELETE .../[registroId]): posição, marca e modelo, data e km de instalação, km de retirada (em branco enquanto está em uso) e observação. Registro simples, sem estoque.
  • Checklist (GET e POST /api/veiculos/[id]/checklists): oito itens fixos (pneus, freios, luzes, óleo, água, documentos, limpeza, extintor), cada um OK ou com problema, mais hodômetro e observação. Guarda quem fez e quando; não se altera nem se apaga. O motorista registra pelo app, na viagem (/driver/viagem/[id]/checklist, POST /api/driver/checklists): o veículo não vem do aparelho, é o da viagem, que precisa ser dele e estar em rota. O checklist do motorista precisa de sinal (não entra na fila offline).
  • Custos (GET /api/veiculos/[id]/custos?de=AAAA-MM&ate=AAAA-MM, só administrador; padrão do mês corrente e os dois anteriores, no máximo 36): manutenção concluída, abastecimento, total, km rodados, custo por km e consumo médio. Os km saem dos hodômetros dos abastecimentos, partindo do último abastecimento anterior ao período; sem dois abastecimentos em sequência não há km nem custo por km.

Alertas (/dashboard/frota, no menu em Frota; GET /api/frota): documentos de veículo e CNHs vencidos ou a vencer em 30 dias, numa lista só (a CNH é lida do cadastro do motorista, Driver.cnhExpiry; motorista desativado não entra), e os veículos com a situação "Em manutenção". Para o administrador vai também o custo do mês corrente (manutenção concluída e abastecimento de todos os veículos); para a operação o campo nem vai na resposta.

Permissão: tudo é da equipe interna (ADMIN e OPERATION), menos os custos, que são do administrador. Veículo de outra empresa responde 404, como um id inventado.

Ainda não existe: integração com bomba ou cartão de combustível, multas, estoque de pneus, telemetria, aviso de vencimento por mensagem (o alerta é só na tela), manutenção preventiva programada por km, e anexo do documento (PDF ou foto). O abastecimento não lança despesa no Financeiro (só a manutenção lança), e a situação de uma manutenção já registrada não é alterada pela tela.

Equipe (recursos humanos operacional)

Em /dashboard/equipe (menu Frota), o controle simples de quem vai para a estrada: motoristas (os do cadastro de Motoristas) e ajudantes. Uma aba por assunto. As regras e as contas ficam em src/lib/equipe.ts; as contas são funções puras e não gravam nada (quem está ausente num dia, a diferença do acerto e a comissão são calculados na leitura).

  • Pessoas (GET /api/equipe): motoristas e ajudantes numa lista só, com quem está ausente hoje (o dia é o do relógio do Brasil). O ajudante (Helper: nome, CPF único por empresa, telefone, ativo) é cadastrado aqui (GET e POST /api/equipe/ajudantes, PATCH .../[id]); ele não tem login. O CPF é a chave do cadastro e não se altera, como no motorista.
  • Ausências (Absence; GET e POST /api/equipe/ausencias, PATCH e DELETE .../[id]): de um motorista ou de um ajudante, com tipo (férias, folga, atestado, falta, outro), primeiro e último dia (os dois contam; são dias do calendário, gravados à meia-noite UTC) e observação. GET ?dia=AAAA-MM-DD devolve só as que cobrem aquele dia. Na montagem de viagem (/dashboard/manifestos), escolher um motorista ausente hoje mostra um aviso; não bloqueia.
  • Adiantamentos e acertos (CrewAdvance; GET e POST /api/equipe/adiantamentos, PATCH .../[id], só administrador): data, valor, motivo (adiantamento de viagem, vale, outro), viagem opcional e observação. Registrar lança a despesa no Financeiro na mesma transação (categoria "Adiantamento", em aberto, vencendo no dia), como a manutenção faz. O acerto (action: "acertar") guarda o valor gasto comprovado e a tela mostra a diferença: a devolver (gastou menos) ou a receber (gastou mais). action: "reabrir" desfaz um acerto digitado errado. O adiantamento que um fechamento mensal descontou (CrewAdvance.closingId) aparece como "Descontado no fechamento de MM/AAAA", sai do total "em aberto" e não se acerta nem se reabre por aqui (409, com o aviso de reabrir o mês na aba Fechamento).
  • Produtividade (GET /api/equipe/produtividade?de=AAAA-MM&ate=AAAA-MM, mesmo período dos relatórios): por motorista, viagens finalizadas, entregas, entregas no prazo, peso transportado e, só para o administrador, o frete das cargas entregues e a comissão (percentual do frete entregue). O prazo é a mesma conta dos Relatórios (desempenhoPorMotorista). Para a operação os campos de frete e comissão nem vão na resposta. Havendo comissão, uma linha no pé da tabela diz que o valor a pagar é o da aba Fechamento e por que os dois podem diferir.
  • Percentual de comissão (Driver.commissionPct, opcional): editado no cadastro do motorista, só pelo administrador (PATCH /api/motoristas/[id] responde 403 se outro perfil mandar o campo). O percentual só sai em GET /api/motoristas, e só para o administrador; viagens, veículos e cargas devolvem o motorista sem ele.

Remuneração variável e fechamento mensal

Além da comissão, o que a empresa combina pagar ao motorista próprio por viagem, e o fechamento do que ela deve a cada um no mês. As regras e as contas ficam em src/lib/remuneracao.ts (funções puras, testadas sem banco); o que lê o banco fica em src/lib/remuneracao-db.ts. É um demonstrativo gerencial, não folha de pagamento.

  • Formas de remuneração (no cadastro do motorista, parte "Remuneração"; PATCH /api/motoristas/[id]): além do percentual do frete (commissionPct), valores opcionais de diária por dia de viagem (dailyRate), por km rodado (perKmRate), por entrega feita (perDeliveryRate) e fixo por viagem (perTripRate). Convivem: cada forma com valor entra na conta; em branco, a forma não entra (zero é um valor). São colunas anuláveis em Driver, como a comissão já era: uma tabela à parte só acrescentaria uma junção para cinco números que são do cadastro. Seguem a regra da comissão: só altera quem altera os valores da equipe (os demais recebem 403), só saem em GET /api/motoristas para quem lê esses valores, e viagens, veículos e cargas devolvem o motorista sem eles.
  • Ajudante: diária e fixo por viagem (Helper.dailyRate e Helper.perTripRate), alterados no painel do ajudante na aba Fechamento (PATCH /api/equipe/ajudantes/[id]/remuneracao, de equipeValores; numa rota à parte porque o cadastro do ajudante é da operação). Os valores não vão em GET /api/equipe/ajudantes. O ajudante não tem comissão, km nem valor por entrega.
  • Apuração por viagem (apurarViagem): só viagem finalizada. Dias = do dia da saída ao da finalização, os dois contando, no relógio do Brasil (uma viagem que sai e volta no mesmo dia é 1 dia). Km = hodômetro de retorno menos o de saída; sem os dois não há valor por km, e a tela diz por quê. Entregas = cargas entregues na viagem. Frete = o das cargas entregues (carga a cotar não soma). Cada parcela é arredondada em centavos.
  • Motorista de parceiro (Driver.carrierId) em viagem com frete contratado não gera remuneração por aqui: o pagamento é o do contrato com o parceiro, e somar os dois pagaria a viagem duas vezes. A viagem aparece na lista com o motivo e valor zero. Com o contrato cancelado, ou em viagem sem contrato, vale a remuneração do cadastro. O ajudante é da empresa e recebe a dele mesmo em viagem com contrato.
  • Fechamento mensal (aba Fechamento da Equipe; GET /api/equipe/fechamento?mes=AAAA-MM): por motorista e por ajudante (cada um na sua linha), as viagens finalizadas no mês com a apuração de cada uma, o total do variável, os descontos e o saldo (variável − descontos). Não há outro desconto: o sistema não tem falta, multa nem encargo para descontar.
  • O que é desconto (descontoDoAdiantamento): só o que a pessoa deve à empresa e ainda não foi descontado em fechamento nenhum. O vale em aberto entra inteiro. Do adiantamento já acertado entra só a sobra a devolver (recebeu 500, gastou 420 com comprovante: 80); acertado sem sobra (gastou tudo, ou gastou mais e a empresa é que completa) não entra. O adiantamento de viagem e o de motivo "outro" ainda sem acerto ficam de fora: são dinheiro da empresa na mão da pessoa para pagar despesa, e só o acerto diz quanto sobrou; o painel e o recibo avisam ("1 adiantamento sem acerto (R$ 300,00) ficou fora da conta"). O que decide é o motivo e o acerto, não a viagem ligada.
  • Dívida de meses anteriores: o fechamento de um mês olha os adiantamentos com data até o fim dele, então o vale (ou a sobra) de um mês que nunca foi fechado entra no próximo fechamento da pessoa, numa linha à parte ("De meses anteriores") na tela e no recibo. Na mesma linha entra o saldo devedor do último fechamento anterior, quando ele fechou negativo (descontos maiores que o variável): a dívida não some, segue de fechamento em fechamento até ser coberta. Cada adiantamento entra em um fechamento só.
  • Fechar o mês (POST /api/equipe/fechamento, só administrador): grava o fechamento (CrewPayClosing, um por pessoa e mês) com os valores congelados (cada viagem e cada desconto como estavam, os adiantamentos sem acerto, o saldo devedor trazido, as formas do cadastro e quem fechou) e, na mesma transação, lança a despesa no Financeiro (categoria "Remuneração variável", a pessoa como contraparte, em aberto) e dá baixa nos adiantamentos descontados: CrewAdvance.closingId passa a apontar para o fechamento. O status e o acerto do adiantamento não mudam (um vale descontado continua OPEN no banco); o que o tira do "em aberto" é a ligação com o fechamento. Foi a forma escolhida em vez de um status novo porque reabrir precisa devolver cada um ao que era (em aberto, ou acertado com sobra), e assim não há o que lembrar. O lançamento é o saldo a pagar: o adiantamento já virou despesa quando foi entregue, e lançar o variável inteiro contaria esse dinheiro duas vezes. Sem saldo a pagar o mês fecha sem lançamento. Mês sem viagem finalizada e sem desconto novo não fecha (só saldo devedor anterior não basta).
  • Fechado não muda sozinho: se uma viagem do mês é alterada, o cadastro muda ou entra um vale depois, o fechamento continua como foi gravado; a lista marca "mudou" e o painel mostra a conta de hoje ao lado, com a oferta de reabrir. Reabrir (DELETE /api/equipe/fechamento/[id], só administrador) apaga o fechamento e o lançamento dele, se ainda está em aberto, e devolve os adiantamentos descontados ao estado em que estavam; se o lançamento já foi pago no Financeiro, recusa (409) com o aviso de reabri-lo lá antes. O que estava fechado fica na Auditoria (fechamento-equipe.fechar e .reabrir). Reabrir um mês de que um fechamento posterior trouxe saldo devedor não mexe no posterior: ele passa a aparecer como "mudou".
  • Demonstrativo imprimível (/dashboard/equipe/fechamento/[id]/recibo, pelo botão Recibo do fechamento; GET /api/equipe/fechamento/[id]): folha A4 no molde do recibo de frete, com a empresa, a pessoa (nome e CPF), o mês, as viagens com dias, km, entregas, frete e o valor de cada forma, os descontos do mês, os de meses anteriores à parte, o saldo, as assinaturas e a frase "Demonstrativo de remuneração variável. Não é holerite nem recibo de salário: os encargos e a folha são do contador."
  • Custo da viagem: a remuneração do motorista e a do ajudante entram no acerto e no "Resultado por viagem" dos Relatórios como custo ("Remuneração do motorista"), mostradas à parte. Com o mês fechado vale o valor congelado da viagem; em aberto, o cadastro de hoje.
  • Aplicativo do motorista (/driver/remuneracao; GET /api/driver/remuneracao?mes=AAAA-MM): só leitura, o total do mês (este mês ou o anterior) e uma linha por viagem finalizada, que abre o detalhe ao tocar; com mais viagens do que cabem na tela, a lista rola por dentro. Só aparece se a empresa ligar o interruptor "Motorista vê os ganhos das viagens no aplicativo", em Empresa (Tenant.driverPayVisible, GET e PATCH /api/empresa/remuneracao, desligado por padrão). Desligado, a rota responde { "visivel": false } e nenhum valor sai do servidor. O motorista não recebe adiantamentos, saldo, frete das cargas nem as formas do cadastro, e nunca nada de outro motorista.

A comissão da aba Produtividade continua como era (percentual sobre o frete das cargas que viraram "Entregue" no período, pelo motorista da carga). A do fechamento é por viagem finalizada no mês (frete das cargas entregues nela): numa viagem que atravessa a virada do mês, os dois números diferem. A Produtividade é relatório de desempenho e diz isso na própria tela: o valor a pagar é o do Fechamento.

Não existe, e não é o que esta parte faz: folha de pagamento, encargos, INSS, IRRF, FGTS, hora extra, jornada e ponto, banco de horas, DSR e férias. O que sai daqui é um demonstrativo gerencial do variável; a folha e os encargos são do contador. Também não existe: pagar o fechamento em parcelas (o lançamento é um só, e se baixa no Financeiro), desconto que não seja adiantamento, descontar só uma parte de um vale (ele entra inteiro; o que passar do variável vira saldo devedor do fechamento), registrar que a sobra de um acerto foi devolvida em dinheiro (acertado com sobra, ela entra no próximo fechamento), somar ao saldo o complemento que a empresa deve num acerto em que a pessoa gastou mais, fechamento de vários meses de uma vez, e exportação do fechamento em planilha. Fechar um mês anterior a outro já fechado é aceito, mas o saldo devedor dele não entra no fechamento posterior que já existe (este aparece como "mudou").

Permissão: pessoas, ajudantes, ausências e as contagens da produtividade são da equipe interna (ADMIN e OPERATION); adiantamentos, frete, comissão, as formas de remuneração e o fechamento são de quem lê os valores da equipe, e fechar e reabrir o mês é só do administrador (equipeFechamento). Registro de outra empresa responde 404, como um id inventado, e o banco recusa ausência, adiantamento ou fechamento apontando para motorista, ajudante, viagem ou lançamento de outra empresa.

As colunas e a tabela da remuneração variável são criadas por prisma/sql/037-remuneracao-e-ocupacao.sql (com a cubagem útil do veículo); a ligação do adiantamento com o fechamento que o descontou, por prisma/sql/038-adiantamento-no-fechamento.sql, que também liga aos fechamentos já gravados os adiantamentos que eles descontaram. Rode npm run db:rls depois delas.

As tabelas são criadas por prisma/sql/019-equipe-e-baixa.sql (que também traz as colunas da baixa com encargos e do centro de custo); rode npm run db:rls depois dela.

Ainda não existe: folha de pagamento, ponto e jornada, banco de horas, saldo e período aquisitivo de férias, anexo do atestado e comissão de ajudante (o ajudante ligado à viagem tem diária e fixo por viagem no fechamento mensal, mas não entra na produtividade nem tem percentual sobre o frete). O acerto não mexe na despesa lançada no Financeiro: a sobra devolvida ou o complemento pago são lançados lá, à mão, e o adiantamento não é apagado pela tela. A viagem conta como finalizada pela data própria de finalização (Manifest.finishedAt); as finalizadas antes dessa coluna existir seguem contando pela data da última alteração do manifesto. A comissão da Produtividade é só uma conta de desempenho: o que vira lançamento a pagar é o fechamento mensal.

Viagem: dados, rota, despesas e acerto

Em /dashboard/manifestos, o botão Viagem de cada cartão abre a tela da viagem, com uma aba por assunto. As regras e as contas ficam em src/lib/viagem.ts (funções puras: o link da rota, a ordem das paradas e o acerto são calculados na leitura).

  • Dados (PATCH /api/manifestos/[id]/dados): ajudante (do cadastro da Equipe, ativo), hodômetro de saída e de retorno, previsão de saída e de retorno e observação. Todos opcionais; campo em branco apaga. Vale com a viagem em montagem, em rota e finalizada (o hodômetro de retorno chega no fim); só a cancelada não recebe. Os mesmos campos são aceitos na montagem (POST /api/manifestos). Liberar a saída grava departedAt e finalizar grava finishedAt.

  • Rota (PUT /api/manifestos/[id]/ordem): a ordem das entregas, com subir e descer, gravada em Collection.manifestSequence. É a ordem que o painel e o app do motorista mostram. Vale em montagem e em rota; carga sem ordem vai para o fim, pela data de criação, e a carga retirada da viagem perde a ordem. O botão Google Maps (no painel e no app) monta um link do Google Maps (https://www.google.com/maps/dir/?api=1...) com as paradas que faltam, na ordem: o endereço é o da entrega (logradouro, número, bairro, cidade e CEP) quando a carga tem, senão a cidade do destino; a origem é onde o aparelho está, e o link leva até 10 paradas (limite do Google Maps); passando disso entram as primeiras. Não usa API paga nem chave.

  • Sugerir ordem (POST /api/manifestos/[id]/roteiro, corpo opcional { "voltar": true }): roteirização pelo endereço localizado de cada carga e, onde não há, pela cidade (o endereço, o mapa e o GPS estão em Mapa, endereço e GPS). O sistema acha a cidade de cada entrega pelo texto do destino ("Mirassol - SP", "Mirassol/SP", "MIRASSOL", ou no fim de um endereço), pela mesma chave das tabelas de frete (chaveDaCidade), e propõe a ordem partindo da origem mais comum das cargas: vizinho mais próximo seguido de melhoria 2-opt, com a volta à origem contada (dá para desligar). Entregas na mesma cidade, sem endereço localizado, ficam juntas; carga sem endereço localizado e cuja cidade não foi reconhecida fica sem localização, vai para o fim na ordem atual e é avisada na tela; carga já entregue não muda de lugar, e com a viagem na rua a conta parte da cidade da última entrega feita. Nome que existe em mais de um estado, sem UF escrita, é procurado na UF da origem; se continuar ambíguo, fica sem localização. A rota só calcula: a tela mostra a ordem proposta e a distância de hoje e a sugerida, e o Aplicar grava pela rota de ordem de sempre (PUT .../ordem, com auditoria). A sugestão nunca é pior que a ordem atual: sem ganho, a resposta vem com mudou: false. As contas são funções puras em src/lib/roteiro.ts. A distância é em linha reta (haversine), entre os endereços localizados ou os centros das cidades: serve para a ordem não ir e voltar, não é o km de estrada, e a tela diz isso ao lado do número. Com ROTA_URL (servidor OSRM próprio) ela passa a ser por estrada.

  • Tabela dos municípios: src/data/municipios.json traz os 5.571 municípios do Brasil que constam na fonte (com Brasília, Fernando de Noronha e Boa Esperança do Norte/MT, o mais novo), com nome, UF, latitude e longitude do centro, agrupados por UF. Fonte: kelvins/municipios-brasileiros (csv/municipios.csv, commit 503e2f7, de 10/12/2025), de Kelvin S. do Prado, licença MIT, derivado de dados públicos do IBGE. Só o servidor carrega o arquivo (src/lib/municipios.ts); ele não vai para o pacote do navegador.

  • Despesas (TripExpense; GET e POST /api/manifestos/[id]/despesas, PATCH e DELETE .../[despesaId]): tipo (pedágio, combustível, alimentação, hospedagem, estacionamento, manutenção, outro), valor, data, observação e quem lançou. Nasce pendente. O motorista lança pelo app na viagem dele em rota (/driver/viagem/[id]/despesas; GET e POST /api/driver/manifestos/[id]/despesas) e vê só as que ele lançou e o total delas. Combustível com litros e hodômetro gera também o abastecimento da frota (Fueling), ligado à despesa; sem um dos dois fica só a despesa. Só o administrador aprova ou recusa: aprovar cria, na mesma transação, o lançamento no Financeiro (a pagar, ou já pago), com a categoria do tipo e a viagem como centro de custo; recusar desfaz o abastecimento gerado. Só a pendente pode ser excluída.

  • Acerto (GET /api/manifestos/[id]/acerto, só administrador, só viagem finalizada): frete das cargas, despesas aprovadas, combustível, custo total, km rodados, custo por km, resultado (frete − despesas − combustível) e margem, mais os adiantamentos ligados à viagem e o saldo deles contra as despesas aprovadas (a devolver ou a receber). O combustível são os abastecimentos da frota feitos no veículo entre o dia da saída e o da finalização, menos os que nasceram de uma despesa da viagem, que já estão nas despesas. Despesa pendente fica fora do custo e aparece em aviso. Em viagem feita com veículo de parceiro, o frete contratado entra como mais uma linha do custo (ver Parceiros e frete contratado).

  • Remuneração do motorista no acerto: a remuneração variável que a viagem rende ao motorista e ao ajudante entra no custo total e no resultado (linha "Remuneração do motorista"), e uma frase logo abaixo separa quanto é de cada um. Conta uma vez só: o lançamento que o fechamento do mês faz no Financeiro não é despesa de viagem, e a viagem de motorista de parceiro com frete contratado pesa zero aqui (o custo dela é o do contrato; a frase diz isso). Sem os dois hodômetros o valor por km fica de fora, e a frase também diz. Com o mês fechado vale o valor congelado no fechamento. O mesmo custo entra no "Resultado por viagem" dos Relatórios.

  • Ocupação do veículo (src/lib/ocupacao.ts; a capacidade está em Frota): o cartão da viagem e a aba Dados mostram uma linha "Peso 1.250 de 3.000 kg (42%) · Cubagem 8,2 de 20 m³ (41%)", em âmbar quando passa de 100% e "capacidade não cadastrada" quando o veículo não tem as medidas. No celular é uma linha só.

  • Conferência ao montar e alterar: o formulário de montagem mostra a capacidade ao lado de cada placa ("TST1A11 (TRUCK) · 3.000 kg · 20 m³"), marca "não comporta" nos veículos em que as cargas escolhidas não cabem (sem esconder nenhum) e mostra a ocupação do veículo escolhido. Passar da capacidade é aviso, não trava: POST /api/manifestos e PATCH /api/manifestos/[id] gravam a viagem e devolvem ocupacao e aviso com os números.

  • Saída com excesso (POST /api/manifestos/[id]/liberar): com a carga acima de 100% do peso ou da cubagem, a liberação responde 409 com confirmar: "sairComExcesso" e os números, e o cartão pede a confirmação ("Sair com excesso de peso"). Só com { "sairComExcesso": true } no corpo a viagem sai, e a confirmação fica na Auditoria (quem liberou, o peso, a cubagem e as capacidades de quando saiu). É confirmação e não proibição porque excesso de peso é infração e risco, mas o cadastro do veículo pode estar desatualizado. A conferência usa a capacidade de agora: corrigido o cadastro, a saída deixa de pedir. Veículo sem capacidade cadastrada não pede nada.

Permissão: dados, ordem, sugestão de ordem e lançamento de despesa são da equipe interna (ADMIN e OPERATION); aprovar despesa e o acerto são do administrador; o motorista só alcança a viagem dele que está em rota. Viagem de outra empresa responde 404, e o banco recusa despesa apontando para viagem, usuário, abastecimento ou lançamento de outra empresa. Tudo fica na Auditoria (dados e ordem da viagem; despesa lançada, aprovada, recusada e excluída).

As colunas e a tabela são criadas por prisma/sql/020-viagem.sql; rode npm run db:rls depois dela.

Ainda não existe (o que falta em endereço, mapa e GPS está em Mapa, endereço e GPS): trânsito, janela de entrega e capacidade do veículo na ordem sugerida, apelido de cidade ("Rio Preto" não é reconhecido como São José do Rio Preto: precisa do nome oficial), pedágio automático (cálculo por rota ou integração com tag), etapa "Em retorno" (o fluxo continua Em montagem → Em rota → Finalizada), foto do comprovante da despesa, lançamento de despesa pelo app sem sinal (não entra na fila offline), edição de despesa já lançada (exclui e lança de novo) e despesa prevista na montagem. Duas viagens do mesmo veículo no mesmo dia dividem o abastecimento da frota desse dia: ele aparece no acerto das duas. Excluir no Financeiro o lançamento de uma despesa aprovada não a tira do custo da viagem.

Parceiros e frete contratado

O parceiro de transporte é o agregado, o autônomo ou a transportadora dona de veículo que roda para a empresa. O cadastro fica em /dashboard/parceiros (menu Frota), e a viagem feita no veículo dele ganha um contrato de frete, com os pagamentos e o saldo, na aba Frete da tela da viagem. As regras e as contas ficam em src/lib/parceiros.ts e src/lib/frete-contratado.ts (funções puras: valor sugerido, líquido, situação, saldo e fechamento são calculados, não digitados).

Cadastro (Carrier; GET e POST /api/parceiros, PATCH /api/parceiros/[id]): tipo (TAC agregado, TAC independente ou transportadora, que viram o tpProp 0, 1 e 2 do MDF-e), nome ou razão social, CPF ou CNPJ (com os dígitos verificadores conferidos, único por empresa e que não se altera), RNTRC (8 dígitos), IE, UF, telefone, e-mail, chave Pix, dados bancários (texto), observação e ativo. A lista traz os veículos e os motoristas ligados a cada parceiro. No celular o cadastro abre em três partes (Cadastro, Pagamento e Vínculos).

  • Veículo e motorista do parceiro: Vehicle.carrierId e Driver.carrierId, opcionais, escolhidos no cadastro do veículo (campo "Dono do veículo") e no do motorista (campo "Parceiro"). Parceiro que não existe nesta empresa é recusado (400), e o banco recusa a ligação com parceiro de outra empresa.
  • MDF-e: quando o veículo tem parceiro, o proprietário do MDF-e (grupo prop: CPF ou CNPJ, nome, RNTRC, IE, UF e tipo) sai do cadastro do parceiro, e a pendência de dado faltando aponta para Parceiros. Os campos soltos de proprietário do veículo (owner*) continuam valendo para o veículo sem parceiro e não são apagados; no cadastro do veículo, escolher o parceiro esconde esses campos.
  • Contratação padrão (PUT /api/parceiros/[id]/contratacao): a forma de pagar o parceiro e o valor dela, opcionais: fixo por viagem, percentual do frete das cargas, por km ou por entrega. É dinheiro: só quem lê o frete contratado recebe esses campos, e só quem o altera grava.

Contrato de frete da viagem (FreightContract, um por viagem; GET, PUT e DELETE /api/manifestos/[id]/frete):

  • Nasce sozinho quando a viagem é montada (ou tem o veículo trocado, ainda em montagem) com veículo de parceiro ativo, com a forma e a base do padrão dele e o valor bruto que a forma dá naquele momento. Também pode ser criado à mão na aba Frete, em viagem de veículo próprio. Trocar o veículo de uma viagem que já tem contrato não mexe no contrato.
  • Valor bruto: a tela mostra o sugerido pela forma (fixo; percentual do frete das cargas da viagem; km rodados pelo hodômetro × valor, quando a viagem tem os dois hodômetros; quantidade de cargas × valor) e avisa quando ele difere do gravado. O que vale é o valor gravado por quem paga: quando o sugerido muda (no contrato por km, quando o hodômetro de retorno é informado), o resumo da aba Frete mostra os dois valores e o botão Usar o valor sugerido, que grava o contrato como está, só com o bruto novo. Nada muda sozinho.
  • Acréscimos e descontos: pedágio ou vale-pedágio pago à parte, outros acréscimos e descontos.
  • Retenções (INSS, IRRF, SEST/SENAT e outras): são valores digitados por quem paga. O sistema não calcula retenção nenhuma: o cálculo é do contador, e a tela e o recibo dizem isso.
  • Líquido = bruto + acréscimos − descontos − retenções. Custo da viagem = bruto + acréscimos − descontos (a retenção não sai do custo).
  • Situação: sai dos pagamentos. Em aberto (sem pagamento), adiantado (pago menos que o líquido) ou quitado (líquido pago). Cancelado é o único estado escolhido (DELETE), só sem pagamento; o cancelado fica como histórico, sai do custo da viagem e é reaberto gravando de novo.
  • Com pagamento registrado, o parceiro do contrato não troca, o líquido não pode ficar menor que o pago, e a alteração não pode quitar o contrato de viagem ainda não finalizada. Viagem cancelada não recebe contrato.

Pagamentos (FreightPayment; POST /api/manifestos/[id]/frete/pagamentos, DELETE .../[registroId]): adiantamento ou saldo, com data, valor, forma (Pix, transferência, dinheiro, cartão-frete ou outro) e observação.

  • Cada pagamento lança a despesa no Financeiro na mesma transação (categoria "Frete contratado", o parceiro como fornecedor e a viagem como centro de custo), como o adiantamento da equipe e a manutenção fazem. O lançamento nasce em aberto, vencendo no dia do pagamento; marcando "Já paguei", nasce pago, nesse dia e com a forma informada.
  • Excluir o pagamento desfaz o lançamento, na mesma transação, se ele ainda está em aberto. Se já foi pago no Financeiro, a exclusão é recusada (409) com o aviso de reabrir o lançamento lá antes.
  • Pagar mais que o líquido e quitar antes de a viagem ser finalizada exigem confirmação explícita: a rota responde 409 com confirmar (pagarAcima ou pagarAntesDeFinalizar), a tela pergunta e reenvia com o campo marcado.
  • Cancelar a viagem cancela junto o contrato sem pagamento. Com pagamento registrado, a viagem não é cancelada: a recusa (409) manda excluir os pagamentos antes.

Subcontratação contratada (o parceiro do contrato é uma transportadora; regras em src/lib/subcontratacao.ts). Quando esta empresa contrata outra transportadora para fazer o transporte, o sistema guarda as chaves dos documentos que ela emitiu. Não emite nem transmite nada em nome dela, e nada daqui vai à SEFAZ.

  • Documentos da subcontratada (SubcontractDocument; GET e POST /api/manifestos/[id]/subcontratacao, DELETE .../[documentoId]): a chave (44 posições, dígito verificador, modelo 57 para o CT-e e 58 para o MDF-e, e o CNPJ do parceiro nas posições 7 a 20) e a data de emissão (no mês da chave, nunca futura). O CT-e é registrado por carga (uma carga pode ter mais de um, e o mesmo CT-e pode cobrir mais de uma carga); o MDF-e, por UF de descarregamento. Só o contrato valendo com transportadora recebe documento; com documento registrado, o parceiro do contrato não troca. Registro e exclusão ficam na Auditoria (subcontratacao.registrar e subcontratacao.excluir, com a chave e quem fez).
  • Onde aparece: na aba MDF-e da viagem (uma linha "Subcontratada", que abre os documentos; só em viagem com veículo de parceiro), na aba Frete (quantos documentos dela há, as cargas sem CT-e dela e o botão "Documentos") e no recibo (seção "Documentos da subcontratada").
  • Permissão: são documentos fiscais, sem valor nenhum do contrato. Lê quem lê o fiscal (fiscalVer); registra e exclui quem escreve no fiscal (fiscal: administrador, diretoria e operação). É a operação que libera a saída, e ela não lê o frete contratado.
  • CT-e: o CT-e desta empresa continua sendo o de sempre (serviço normal) e continua exigido de cada carga na saída. A falta do CT-e da subcontratada é aviso, nunca bloqueio: aparece na aba Frete, no formulário de pagamento quando o valor quita o contrato e na resposta do pagamento que quitou (aviso).
  • MDF-e e liberação da saída: na subcontratação o MDF-e é de um só dos dois, o que gerencia o transporte. Em viagem feita no veículo da transportadora contratada (veículo de parceiro do tipo transportadora, com o contrato de frete dela valendo), cada UF de descarregamento fica coberta pelo MDF-e desta empresa ou pela chave do MDF-e dela registrada no contrato. Sem registro, vale a regra de sempre (o MDF-e é desta empresa), e a frase do bloqueio diz que a chave do MDF-e dela também serve. Havendo MDF-e dos dois para a mesma UF, a tela avisa. Contrato cancelado, de outro parceiro, ou em veículo próprio: a chave registrada não cobre nada.
  • TAC agregado e TAC independente não mudam: o MDF-e continua sendo desta empresa, com o parceiro como proprietário do veículo, e não se registra documento de terceiro.
Norma O que diz No código Teste
Ajuste SINIEF 21/10, cláusula terceira, § 6º (acrescentado pelo Ajuste 06/14) "Nos casos de subcontratação, o MDF-e deverá ser emitido exclusivamente pelo transportador responsável pelo gerenciamento deste serviço, assim entendido aquele que detenha as informações do veículo, da carga e sua documentação, do motorista e da logística do transporte." mdfeDaSubcontratada e lerSaida (src/lib/mdfe-db.ts), viagemSubcontratada e avisoDeMdfeEmDobro (src/lib/subcontratacao.ts), faltasParaSair (src/lib/mdfe.ts) tests/mdfe-rotas.test.ts, "viagem no veículo de parceiro"; tests/subcontratacao.test.ts, "liberação da saída" e "quando a viagem é subcontratada"
Ajuste SINIEF 21/10, cláusula terceira, § 2º Um MDF-e para cada UF de descarregamento O MDF-e dela é registrado com a UF (unloadState) e cobre só essa UF tests/mdfe-rotas.test.ts: "falta UF, continua bloqueada"
Convênio SINIEF 06/89, art. 17, § 7º (redação do Ajuste 03/02) "A empresa subcontratada deverá emitir o Conhecimento de Transporte [...] podendo, a critério do fisco, a prestação do serviço ser acobertada somente pelo conhecimento de que trata o § 3º" CT-e dela por carga; a falta é aviso (cargasSemCte, avisoDeCargasSemCte), não bloqueio, porque a UF pode dispensar tests/subcontratacao.test.ts: "quitar com carga sem CT-e dela"
MOC do CT-e 4.00, Visão Geral (evento 440150) e Ajuste SINIEF 09/07, cláusula décima oitava-A, § 1º, XX (redação do Ajuste 10/16) "Autorizado Subcontratação, registro de que um CT-e de subcontratação foi referenciado em um CT-e com tipo de serviço normal" O CT-e desta empresa segue normal e exigido na saída (faltasParaSair) tests/mdfe-rotas.test.ts: "o MEU CT-e de cada carga continua exigido"
MOC do MDF-e 3.00b, Anexo I (regras F30 a F36) Nenhuma regra exige que o emitente do MDF-e seja o do CT-e relacionado; o manual não trata de subcontratação Nada: o MDF-e dela é dela, e este sistema só guarda a chave —
MOC (chave de acesso) e NT Conjunta 2025.001 44 posições, dígito verificador, modelo nas posições 21 e 22, CNPJ nas 7 a 20 problemaDaChave (src/lib/subcontratacao.ts), sobre src/lib/chave-dfe.ts tests/subcontratacao.test.ts: "chave do documento da subcontratada"

Por que a saída não passa a dispensar o meu MDF-e sozinha. A norma não diz "o MDF-e é da subcontratada": diz que é de quem gerencia o serviço, e isso é um fato de cada operação (pode ser esta empresa, mesmo com o veículo de outra). O sistema não decide: quem registra a chave do MDF-e dela declara, com nome na Auditoria, que é ela quem emite. Fontes: os manuais foram lidos nos arquivos dos portais; os Ajustes SINIEF 21/10, 03/02, 10/16 e 15/89 foram lidos em republicação (LegisWeb), em 12/10/2026, porque o site do CONFAZ não respondeu. Não foram lidos: o texto consolidado do Convênio SINIEF 06/89 (o § 3º do art. 17 e a redação vigente do § 7º; uma consulta estadual cita o § 7º com "exceto quanto ao transporte multimodal", o que indica redação mais nova), os incisos da cláusula terceira do Ajuste SINIEF 09/07 e a regra de cada UF. É com o contador: se a UF dispensa o CT-e da subcontratada, quem recolhe o ICMS, e quem é o "responsável pelo gerenciamento" em cada operação.

Acerto e resultado da viagem: o custo do contrato entra no acerto (GET /api/manifestos/[id]/acerto, linha "Frete contratado") e no "Resultado por viagem" dos Relatórios, uma vez só. As despesas que os pagamentos lançam no Financeiro não são despesa de viagem e não entram de novo na conta. (No DRE dos Relatórios, que é por caixa, o que foi pago aparece na categoria "Frete contratado".)

Recibo (GET /api/manifestos/[id]/frete/recibo; página /dashboard/manifestos/[id]/recibo-frete, pelo botão "Recibo" da aba Frete): folha A4 no molde da minuta, com a empresa, o parceiro (nome, CPF ou CNPJ, RNTRC), a viagem, o veículo, o motorista, as origens e os destinos, o bruto, os acréscimos, os descontos, as retenções, o líquido, os pagamentos feitos, o saldo e as linhas de assinatura. O rodapé diz: "Recibo de pagamento de frete. Não é documento fiscal e não substitui o RPA, o CIOT nem o comprovante de pagamento eletrônico de frete." O recibo não tem número de RPA.

Fechamento por parceiro (GET /api/parceiros/[id]/fechamento?de=AAAA-MM&ate=AAAA-MM, mesmo período dos relatórios; botão "Fechamento" da lista): as viagens do parceiro no período, com bruto, retenções, líquido, pago e saldo de cada contrato, e o total. A viagem entra pelo dia em que saiu, no relógio do Brasil (a que ainda não saiu, pelo dia em que foi montada); contrato cancelado não entra. ?formato=csv devolve o mesmo quadro em CSV, no formato da exportação do portal (separador ;, BOM); a tela também imprime pelo navegador. A lista de parceiros mostra o total em aberto a pagar de cada um.

Permissão (capacidades novas em src/lib/permissoes.ts): o cadastro é de parceirosVer (administrador, diretoria, operação e financeiro) e parceiros (os três primeiros); o que é dinheiro (contrato, pagamentos, recibo, fechamento, total em aberto e contratação padrão) é de freteContratadoVer (administrador, diretoria e financeiro) e freteContratado (administrador e financeiro), os mesmos perfis que leem e lançam no Financeiro. Para a operação, a contratação padrão e o total em aberto nem vão na resposta, e a aba Frete não aparece. Registro de outra empresa responde 404, como um id inventado, e o banco recusa veículo, motorista, contrato ou pagamento apontando para registro de outra empresa. Tudo fica na Auditoria (parceiro.* e frete-contratado.*).

Sugestão para o pagamento do frete no MDF-e (GET /api/manifestos/[id]/frete/sugestao-mdfe, de freteContratadoVer): na conferência do MDF-e da viagem, o grupo "Pagamento do frete" mostra o que o contrato sugere: o valor do contrato (bruto + acréscimos − descontos, o vContrato), a soma dos adiantamentos já registrados (vAdiant), à vista ou a prazo, a forma do último pagamento, a chave Pix do parceiro e quem paga (o emitente fiscal). A sugestão aparece como exemplo apagado nos campos em branco e numa linha com o botão Usar a sugestão; só depois de aceita ela entra no formulário. A validação e a montagem do MDF-e não leem nada dela. Quem não lê o frete contratado (a operação, a expedição) nem chama a rota, que recusa com 403: os campos ficam em branco, como sempre.

As tabelas e as colunas são criadas por prisma/sql/034-parceiros.sql e prisma/sql/036-subcontratacao.sql (só acrescentam); rode npm run db:rls depois delas.

Ainda não existe: cálculo automático de INSS, IRRF ou SEST/SENAT, emissão de RPA, geração de CIOT e integração com administradora de pagamento eletrônico de frete (PEF), vale-pedágio obrigatório, tabela de piso mínimo de frete da ANTT, importação do XML e conferência na SEFAZ dos documentos da subcontratada (só a chave digitada é guardada), portal do agregado e pagamento em lote por arquivo bancário. A sugestão do contrato para o pagamento do frete no MDF-e não traz o vencimento do saldo nem os componentes do pagamento, e é a mesma em todos os MDF-e da viagem (o contrato é da viagem, não da UF). O contrato não se atualiza sozinho quando as cargas, os hodômetros ou o veículo da viagem mudam (a tela avisa a diferença entre o sugerido e o gravado e oferece o botão de usar o sugerido); o bruto do contrato criado sozinho por km nasce zerado, porque a viagem ainda não tem o hodômetro de retorno. Excluir no Financeiro o lançamento de um pagamento deixa o pagamento "sem lançamento" (a tela mostra), e ele continua contando como pago. Não há tela para ligar vários veículos ao parceiro de uma vez (liga-se no cadastro de cada veículo), nem valor financeiro com botão de esconder (as telas financeiras do sistema não têm esse padrão).

Mapa, endereço e GPS

Roteirização por endereço, mapa da viagem e posição do motorista, só com dados e serviços abertos do OpenStreetMap: nenhum serviço pago, nenhuma chave. O que cada parte usa, e os limites de cada uma:

  • Endereço da entrega. A carga ganhou quatro campos opcionais além da cidade: logradouro, número, bairro e CEP (src/lib/endereco.ts). Entram no pedido do painel (seção "Endereço da entrega", recolhida) e do portal (o destinatário frequente preenche os campos a partir do endereço dele, para conferir), e na carga criada de uma NF-e (lidos do endereço do destinatário da nota). São editáveis enquanto a carga é editável (antes de embarcar) e entram na Auditoria. Mudar o endereço ou a cidade apaga a coordenada, que é procurada de novo.
  • Localizar o endereço (geocodificação): Nominatim. Em segundo plano, o despachante do servidor (src/lib/eventos.ts) procura a coordenada das cargas com logradouro que ainda não foram procuradas, as que já estão em viagem primeiro (src/lib/geo-db.ts, src/lib/geo.ts). Nunca dentro da requisição de alguém. Cumpre a política de uso do Nominatim: no máximo uma consulta por segundo no servidor inteiro (fila única em memória; na prática são até 2 consultas a cada 15 segundos), User-Agent com o nome do sistema e o contato de GEO_CONTATO, e cache no banco (GeoCache) pela chave do endereço normalizado, inclusive do "não achei": o mesmo endereço nunca é perguntado duas vezes. Sem GEO_CONTATO o serviço não é consultado: as cargas ficam esperando e a rota segue pela cidade. A consulta leva logradouro, número, cidade e UF (bairro e CEP ficam de fora: no OpenStreetMap do Brasil eles faltam ou divergem, e a consulta com eles volta vazia). Resultado a mais de 100 km do centro da cidade do destino é descartado. Falha do serviço não é guardada e deixa a localização em pausa por 10 minutos; a localização corre à parte no despachante, com trava e tratamento de erro próprios, e não segura nem derruba eventos, push, títulos vencidos nem a conferência de cobranças.
  • O GeoCache não tem tenantId. É dado público (a coordenada de uma rua) e o cache é do sistema: guarda só o texto normalizado do endereço e a coordenada, nada que diga de que empresa, cliente ou carga veio a pergunta. Só o cliente sistema lê e grava; o papel da aplicação não tem permissão nenhuma na tabela (prisma/sql/010-rls.sql).
  • Ordem sugerida por endereço. POST /api/manifestos/[id]/roteiro usa a coordenada da carga quando houver e, senão, o centro da cidade (src/lib/roteiro.ts): com endereço localizado, as entregas dentro da mesma cidade também entram na ordem. A resposta diz quantas entraram pelo endereço (porEndereco) e como a distância foi medida (medida).
  • Distância: linha reta por padrão; por estrada só com servidor próprio. Sem ROTA_URL, a distância é em linha reta (haversine) e a tela diz isso. Com ROTA_URL apontando para um servidor OSRM (por exemplo uma instância própria, http://osrm:5000), a conta usa a matriz de distâncias por estrada dele (/table/v1/driving/...?annotations=distance, src/lib/rota-osrm.ts), com tempo limite de 4 segundos e até 100 pontos; qualquer falha volta para a linha reta sem derrubar a sugestão. O servidor de demonstração público do OSRM (router.project-osrm.org) não é padrão e é recusado mesmo se for posto na variável: a política dele proíbe uso em produção.
  • Mapa. Na aba Rota da viagem (botão Mapa, que troca a lista pelo mapa) e na viagem do app do motorista (botão Ver mapa): as paradas numeradas na ordem, a linha entre elas e a posição do motorista. Marcador cheio é endereço localizado; tracejado é o centro da cidade; verde é entrega feita. Desenhado com o Leaflet (única dependência nova, carregada só no navegador) sobre os blocos de imagem do OpenStreetMap, com o crédito © OpenStreetMap visível no canto (src/components/mapa/mapa-da-viagem.tsx). GET /api/manifestos/[id]/mapa (quem lê viagens) e GET /api/driver/manifestos/[id]/mapa (o motorista, só a viagem dele em rota) devolvem os pontos, que já estão no banco: abrir o mapa não consulta serviço de localização. A política dos blocos do OSM pede uso moderado e proíbe baixar blocos em massa: o service worker do app do motorista não guarda os blocos, e NEXT_PUBLIC_MAPA_TILES troca o servidor por um próprio ou contratado quando o volume crescer. A linha do mapa liga as paradas em linha reta: não é o caminho da estrada.
  • GPS do motorista. No app, com a viagem em rota, o botão Compartilhar localização liga o envio. A permissão do navegador só é pedida depois do toque; enquanto ligado, a posição vai a cada 30 segundos, e também ao registrar uma entrega, para POST /api/driver/manifestos/[id]/posicao (só a viagem dele em rota; latitude, longitude e precisão conferidas; no máximo 12 envios por minuto por motorista, depois 429). Só funciona com o app aberto na tela: navegador não rastreia em segundo plano, e a tela diz isso. A última posição fica na viagem e um histórico enxuto em TripPosition (um ponto novo só entra se o caminhão andou 25 m ou se passaram 5 minutos; no máximo 500 pontos por viagem, os mais antigos saem). O painel mostra a posição e "há quanto tempo" no mapa da aba Rota (relido a cada 30 segundos) e, na lista de viagens, o selo localização há X min. Viagem que não está em rota não mostra posição.
  • Privacidade. A posição do motorista é só do painel (quem lê viagens). O rastreio público e o portal do cliente não recebem posição nem coordenada: só o status, como sempre. Posição não entra na Auditoria nem nos eventos enviados a sistemas de fora.
  • Trânsito não existe em fonte aberta. Nem a ordem sugerida nem o mapa conhecem trânsito, e as telas dizem isso. O link Google Maps (no painel e no app) continua: é lá que o motorista vê o trânsito. Com endereço na carga, o link leva o endereço inteiro; sem ele, a cidade.

Variáveis: GEO_CONTATO (liga a localização por endereço), GEO_URL (padrão https://nominatim.openstreetmap.org), ROTA_URL (opcional, servidor OSRM próprio) e NEXT_PUBLIC_MAPA_TILES (opcional; é lida na hora do build, como toda variável NEXT_PUBLIC_). O projeto não define Content-Security-Policy (nem em next.config.ts nem em src/proxy.ts), então não houve o que liberar para os blocos; se uma política for criada, ela precisa de img-src para o servidor dos blocos.

Capacidades: ver o mapa e a posição é de quem lê viagens (manifestosVer); sugerir e alterar a ordem, de quem altera viagens (manifestos). Viagem de outra empresa responde 404, e o banco recusa posição apontando para viagem de outra empresa. As colunas e as tabelas são criadas por prisma/sql/026-mapa.sql; rode npm run db:rls depois dela.

Ainda não existe: tempo de viagem e previsão de chegada; caminho pela estrada desenhado no mapa (a linha é reta); rastreamento com o app fechado ou a tela bloqueada (exigiria aplicativo nativo); trilha do percurso no mapa (o histórico é guardado, mas a tela mostra só a última posição); aviso quando o motorista para de compartilhar; correção manual do ponto no mapa (endereço que o OpenStreetMap não conhece fica no centro da cidade); localização do endereço de coleta (a origem segue pela cidade); apelido de cidade ("Rio Preto" não é reconhecido como São José do Rio Preto); janela de entrega e capacidade do veículo na ordem sugerida; pedágio. Um endereço que o serviço não achou só é procurado de novo se for alterado. Com mais de uma cópia do servidor, o limite de uma consulta por segundo vale por cópia: rode o despachante em uma só (TMS_EVENTOS=off nas outras).

Comprovante de entrega

O comprovante segue o que as grandes transportadoras fazem: no e-commerce de última milha a prova é a foto da carga no local; na carga B2B, o canhoto da nota assinado. As regras ficam em src/lib/comprovantes.ts (o que a tela também usa) e src/lib/entregas.ts (corpo da baixa e conferência); o que grava, em src/lib/comprovantes-db.ts; a redução da foto no aparelho, em src/lib/foto.ts.

Perfil da empresa. O administrador escolhe em Empresa (aba Identidade, cartão "Comprovante de entrega"; GET e PATCH /api/empresa/comprovantes, coluna Tenant.podProfile) o que a baixa exige:

Perfil A baixa exige Tentativa sem sucesso exige
LIVRE (padrão) nada além de quem recebeu: foto e assinatura opcionais nada: foto da fachada opcional
ECOMMERCE pelo menos uma foto da entrega (a carga no local) foto da fachada
B2B pelo menos uma foto do canhoto assinado foto da fachada

O servidor impõe a regra (400 dizendo o que falta, por exemplo "Falta a foto do canhoto assinado."); a tela do motorista faz a mesma conta para marcar o obrigatório. A assinatura na tela é opcional em todos os perfis.

Tela do motorista (/driver/entregas/[id]/baixa), em blocos curtos, nesta ordem: quem recebeu (botões: destinatário, funcionário, portaria, familiar, vizinho, outro) → nome e documento → fotos (cartões "Foto da entrega" e "Foto do canhoto", que abrem a câmera traseira; miniatura com "Trocar" e "Remover") → "Entrega com ressalva" (recolhida) → "Colher assinatura na tela" (recolhida) → "Finalizar entrega". O botão final diz o que falta em vez de ficar desligado. O perfil da empresa é lido de GET /api/driver/comprovantes e lembrado no aparelho, para a tela marcar o obrigatório mesmo sem sinal.

  • Fotos por tipo (tabela ProofPhoto): ENTREGA, CANHOTO, AVARIA e FACHADA. Até 6 por comprovante: no máximo 2 de entrega, 2 de canhoto, 2 de fachada e 3 de avaria. Só imagem embutida JPEG, PNG ou WebP. O servidor grava o SHA-256 dos bytes de cada imagem (não do texto base64).
  • Foto pequena antes de sair do aparelho: a foto é reduzida no canvas para 1600 px no maior lado, em JPEG com qualidade 0,7. Foto que o navegador não consegue abrir (HEIC sem suporte) recebe a mensagem do formato. O teto por foto nova é de 1,5 milhão de caracteres. O formato antigo da baixa (photoBase64, sem photos) continua aceito com o teto antigo, sem a relação de quem recebeu: é o que está na fila offline de quem colheu a baixa antes da atualização do aplicativo; a foto dele entra como foto da entrega.
  • Fila offline: continua valendo com várias fotos. O tamanho total da baixa é conferido antes de guardar, e se o aparelho não conseguir guardar (sem espaço, armazenamento bloqueado) o motorista é avisado de que a baixa não foi salva e a tela fica como está. Repetir a mesma baixa continua respondendo 200 sem duplicar comprovante nem foto.
  • Ressalva: tipo (AVARIA, FALTA, VIOLADA, OUTRA) e descrição de 5 a 500 caracteres; avaria e embalagem violada exigem pelo menos uma foto de avaria. A entrega é concluída do mesmo jeito (a carga vira entregue); o comprovante fica marcado, e a equipe com ocorrencias é avisada pelo sininho.
  • Distância do endereço: quando a baixa traz posição e a carga tem a coordenada do endereço (Mapa, endereço e GPS), a distância em linha reta fica gravada (distanceMeters). Não bloqueia a entrega. No painel, acima de 500 m aparece "a X m (ou km) do endereço". Não vai para o portal nem para o rastreio público.
  • Comprovante antigo (uma foto em photoBase64, sem relação, sem tipo) continua aparecendo no painel e no portal, com a foto dele como foto da entrega. Baixas novas gravam só em ProofPhoto.

Conferência no painel. A fila (/dashboard/comprovantes, GET /api/comprovantes) tem os filtros Aguardando conferência, Com ressalva (?ressalva=1, em qualquer situação, a mais nova primeiro), Devolvidos ao motorista (?status=REJECTED) e Aprovado. A página do comprovante (/dashboard/entregas/[id]/comprovante) mostra as fotos agrupadas por tipo (o toque amplia), o SHA-256 de cada uma (o toque copia), quem recebeu com a relação, a ressalva, o aviso de distância e o histórico de devoluções com as fotos substituídas.

Devolução ao motorista. Aprovar é final; a recusa virou devolução (POST /api/comprovantes/[id]/conferir com decision: "REJECTED" e o motivo): fica registrada em ProofRejection (quem, quando, motivo), o motorista da viagem é avisado pelo sininho (o app do motorista tem sininho) e vê o comprovante em "Comprovantes para refazer" na tela inicial do app, com o motivo. Ele manda fotos novas e, se quiser, corrige nome e documento (/driver/entregas/[id]/refazer, POST /api/driver/entregas/[id]/refazer): o comprovante volta para "Aguardando conferência", as fotos devolvidas ficam guardadas como substituídas, a carga continua entregue. Só o motorista da viagem daquela carga refaz (viagem em rota ou finalizada). Repetir o mesmo envio responde 200 sem gravar de novo (o rejectionId diz a qual devolução ele responde). As fotos novas substituem todas as anteriores, então precisam atender sozinhas ao perfil e à ressalva. O SQL 028 cria a devolução em aberto dos comprovantes que já estavam recusados: eles passam a aparecer na lista do motorista.

Tentativa de entrega sem sucesso. É a ocorrência do motorista estendida, na mesma rota (POST /api/driver/entregas/[id]/ocorrencia): o corpo com reason registra a tentativa (/driver/entregas/[id]/insucesso, botão "Não entreguei" na parada). Motivo padronizado (AUSENTE, ENDERECO_NAO_LOCALIZADO, RECUSADO, FECHADO, MUDOU_SE, AREA_DE_RISCO, ou OUTRO com descrição), foto da fachada (obrigatória nos perfis ECOMMERCE e B2B) e a posição, quando houver. Cada tentativa fica em DeliveryAttempt (uma carga pode ter várias) e abre um chamado de reentrega para a equipe com ocorrencias, como toda ocorrência do motorista. A carga não muda de status: continua em rota, na mesma viagem; o que muda é o contador de tentativas, que aparece na parada do motorista, na lista de cargas e na aba Rota da viagem do painel, com a página /dashboard/entregas/[id]/tentativas (motivo, observação, foto da fachada, posição e chamado; para quem tem comprovantes). Só carga ainda em rota recebe tentativa. Repetir o envio com a mesma key não cria outra. Precisa de sinal: não entra na fila offline.

Na viagem do motorista cada parada mostra de relance: pendente, entregue, entregue com ressalva, N tentativa(s) sem sucesso ou comprovante para refazer.

Portal do cliente. A regra de quando o comprovante aparece não mudou (só depois de aprovado). Aprovado, mostra também as fotos de entrega e de canhoto que valem hoje e quem recebeu com a relação. A ressalva (tipo e descrição, sem foto) aparece assim que a entrega é registrada, porque é o cliente que precisa saber. Foto de avaria, foto de fachada, tentativa sem sucesso, posição, distância e o motivo de uma devolução não vão para o portal.

O que não entra na auditoria nem nos eventos enviados para fora: foto, assinatura, documento, posição, hash e a descrição da ressalva. A baixa registra o nome de quem recebeu e o tipo da ressalva; o reenvio, o nome; a tentativa, o motivo e o número dela.

As tabelas e colunas são criadas por prisma/sql/028-comprovantes.sql; rode npm run db:rls depois dela.

Evento fiscal de comprovante de entrega do CT-e: existe, como ação de quem tem a capacidade fiscal, na tela de CT-e ("Entrega na SEFAZ", ver CT-e). Só para comprovante aprovado, e nunca enviado sozinho. O hash que o evento pede não é o SHA-256 guardado aqui: é calculado na hora, a partir da imagem.

Ainda não existe: evento de comprovante de entrega da NF-e enviado por este sistema (o MOC do CT-e diz que o do CT-e é propagado às NF-e pela própria SEFAZ); envio automático do evento do CT-e quando o comprovante é aprovado; leitura automática do canhoto (OCR); código de confirmação por SMS; assinatura com certificado; fotos fora do banco (ficam embutidas na tabela); reenvio de comprovante e tentativa sem sucesso sem sinal (não entram na fila offline); mudança de status da carga depois de uma tentativa sem sucesso (retorno ao depósito, reagendamento) e limite de tentativas; aviso ao cliente sobre a tentativa sem sucesso; perfil por cliente ou por carga (é um só por empresa).

Atendimento e ocorrências

Chamados de clientes ou da equipe sobre uma carga ou sobre o serviço: atraso, avaria, extravio, cobrança, reentrega ou outro. As regras ficam em src/lib/ocorrencias.ts; a abertura (número e aviso) em src/lib/ocorrencias-db.ts.

  • Fluxo: Aberto → Em análise → Em tratamento → Resolvido → Encerrado. A equipe só anda para a frente e pode pular etapa (resolver direto um chamado aberto). O Resolvido pode ser reaberto e volta para Em tratamento. Encerrado é final: não muda de status nem recebe mensagem. Resolver, reabrir e encerrar gravam as datas (resolvedAt, closedAt).
  • Número: sequencial por empresa, sem buraco nem repetição, com a mesma trava da emissão de fatura (uma abertura por vez na empresa).
  • Conversa: as mensagens ficam em ordem cronológica; a descrição de quem abriu é a primeira fala. A equipe escreve uma resposta (o cliente lê) ou uma nota interna (só a equipe lê).

Painel (/dashboard/ocorrencias, no menu em Operação, ADMIN e OPERATION): os contadores por status no topo, que também filtram a lista; filtro por tipo; e "Abrir chamado", informando o código de rastreio da carga (o chamado fica ligado à carga e ao cliente dono dela), ou escolhendo um cliente, ou nenhum dos dois (chamado só interno). Na lista, a carga é um link para o rastreio dela. A tela do chamado (/dashboard/ocorrencias/[id]) tem os dados, a conversa e as trocas de status, prioridade (baixa, normal, alta) e responsável (alguém da equipe), gravadas na hora.

Portal do cliente (/portal/atendimento): "Abrir atendimento" (assunto, título, descrição e, se quiser, uma das cargas dele), a lista dos chamados da empresa dele e a conversa de cada um, onde ele responde enquanto o chamado não estiver encerrado. O cliente vê os chamados que ele abriu e os que a transportadora abriu em nome dele (com carga ou cliente informado), com a descrição. Não vê: nota interna, prioridade, responsável, o nome de quem respondeu pela transportadora, nem chamado só interno. Chamado de outro cliente responde 404, igual a um id inventado.

App do motorista (/driver/entregas/[id]/ocorrencia, pelo botão "Registrar ocorrência" de cada entrega da viagem): tipo e descrição. Nasce como chamado interno, ligado à carga e sem cliente, então não aparece no portal; a equipe decide o que dizer ao cliente. Só vale para carga de uma viagem dele em rota, e precisa de sinal (não entra na fila offline).

Rota Quem O que faz
GET /api/ocorrencias?status=&type= equipe Lista (do mais novo para o mais antigo) e contadores por status, estes sempre de todos os chamados
POST /api/ocorrencias equipe Abre chamado (type, title, description, priority, trackingCode ou clientId)
GET /api/ocorrencias/[id] equipe O chamado, a conversa inteira, os status para onde pode ir e a equipe que pode assumir
PATCH /api/ocorrencias/[id] equipe Troca status, priority e assigneeId (vazio tira o responsável); troca fora do fluxo é 409
POST /api/ocorrencias/[id]/mensagens equipe Resposta ou nota interna (body, internal)
GET e POST /api/portal/atendimento cliente Lista e abre os chamados da empresa dele (type, title, description, collectionId)
GET /api/portal/atendimento/[id] cliente O chamado e a conversa, sem notas internas
POST /api/portal/atendimento/[id]/mensagens cliente Resposta do cliente (body); encerrado é 409
POST /api/driver/entregas/[id]/ocorrencia motorista Registra ocorrência na entrega (type, description)

Abrir um chamado e trocar o status avisam os sistemas de fora (ocorrencia.aberta e ocorrencia.status, na seção Integração).

Ainda não existe: prazo de atendimento (SLA) com relógio, anexo de arquivo ou foto no chamado, e-mail ou mensagem automática ao cliente quando a transportadora responde (ele vê ao entrar no portal; o aviso por WhatsApp pode ser montado no destino dos eventos), aviso de mensagem nova, alteração de tipo, título ou carga depois de aberto, e os chamados na tela da carga. O painel inicial não conta chamados.

Minuta de despacho (impressão)

A folha que acompanha a carga, para imprimir: /dashboard/coletas/[id]/minuta, aberta pelo atalho "Minuta" de cada linha da lista de minutas. Não é documento fiscal: o rodapé diz "Documento sem valor fiscal. Não substitui o CT-e.". A montagem fica em src/lib/minuta.ts (montarMinuta, função pura); a rota é GET /api/coletas/[id]/minuta.

  • O que sai na folha, só com o que a carga já tem: nome e símbolo da empresa, o código de rastreio como número (com o mesmo código de barras Code 128 da etiqueta) e a data de emissão; remetente, destinatário e cliente pagador; origem e destino; volumes, peso, cubagem, valor da mercadoria e as NF-e (número e chave); frete, composição e condição de pagamento; viagem, veículo e motorista quando a carga está em viagem; número e chave do CT-e registrado ou autorizado em produção; observação; e as assinaturas de remetente, motorista e recebedor (com nome, documento, data e hora). Seção sem dado não aparece; campo sem dado fica em branco.

  • Documento e endereço de remetente e destinatário só existem quando há NF-e importada ligada à carga, e só entram se o nome na nota for o mesmo da carga. O endereço do destinatário é o da entrega; sem ele, o da nota.

  • Quem vê: quem lê cargas (coletasVer: toda a equipe interna). O frete segue a lista de minutas, que hoje o mostra a todos esses perfis (verFreteNaMinuta; o teste cobra que as duas fiquem iguais). Carga de outra empresa responde como carga que não existe.

  • Impressão: pelo navegador, em A4 retrato, em preto e branco; só a folha sai no papel.

  • Mais de uma folha (carga com muitas NF-e ou observação longa): a quebra de página não corta quadro ao meio (break-inside: avoid em cada seção); a carga e as observações, que podem ser maiores que uma folha, quebram entre linhas, sem partir a chave de uma nota nem deixar o título sozinho no pé da folha; e as assinaturas e o rodapé saem sempre juntos, na mesma folha ([data-fecho]). As folhas de continuação levam um cabeçalho mínimo (empresa e número), feito só com CSS, numa caixa de margem da página (@page { @top-right }, vazia na primeira folha): o navegador que não desenha caixas de margem imprime a continuação sem ele. As regras estão em estiloDeImpressao (folha.tsx) e são conferidas em tests/minuta-tela.test.tsx com uma carga de 60 notas; a impressão em papel, em cada navegador, não foi conferida.

  • Redespacho e subcontratação: quando a carga tem, a folha mostra o expedidor e o recebedor (nome, CNPJ/CPF e endereço, como foram digitados) e um quadro "Tipo de serviço", com a chave do CT-e anterior. Carga de serviço normal sem expedidor nem recebedor imprime a folha de sempre. Ver CT-e.

Ainda não existe: PDF gerado no servidor, envio da minuta por e-mail ou WhatsApp, impressão de várias minutas de uma vez, a minuta no portal do cliente e no app do motorista, numeração própria (o número é o código de rastreio) e CPF do motorista na folha.

Recebimento, conferência e depósito

Conferência dos volumes quando a carga chega, etiqueta com código de barras, posição de cada volume e a visão do que está parado no depósito. As regras ficam em src/lib/deposito.ts; o código de barras em src/lib/code128.ts. No menu em Operação, "Depósito", para ADMIN e OPERATION. Nenhuma tela ou rota daqui mostra valor de nota ou frete.

  • Volume: a carga diz quantos volumes tem, e cada um tem um código: o de rastreio da carga mais a sequência (1234567890-02). O volume só ganha linha no banco (CollectionVolume) quando é conferido: recebido, avariado (com observação) ou faltando, com peso conferido opcional, posição, quem e quando. Sem linha ele está "a conferir".
  • Conferência por leitura (/dashboard/deposito/conferencia): um campo só, sempre com o foco. Leitor de código de barras é um teclado: digita o código e aperta Enter. Lê-se o código de rastreio da carga (ou a etiqueta de um volume dela) e a carga abre com os volumes esperados; cada etiqueta lida marca o volume como recebido, e "Conferir" faz o mesmo sem leitor. Leitura repetida não conta duas vezes. Etiqueta de outra carga é recusada, e código de carga de outra transportadora responde igual a código que não existe. Em "Editar" ficam peso, avaria, faltando e posição. Só carga confirmada ou coletada é conferida.
  • Concluir: o que não foi lido fica como faltando; a conferência (WarehouseReceipt) grava quem, quando e as divergências; e a carga confirmada passa para coletada pelo mesmo caminho do painel de minutas (mudarStatusDaColeta, em src/lib/coletas-db.ts), com a linha no histórico e o evento coleta.status. Precisa de ao menos um volume presente. Enquanto a carga estiver no depósito dá para corrigir volume (o faltante que apareceu) e concluir de novo: os números gravados acompanham.
  • Divergências: de quantidade quando chegou um número de volumes diferente do declarado; de peso quando todos chegaram, todos foram pesados e a soma difere do declarado em mais de 2% (TOLERANCIA_DE_PESO_PCT).
  • Etiqueta (/dashboard/deposito/etiquetas/[coletaId], com link na lista de minutas e na conferência): uma por volume, com cliente, destino, destinatário, "Volume 2 de 3", o código legível e o código de barras Code 128 (subconjunto B), desenhado em SVG sem biblioteca. A tabela de padrões, o dígito verificador e o desenho são testados contra valores gerados por outra implementação (tests/code128.test.ts). Imprime pelo navegador, duas por linha numa A4; só as etiquetas saem no papel.
  • Posições (/dashboard/deposito/posicoes): cadastro com código curto único por empresa (A-01-03: letras, números e hífen, com ao menos uma letra, gravado em maiúsculas), descrição e ativa ou não. Aloca-se lendo o código da posição na conferência (vai para os volumes conferidos ainda sem lugar) ou no "Editar" de um volume. Posição não é apagada, é desativada.
  • Visão do depósito (/dashboard/deposito): as cargas coletadas ainda sem manifesto, da mais antiga para a mais nova, com volumes, posição, data de entrada (quando passou para coletada) e dias parada; busca por código, cliente ou posição; cartões com cargas, volumes e cada alerta, que também filtram: parada há mais de 3 dias (DIAS_PARADO_ALERTA), divergência de quantidade, volume avariado e carga sem posição (não passou pela conferência ou tem volume presente sem lugar).
Rota Quem O que faz
GET /api/deposito equipe O que está no depósito, com contadores, dias e alertas
GET /api/deposito/conferencia?codigo= equipe Acha a carga pelo código de rastreio ou pela etiqueta de um volume; devolve os volumes esperados
GET /api/deposito/coletas/[id] equipe A carga com os volumes (é o que a etiqueta imprime)
POST /api/deposito/coletas/[id]/volumes equipe Leitura (codigo ou sequence) ou correção (status, weight, damageNote) de um volume
POST /api/deposito/coletas/[id]/posicao equipe Põe volumes numa posição (locationCode, sequences); em branco tira
POST /api/deposito/coletas/[id]/concluir equipe Conclui a conferência e dá a carga como coletada
GET e POST /api/deposito/posicoes equipe Lista (com quantos volumes há em cada uma) e cadastra posição
PATCH /api/deposito/posicoes/[id] equipe Altera code, description e active

As tabelas são criadas por prisma/sql/016-deposito.sql.

Ainda não existe: leitura pela câmera do celular (só leitor que digita, ou digitação), impressão direta em impressora térmica (ZPL), inventário cíclico, mais de uma unidade (um depósito por empresa), foto da avaria, vínculo do volume com a NF-e, conferência de embarque no manifesto, e histórico de movimentação entre posições (fica só a posição atual). Carga com mais de 999 volumes não é conferida nem etiquetada volume a volume. O volume continua com a posição gravada depois que a carga sai para entrega; ela só deixa de contar na ocupação da posição.

Documentos fiscais (NF-e por XML)

Importação do XML da NF-e para guardar a nota e criar a carga com os dados dela. No menu em Operação, "Notas fiscais" (/dashboard/fiscal), para ADMIN e OPERATION. As regras ficam em src/lib/nfe.ts.

  • Leitor de XML (lerNfe): função pura, sem biblioteca. Aceita a nota com o protocolo (nfeProc) ou sozinha (NFe), com ou sem prefixo de namespace, e tira a chave de acesso (do Id de infNFe ou de protNFe/chNFe), número e série, emissão, emitente e destinatário (CNPJ/CPF, razão social, município, UF, endereço), valor total (vNF), volumes e peso bruto (somando os blocos transp/vol), natureza da operação e modalidade do frete. O dígito verificador da chave (módulo 11) é conferido, e a chave precisa ser a daquele emitente, série e número.
  • Recusas, cada uma com a sua frase: arquivo que não é XML ou está malformado, XML que não é de NF-e (CT-e, NFC-e modelo 65, qualquer outro), chave inválida, nota sem número, emitente ou valor, e arquivo acima de 1 MB (resposta 413). O XML é tratado só como texto: nada é executado, só as cinco entidades do XML e as referências numéricas são expandidas, e arquivo com DOCTYPE é recusado.
  • Nota guardada (FiscalDocument): os dados lidos, o XML original como chegou, quem importou e a carga (opcional). A chave é única por empresa: reimportar responde 409 com a nota que já existe e a carga a que ela está ligada.
  • Importar (/dashboard/fiscal, "Importar XML", um ou vários arquivos): cada arquivo vira uma linha com o que foi lido ou o motivo da recusa. A nota abre com a carga sugerida: cliente pagador = o cadastro ativo cujo CNPJ é o do emitente ou o do destinatário (se os dois são clientes, decide a modalidade do frete da nota: 0 emitente, 1 destinatário; sem cliente ou sem modalidade, o operador escolhe), remetente e destinatário, origem e destino como "Cidade - UF", volumes, peso, valor e chave da NF. O que a nota não traz vem como aviso para o operador preencher.
  • Criar a carga: confirmar cria a coleta pelo mesmo caminho do painel de minutas (criarColetaConfirmada, em src/lib/coletas-db.ts: frete pela tabela do cliente, já confirmada, com código de rastreio e a primeira linha do histórico) e liga a nota a ela na mesma transação. A chave e o valor da NF da carga são os da nota, não os do formulário. Se já existe carga com a chave da nota, não cria outra: manda ligar.
  • Ligar a uma carga que já existe: pela tela da nota, com o código de rastreio. Se a carga já tem chave de NF-e, ela precisa ser a da nota. Se não tem e ainda pode ser editada (a regra do painel, isEditable), a chave da nota é gravada nela; carga que já embarcou recebe só o anexo.
  • Consultar: lista das notas (número, emitente, destinatário, valor, carga ligada, data), busca por chave, número, CNPJ/CPF ou razão social, e download do XML original.
  • Portal do cliente: em /portal/coletas/[id] o cliente baixa o XML das notas ligadas às cargas dele.
  • DANFE em PDF: ao lado de "Baixar XML", na nota aberta do painel e na carga do portal, o botão "DANFE (PDF)" gera o documento na hora a partir do XML guardado (ver "DANFE em PDF" abaixo).
Rota Quem O que faz
GET /api/fiscal/notas?busca= equipe Notas importadas, sem o XML
POST /api/fiscal/notas equipe Importa um XML ({ xml }); devolve a nota, a carga sugerida e a carga que já tem a chave
GET /api/fiscal/notas/[id] equipe A nota e, se ainda sem carga, a sugestão
GET /api/fiscal/notas/[id]/xml equipe O XML original, como anexo
GET /api/fiscal/notas/[id]/danfe quem baixa o XML (fiscalVer) O DANFE em PDF, como anexo (<chave>-danfe.pdf)
POST /api/fiscal/notas/[id]/carga equipe Cria a carga sugerida e liga a nota a ela
POST /api/fiscal/notas/[id]/ligar equipe Liga a nota a uma carga pelo trackingCode
GET /api/fiscal/cte equipe Cargas alocadas numa viagem em montagem, em rota ou entregues, com os dados que um CT-e precisa e o CT-e emitido pelo sistema (emitido)
POST /api/fiscal/cte equipe Registra à mão cteNumber e cteKey de um CT-e emitido em outro sistema; os dois vazios desfazem
GET /api/portal/coletas/[id]/notas/[notaId] cliente O XML de uma nota de uma carga dele
GET /api/portal/coletas/[id]/notas/[notaId]/danfe cliente O DANFE em PDF de uma nota de uma carga dele

A tabela é criada por prisma/sql/017-documentos-fiscais.sql.

DANFE em PDF

O PDF não é montado aqui: quem o gera é o serviço fiscal da casa, alcançado pelo MCP único da empresa, pela ferramenta gerar_danfe (no MCP único, fiscal_gerar_danfe), que recebe o XML da NF-e e devolve o PDF em base64. O cliente fica em src/lib/fiscal-mcp.ts, sem dependência nova: três POST JSON-RPC (initialize, que devolve o cabeçalho mcp-session-id; notifications/initialized; tools/call), com a resposta lida em JSON ou em text/event-stream.

  • Ligar: FISCAL_MCP_URL=https://mcp.avilaops.com/mcp e FISCAL_MCP_TOKEN (token de serviço do TMS) no ambiente. Sem a variável o recurso fica desligado: as rotas respondem 503 e as telas não mostram o botão (a nota aberta e a carga do portal trazem danfe: true|false). O token vai como Authorization: Bearer; sem ele o MCP único responde 401 e a tela diz que não conseguiu falar com o serviço fiscal. FISCAL_MCP_TIMEOUT_MS, opcional, troca o tempo limite da conversa, que é de 20 segundos.
  • Acesso: o mesmo do XML. No painel, a capacidade fiscalVer; no portal, só nota ligada a uma carga do cliente logado (a de outro cliente, a sem carga e a de outra transportadora respondem 404, e o serviço fiscal nem é chamado).
  • Falhas, cada uma com a sua frase na tela: serviço desligado (503), serviço fora do ar ou resposta fora do protocolo (502), XML recusado pela ferramenta ou resposta que não é um PDF (502) e tempo esgotado (504). O texto que o serviço devolve no erro vai só para o log do servidor.
  • O que sai daqui: o XML da nota é enviado ao serviço fiscal a cada pedido. O PDF não é guardado: é gerado de novo a cada clique, e a resposta vai como anexo, sem cache.

Ainda não existe: o botão na linha da lista de notas (só na nota aberta), guardar o PDF gerado, gerar vários de uma vez e DACTE.

CT-e

A tela /dashboard/fiscal/cte lista as cargas alocadas numa viagem em montagem (antes da saída), em rota ou entregues, com a situação do CT-e de cada uma. Dela se confere e emite o CT-e pela SEFAZ (modelo 57, modal rodoviário, leiaute 4.00) ou, quando ela não responde, pela SVC; se baixa o XML autorizado e o DACTE; se corrige por carta de correção; se registra o comprovante de entrega na SEFAZ; se confere a situação na SEFAZ; e se cancela. O registro manual do número e da chave de um CT-e emitido em outro sistema continua existindo (cteNumber, cteKey e cteStatus da carga; a chave é conferida, nada é enviado à SEFAZ).

Na lista, cada linha mostra as ações principais (Conferir e emitir, Baixar XML, Corrigir e DACTE) e guarda as outras (Entrega na SEFAZ, Conferir na SEFAZ, Cancelar e o registro de CT-e de fora) no menu Mais; uma ação secundária sozinha fica à vista. O menu é um <details>: no computador abre por cima da tabela, no celular abre dentro do cartão, e fecha ao escolher a ação ou clicar fora. No computador as colunas têm largura fixa: o texto que não cabe (cliente, remetente, destinatário, origem e destino) corta com reticências e traz o texto inteiro na dica (title); no celular a rota quebra a linha em vez de cortar.

Regra da casa: nada é "autorizado" sem o protocolo da SEFAZ. Um CT-e só fica autorizado com cStat 100 e número de protocolo, para a chave, o ambiente e o resumo (digVal) do XML que foi enviado. Resposta "autorizado" sem protocolo, de outra chave ou de outro ambiente não autoriza nada.

O que NÃO foi provado. Não havia certificado A1 de nenhuma transportadora nem acesso à SEFAZ: nenhum CT-e foi autorizado de verdade, nem em homologação. O que existe está provado até onde dá sem a SEFAZ: o XML é validado contra os esquemas oficiais, a assinatura é conferida de volta, e a conversa SOAP com autenticação mútua é exercitada contra um servidor HTTPS local que imita a SEFAZ. A primeira emissão real pode ser rejeitada por regra que só a SEFAZ aplica (cadastro do emitente, IE do tomador, tabela de classificação do IBS/CBS): a tela mostra o código e o motivo que ela devolver. Não conferido contra o serviço real: o espaço de nomes SOAP (http://www.portalfiscal.inf.br/cte/wsdl/<serviço>V4: o MOC traz o exemplo sem o "V4", e a SEFAZ não entrega o WSDL sem certificado) e a negociação do certificado do cliente.

Passo a passo para a transportadora

  1. Credenciar-se como emissora de CT-e na SEFAZ do seu estado (ambiente de homologação e de produção) e ter o RNTRC.
  2. Em Empresa → Fiscal, preencher os dados do emitente (CNPJ, IE, endereço, RNTRC, regime, série e próximo número, CFOP, ICMS, IBS/CBS) e salvar. O ambiente nasce em homologação.
  3. Na mesma tela, enviar o certificado digital A1 (e-CNPJ, arquivo .pfx/.p12 com a senha).
  4. Alocar a carga numa viagem (Manifestos) e, em CT-e, "Conferir e emitir" e "Emitir em homologação", antes de liberar a saída. Corrigir o que a SEFAZ rejeitar.
  5. Só depois de autorizar em homologação: em Empresa → Fiscal, trocar o ambiente para produção (a tela pede o CNPJ digitado de novo) e conferir o próximo número.

Dados fiscais do emitente (FiscalIssuer, uma linha por empresa, só o administrador): CNPJ (aceita o CNPJ alfanumérico), IE, razão social, endereço com o código IBGE do município (achado na tabela de src/lib/municipios.ts; cidade que não existe é recusada), RNTRC, regime (CRT), série, próximo número, ambiente, CFOP dentro e fora do estado (só os de prestação de serviço de transporte, escolhidos em lista), situação do ICMS (00, 20, 40, 41, 60, 90 ou Simples Nacional, com uma linha que explica cada uma), a alíquota interna do ICMS (a interestadual o sistema calcula), o percentual de redução da base e a caixa "a redução vale na prestação interestadual" (os dois só na situação 20; a caixa nasce desmarcada), e os parâmetros do IBS/CBS (CST e cClassTrib escolhidos na lista dos pares que valem no CT-e, alíquotas do IBS da UF, do IBS do município e da CBS, e PIS/COFINS, que só entram na base de cálculo). Uma configuração antiga com valor que deixou de valer (o CST 400, um CFOP que não é de transporte) aparece marcada na tela, e a emissão para até a correção. Passar a produção exige digitar o CNPJ de novo. O próximo número não pode ficar abaixo de um CT-e já autorizado.

Certificado A1. O arquivo e a senha ficam cifrados no banco (AES-256-GCM, chave derivada de TMS_CHAVE_DE_DADOS, amarrada à empresa e ao campo, como as credenciais do Mercado Pago) e não voltam para a tela, para log nem para a auditoria: a leitura devolve só o titular, o CNPJ, a validade e se confere com o emitente. É recusado o certificado de outro CNPJ (o de outro estabelecimento da mesma empresa, mesma raiz de 8 posições, é aceito, como o MOC permite), vencido, sem CNPJ (e-CPF) ou que não seja A1 (pela política 2.16.76.1.2.1.x). Não é conferida a cadeia até a ICP-Brasil nem a revogação: quem confere é a SEFAZ. Sem TMS_CHAVE_DE_DADOS o envio fica desligado e a tela explica.

O documento (src/lib/cte/montar.ts, função pura; src/lib/cte/preparar.ts decide quem é quem):

  • remetente e destinatário saem da NF-e importada e ligada à carga (CNPJ/CPF, IE, endereço com código IBGE). Sem NF-e, só quando o nome é o do cliente pagador (cadastro) ou, para o destinatário, o de um destinatário frequente com CNPJ/CPF; carga interestadual sem NF-e não emite (a SEFAZ rejeita, 813);
  • tomador é o cliente pagador: pelo CNPJ ele é o remetente (toma3 0), o destinatário (toma3 3) ou um terceiro (toma4, com o endereço do cadastro);
  • início e fim da prestação são a origem e o destino da carga; valor da prestação é o frete; valor da carga é o valor da NF;
  • chave de acesso com dígito verificador (com o CNPJ alfanumérico: cada caractere vale o código ASCII menos 48), cCT sorteado, emissão normal, QR Code em infCTeSupl;
  • ICMS pela configuração e pelas UF da prestação (resolverIcms): ICMS00, ICMS20 (base reduzida), ICMS45 (40 e 41), ICMS60 (substituição tributária), ICMS90 e ICMSSN. Alíquota: a interna da configuração quando a prestação começa e termina na UF do emitente; a interestadual da Resolução do Senado 22/1989 quando termina em outra (7% do Sul e do Sudeste, menos o ES, para Norte, Nordeste, Centro-Oeste e ES; 12% no resto);
  • prestação que começa em outra UF (CFOP 5932/6932, regra G051): o ICMS vai no grupo ICMSOutraUF (CST 90), devido à UF de início, com a alíquota interestadual a partir dela; a conferência avisa do recolhimento à outra UF. O Simples Nacional continua em ICMSSN;
  • situação 20 (base reduzida) na prestação interestadual: só com confirmação. A redução é benefício de cada estado ou convênio. Dentro do estado ela é aplicada; para outro estado, só quando a configuração diz que ela vale lá (reducaoNaInterestadual, coluna icmsReductionInterstate). Com o campo desligado (o padrão) o CT-e interestadual não é emitido, nem com a redução nem sem ela: a conferência manda consultar o contador e diz onde ligar;
  • o que o sistema não sabe calcular bloqueia a emissão, com a frase "consulte o contador" (ver "Bloqueado, à espera do contador", abaixo);
  • IBS e CBS: grupo imp/IBSCBS e total imp/vTotDFe. Base = prestação menos ICMS, PIS e COFINS (LC 214/2025, art. 12). CST que valem no CT-e: 000 (com valores), 200 (com valores e o grupo gRed em cada tributo: percentual de redução da tabela e alíquota efetiva) e 410 (sem valores). O par CST × cClassTrib é conferido contra a tabela oficial embutida (CLASSIFICACOES_DO_IBSCBS, 12 pares). Obrigatório para o regime normal; o Simples pode ficar sem;
  • datas com hora (dhEmi, dhEvento) no fuso da UF do emitente (UTC-3; UTC-4 em AM, MT, MS, RO e RR; UTC-5 no AC), e o AAMM da chave sai do mesmo relógio;
  • responsável técnico (infRespTec): é a empresa cadastrada em Empresa → Fiscal: vai sempre que o cadastro tem CNPJ, telefone, contato e e-mail válidos; faltando algum o grupo não é montado e a conferência avisa que a UF pode rejeitar (867). Com RESPTEC_CSRT_ID e RESPTEC_CSRT o grupo leva também o código de segurança (idCSRT e hashCSRT, o Base64 do SHA-1 de CSRT + chave de acesso); o CSRT não vai para o XML nem para o log;
  • em homologação, o nome do remetente e do destinatário é a frase que a SEFAZ exige;
  • veículo e motorista da viagem vão na observação (o modal rodoviário do CT-e 4.00 não tem esses grupos: são do MDF-e).

Assinatura (src/lib/cte/assinar.ts): XMLDSig "enveloped" do infCte (e do infEvento), RSA-SHA1, resumo SHA-1, C14N 1.0, transformações Enveloped e C14N: os algoritmos que o MOC 4.00 exige (item 3.2.4), os mesmos que o ERP da casa usa na NF-e.

Transmissão (src/lib/cte/soap.ts, sefaz.ts, enderecos.ts): SOAP 1.2 sobre TLS 1.2 com autenticação mútua (o certificado da empresa), para CTeRecepcaoSincV4 (XML em GZip + Base64), CTeStatusServicoV4, CTeConsultaV4 e CTeRecepcaoEventoV4, nos endereços que o Portal do CT-e publica para cada autorizador (MT, MS, MG, PR, RS, SP, SVRS e SVSP), em homologação e produção. O certificado do servidor da SEFAZ é sempre conferido, contra as raízes do Node mais as raízes v5 e v10 da ICP-Brasil (src/lib/cte/raizes-icp-brasil.ts, com a origem e as impressões digitais). Tempo limite de 30 segundos.

Numeração (CteNumbering, por empresa + ambiente + série, com a mesma trava da fatura): emissões simultâneas nunca levam o mesmo número. Rejeição não consome o número (a SEFAZ não grava CT-e rejeitado): o reenvio da mesma carga usa o mesmo número, com chave nova. Autorização consome, e a rejeição 539 (número usado por outro documento) também. Envio sem resposta não decide nada: o CT-e fica "sem resposta" com o mesmo XML, e a próxima tentativa consulta a SEFAZ pela chave antes de reenviar (se ela autorizou, grava; se não consta, reenvia o mesmo XML). Um buraco na numeração só aparece se uma carga rejeitada nunca for reenviada; o CT-e 4.00 não tem mais inutilização.

O CT-e da carga. Só o CT-e autorizado em produção preenche cteKey, cteNumber e cteStatus da carga. O de homologação fica só na tabela Cte, marcado como homologação em toda a tela: não tem valor fiscal.

Cancelamento: evento 110111, com justificativa de 15 a 255 letras, até 168 horas da autorização. Só fica cancelado com o evento registrado (135, com protocolo) ou quando a SEFAZ responde que já estava cancelado (218) e a consulta confirma (101). As respostas 134 e 136 (evento guardado com ressalva) não são tratadas como recusa nem como cancelamento: só cancelam se a consulta pela chave disser 101. O CT-e com carta de correção registrada, ou com comprovante de entrega em vigor, não é cancelado: o pedido é recusado antes de ir à SEFAZ, com a rejeição que ela daria (523 e 862). O CT-e autorizado pela SVC é cancelado na SVC.

Eventos do CT-e (tabela CteEvent, no molde da MdfeEvent; prisma/sql/033-cte-eventos.sql): cada evento que a SEFAZ registrou fica com o XML assinado que foi enviado, o procEventoCTe (evento + retorno) e o protocolo. Evento recusado ou sem resposta não vira linha. O desenho é o do MDF-e: 135 registra; 134 (vinculado ao CT-e, com alerta) também, porque estes eventos não mudam a situação do documento; 136 (guardado sem vínculo) não, e a tela diz que ele ficou guardado lá; a duplicidade (631) não é tomada por registro: a tela manda conferir na SEFAZ, que traz o evento. O cancelamento continua também nas colunas cancel* do Cte (é onde estão os cancelamentos anteriores à tabela, que a lista de eventos mostra do mesmo jeito); os novos gravam nos dois lugares.

Carta de correção (evento 110110, ação "Corrigir" na linha do CT-e autorizado): uma lista de correções, cada uma com o grupo e o campo (os nomes das tags do XML, por exemplo compl e xObs), o valor novo e, quando o campo está numa lista, o item. A tela mostra as cartas já registradas. O servidor recusa (400) o que a carta não pode corrigir: os 57 campos da tabela do MOC 4.00 (item 13.1: identificação do documento, tpEmis, dhEmi, CNPJ/CPF/IE de emitente, tomador, remetente e destinatário, valor da prestação e dos componentes, chave da NF-e e todos os campos de ICMS) e, por leitura deste sistema do art. 58-B, I, do Convênio SINIEF 06/89, os grupos do IBS e da CBS inteiros e o vTotDFe (não estão na tabela do manual, que é anterior à Reforma). A comparação é pelo nome, sem olhar caixa nem acento; o que a carta diz no valor novo o sistema não confere. xCondUso vai com a literal do manual (a forma sem acentos, que o esquema aceita). Sequência crescente por CT-e, até 20 (regra O01). Só CT-e autorizado e não cancelado. Limite deste sistema, não da norma: 20 correções por carta.

Comprovante de entrega na SEFAZ (evento 110180, ação "Entrega na SEFAZ" na linha do CT-e autorizado cuja carga tem comprovante aprovado; cancelamento pelo evento 110181, "Cancelar comprovante na SEFAZ"): é ação de quem tem a capacidade fiscal, nada é enviado sozinho. O evento leva o protocolo do CT-e, a data e hora da entrega (a da baixa do motorista, que é quando o comprovante foi criado), o nome e o documento de quem recebeu, a posição quando o comprovante a tem, as chaves das NF-e que o CT-e transporta (infEntrega) e o hashEntrega: o Base64 do SHA-1 da chave do CT-e concatenada com o Base64 da imagem (MOC 4.00, item 6.8, campo IP09). A imagem é lida de onde o módulo de comprovantes a guarda (ProofPhoto.dataUrl): a foto do canhoto que vale hoje; sem ela, a foto da entrega; no comprovante antigo, a foto dele; por último, a assinatura colhida na tela. O SHA-256 que o módulo já guardava não serve para o evento (a norma pede SHA-1 da chave + imagem), e continua como está. Sem imagem, com nome ou documento de menos de 2 letras, ou com a entrega anterior à emissão do CT-e, o evento não é montado e a tela diz por quê. Um comprovante por CT-e (regra O08); cancelado, dá para registrar outro. Documento, posição, imagem e hash não vão para a auditoria nem para a tela. Quando a norma exige o evento: não foi possível afirmar. O Ajuste SINIEF 09/07 não pôde ser lido na fonte oficial (ver a tabela de normas), e a tela diz, em uma linha, para registrar quando a legislação do estado ou o tomador exigir, e o que o MOC garante: um comprovante por CT-e, e com ele em vigor o CT-e não é mais cancelado.

Contingência pela SVC (tpEmis 7 = SVC-RS, 8 = SVC-SP). A SVC autoriza o CT-e (há protocolo; não é emissão off-line), no lugar da SEFAZ de origem, e só quando esta a aciona. Qual SVC atende cada UF, pelo Portal do CT-e: SVC-SP para quem autoriza na SVRS, em MG, no PR e no RS; SVC-RS para quem autoriza na SVSP, em MT, em MS e em SP. Os endereços são os da SVRS (SVC-RS) e os da SVSP (SVC-SP). Na tela, "Conferir e emitir" oferece "A SEFAZ não responde? Emitir pela SVC" só depois de uma emissão normal que não se resolveu (sem resposta, autorizadora parada com 108 ou 109, ou sem conexão), e pede a justificativa (15 a 256 letras). O que o servidor faz, nesta ordem:

  1. pergunta o status à SVC: só segue com 113 ("SVC em operação" para a UF); com 114 ("desabilitada pela SEFAZ de origem") ou sem resposta, nada é emitido nem numerado;
  2. se há um envio normal sem resposta, consulta-o pela chave na SEFAZ de origem. Autorizado: é gravado, e a SVC não emite (a carga já tem CT-e). Cancelado lá: a linha vira rejeitada. Sem resposta, "não consta" ou qualquer outra coisa: a SVC emite, e o envio fica "sem resposta", à espera de "Conferir na SEFAZ" (o manual diz que a consulta só é possível quando a SEFAZ de origem volta, item 4.5.6, e que os pendentes de retorno são tratados depois, item 11.5.3);
  3. monta outro CT-e, com número novo (o manual veda reutilizar em contingência o número transmitido como normal) e o tpEmis da SVC, que entra na chave. Só isso muda no XML: dhCont e xJust não vão. A regra de validação, que é o que a SEFAZ aplica, é literal: "G008. Se forma de emissão do CTe for diferente de 5 (FS-DA): dhCont e xJust não devem ser informados" (MOC 4.00, Anexo I; rejeição 586, "Data e Justificativa de entrada em contingência não devem ser informadas para tipo de emissão diferente de FS-DA"), e a NT 2026.004 a reescreve como "Se forma de emissão do CTe for diferente de 2 (Contingência OffLine)". Os tpEmis 7 e 8 são diferentes de 5 e de 2: os campos são proibidos na SVC. A anotação do esquema ("Informar apenas para tpEmis diferente de 1", cteTiposBasico_v4.00.xsd) é mais larga que a regra e não a contraria: o esquema deixa o par opcional, e quem decide é a G008, o QR Code continua o da UF do emitente e sem sign. A justificativa fica na auditoria (cte.contingencia);
  4. envia à SVC e grava o protocolo dela.

Depois, "Conferir na SEFAZ" no envio que ficou para trás resolve: se a SEFAZ de origem o autorizou, ele é gravado como autorizado sem tomar o lugar do CT-e da SVC, com a frase que manda cancelar (item 11.5.3), e a linha da carga mostra "O CT-e nº N também foi autorizado: cancele-o"; se não consta, é encerrado como rejeitado e o número não volta a ser usado. O CT-e da SVC é consultado e cancelado na SVC ("os pedidos de cancelamento na modalidade SVC somente poderão afetar documentos autorizados em contingência pela correspondente SVC", MOC, Visão Geral, item 6.2). Carta de correção, comprovante de entrega e o cancelamento dele (110110, 110180 e 110181) de um CT-e da SVC vão para a autorizadora normal da UF: a SVC os rejeita ("Se ambiente de Autorização SVC: rejeitar se Tipo de Evento for diferente de EPEC (110113) ou Cancelamento (110111)", item 5.1.5, regra N09, rejeição 530), e a tabela dos tipos de evento (item 3.7.1) dá a "SEFAZ Autoriz." como local deles. Se a SEFAZ de origem responde que o CT-e não consta na base (regra N20, rejeição 217), a tela diz "O CT-e emitido pela SVC (contingência) ainda não foi sincronizado com a SEFAZ de origem ... Tente mais tarde", nada é gravado e a sequência do evento não é consumida. O manual não descreve como nem quando o CT-e da SVC chega à base da origem: é ponto a observar na homologação. O DACTE não mudou: o serviço fiscal confirma o protocolo do cteProc da SVC (autorizado: true) e imprime o documento normal.

Conferir na SEFAZ (ação na linha do CT-e autorizado, cancelado ou sem resposta): consulta pela chave e atualiza o que o sistema tem. Quem decide é o cStat do topo: 100 autorizado, 101 cancelado (a denegação, 110, não existe mais no CT-e 4.00 e não é tratada). CT-e autorizado aqui e cancelado lá passa a cancelado, com auditoria e aviso. Os eventos vinculados que a consulta devolve (procEventoCTe, só os 135 e 134, da chave consultada) e que o sistema não tinha são gravados uma vez, marcados "registrado fora deste sistema"; um cancelamento de comprovante feito por fora marca o comprovante. Qualquer outra resposta não muda nada, e a tela mostra o que a SEFAZ disse.

Eventos, SVC e conferência (norma → código → teste). Fontes lidas em 12/10/2026: MOC do CT-e 4.00, Visão Geral e Anexo I (PDF do Portal do CT-e), NT 2026.004 v1.00, os XSD evCCeCTe, evCECTe, evCancCECTe, retConsSitCTe e cteTiposBasico do pacote PL_CTe_400_NT2026.004 RTC_1.00, e a página "Relação de Serviços Web" do portal (produção e homologação). Testes em tests/cte-eventos.test.ts (sem banco), tests/cte-eventos-rotas.test.ts (rotas, com o servidor que imita a SEFAZ) e tests/cte-eventos-tela.test.tsx.

Regra Norma Código Teste
Leiaute da carta de correção e a literal do xCondUso MOC, Visão Geral, item 6.4 (campos IP01 a IP08); evCCeCTe_v4.00.xsd montarCartaDeCorrecao, CONDICAO_DE_USO_DA_CARTA em src/lib/cte/eventos.ts "o evento assinado passa no esquema do evento e o detalhe, no evCCeCTe..."
Campos que a carta não corrige MOC, item 6.4.1, regra O02 (681), e item 13.1; art. 58-B do Convênio SINIEF 06/89, transcrito no item 6.4 CAMPOS_PROIBIDOS_NA_CARTA, GRUPOS_DE_IMPOSTO_FORA_DA_CARTA, campoProibidoNaCarta, cartaDeCorrecaoSchema em src/lib/cte.ts "bloqueia cada campo da tabela do MOC 4.00, item 13.1..."; "valor do imposto, valor da prestação..."; rotas "recusa no servidor o campo proibido..."
Sequência da carta, de 1 a 20; item de 1 a 99 MOC, item 6.4.1, regras O01 (636) e O03 (522) corrigirCte, proximaSequencia em src/lib/cte-db.ts rotas "registrada pela SEFAZ: ... a sequência cresce..."; "são no máximo 20 cartas por CT-e..."
Só CT-e autorizado e não cancelado MOC, item 6.4 ("exige CT-e autorizado") e regra O06 (218) prepararEvento rotas "recusa no servidor o campo proibido ... (409)"
Evento só vale com o retorno: 135 e 134 registram; 136 e 631 não MOC, itens 5.1.5 (regra N16) e 5.1.7 enviarERegistrar em src/lib/cte-db.ts rotas "nada é dado como registrado sem o retorno da SEFAZ..."; "a resposta se perdeu depois de a SEFAZ registrar..."
Carta de correção e comprovante impedem o cancelamento do CT-e MOC, item 6.2.1, regras O14 (523) e Q16 (862) cancelarCte rotas "... e o CT-e não pode mais ser cancelado"; "só com comprovante APROVADO..."
Leiaute do comprovante de entrega e do cancelamento dele MOC, itens 6.8 e 6.9; evCECTe_v4.00.xsd, evCancCECTe_v4.00.xsd montarComprovanteDeEntrega, montarCancelamentoDoComprovante "o evento assinado passa nos esquemas, com as datas no fuso da UF..."; "o cancelamento do comprovante leva o protocolo..."
hashEntrega: Base64 do SHA-1 de chave + Base64 da imagem MOC, item 6.8, campo IP09; anotação do XSD hashDaEntrega; comprovanteAceito (de onde vem a imagem) "hashEntrega: Base64 do SHA-1..."; rotas "a imagem do hash: o canhoto que vale hoje..."
Um comprovante por CT-e; infEntrega com as NF-e do CT-e, sem repetir; entrega não anterior à emissão MOC, item 6.8.1, regras O06 (872), O08 (870), O09 (865), O15 (861) e O17 (864) registrarEntrega, impedimentoDaEntrega, chavesDeNfeDaEntrega rotas "só com comprovante APROVADO e com imagem..."; "sem imagem, com recebedor incompleto ou com a entrega anterior..."
Cancelamento do comprovante pelo protocolo dele MOC, item 6.9.1, regra O03 (866) cancelarEntrega rotas "só com comprovante APROVADO..." (o nProtCE)
Qual SVC atende cada UF e em que endereço Portal do CT-e, "Relação de Serviços Web", bloco "Autorizadores de contingência" svcDaUf, enderecosDaSvc em src/lib/cte/enderecos.ts "cada autorizador tem a SVC que o Portal do CT-e indica..."; "a SVC-RS atende nos endereços da SVRS..."
No XML da SVC só muda o tpEmis; sem dhCont/xJust ("se forma de emissão do CTe for diferente de 5 (FS-DA): dhCont e xJust não devem ser informados"; na NT 2026.004, "diferente de 2"); QR Code sem sign MOC, Visão Geral, item 11.3.3; Anexo I, campo 14 e regras G008 (586), G011 (516), G213 e G216 (854); NT 2026.004, item 6 montarCte (DadosDoCte.svc), chaveDoCte (tipoDeEmissao) "no XML só muda o tpEmis..."
A SVC só vale ativada pela SEFAZ de origem: status 113 MOC, Visão Geral, itens 4.6.1, 4.6.5 (regras M04 e M05) e 11.2 (c); regra C09 (114) lerStatus (svcAtiva), prepararSvc "o status pedido à SVC diz se ela está ativa..."; rotas "autorizadora parada (108): com a SVC inativa nada é numerado..."
Número novo na contingência; pendentes de retorno tratados depois, com cancelamento do que a origem autorizou MOC, Visão Geral, itens 4.5.6, 11.3.3 e 11.5.3 prepararEnvio, prepararSvc, gravarAutorizacao (substituidoPor), conferirNaSefaz, ctesDaCarga rotas "envio sem resposta e a SEFAZ de origem fora do ar..."; "o envio sem resposta tinha sido autorizado pela origem..."; "... confirma autorizado NA HORA..."
CT-e da SVC: consulta e cancelamento na SVC; carta de correção, comprovante de entrega e o cancelamento dele na autorizadora normal da UF; 217 lá = ainda não sincronizado, sem gravar nem consumir a sequência MOC, Visão Geral, itens 3.7.1 (local dos eventos), 4.5.6, 5.1.5 (regras N09, 530, e N20, 217) e 6.2 (restrição do cancelamento na SVC) destinoDaChave (cancelamento e consulta), prepararEvento e enviarERegistrar (os outros eventos), SVC_AINDA_NAO_SINCRONIZADA rotas "autorizadora parada (108)..." (fim: 217, depois sincronizado); "CT-e da SVC sem carta nem comprovante: o cancelamento vai para a SVC..."; tela "as ações da linha: Corrigir e Conferir na SEFAZ no autorizado (também no da SVC)..."
Comprovante de entrega sem infEntrega quando o CT-e não é de serviço normal MOC, Visão Geral, item 6.8.1, regra O10 (871) chavesDeNfeDaEntrega em src/lib/cte/eventos.ts redespacho "comprovante de entrega: as NF-e só são relacionadas quando o CT-e é de serviço normal..."
Consulta: o topo decide (100, 101); eventos em procEventoCTe MOC, Visão Geral, itens 4.5.2 (campos GR05, GR08 e GR09), 4.5.3 e 4.5.6; retConsSitCTe lerConsulta, lerEventosDaConsulta em src/lib/cte/sefaz.ts; conferirNaSefaz, gravarEventosDaConsulta "lê tipo, sequência, protocolo, data e o XML de cada evento vinculado..."; rotas "cancelado por fora (101 no topo)..."; "grava os eventos vinculados que o sistema não tinha..."

Conferência dos XML novos no validador do serviço fiscal (validar_xml_fiscal, tipo: "cte", em 12/10/2026, pacote PL_CTe_400_NT2026.004 RTC_1.00): 8 de 8 válidos. O CT-e com tpEmis 7 e com tpEmis 8 (assinados) e o cteProc de cada um (esquemas cte_v4.00.xsd ou procCTe_v4.00.xsd, mais o do modal rodoviário); a carta de correção (eventoCTe_v4.00.xsd + evCCeCTe_v4.00.xsd); o comprovante de entrega com posição e duas NF-e, e sem posição nem NF-e (evCECTe_v4.00.xsd); o cancelamento do comprovante (evCancCECTe_v4.00.xsd). O serviço tem o esquema de todos os eventos usados. gerar_dacte com o cteProc da SVC-RS devolveu o PDF com autorizado: true (o DACTE sai com o protocolo, sem marca de contingência); com o CT-e sem protocolo, autorizado: false, e por isso este sistema não o entrega. A ferramenta não aplica as regras de negócio da SEFAZ nem confere a assinatura.

O que NÃO foi provado nos eventos e na SVC. Nada disto foi exercitado contra a SEFAZ nem contra a SVC de verdade (não há certificado). Em particular: se a SVC responde mesmo 113 quando está ativa (é o que o manual diz; se na prática vier outro código, a emissão pela SVC recusa, sem numerar); se o QR Code de um CT-e da SVC deve mesmo levar o endereço da UF do emitente (leitura do item 11.3.3, que só manda mudar o tpEmis, e da regra G213); os endereços da SVC com certificado; o comportamento real das rejeições 523, 862, 870 e 631, que o servidor de teste imita pelo texto do manual; se e quando a SEFAZ de origem passa a ter o CT-e que a SVC autorizou (é o que a carta de correção e o comprovante de entrega dele precisam: o manual manda esses eventos à autorizadora e não descreve a sincronização; até lá a resposta esperada é 217); e dhCont/xJust na SVC: a regra G008 os proíbe pelo texto, mas a anotação do esquema fala em "tpEmis diferente de 1"; se a SVC de verdade rejeitar o CT-e sem eles, é ponto a decidir na homologação.

Redespacho e subcontratação (tpServ 1, 2 e 3). É o caso em que outra transportadora contrata esta (qual dos três tipos é o de cada operação, quem diz é o contador: a definição legal não foi lida, ver as fontes abaixo). A carga guarda o tipo de serviço (padrão: normal), a chave do CT-e anterior (o da transportadora que contratou), o expedidor (quem entrega a carga a você) e o recebedor (quem a recebe de você): colunas novas em Collection (prisma/sql/035-redespacho.sql), regras em src/lib/redespacho.ts. No formulário de minuta ficam num bloco fechado, "Redespacho / subcontratação", com expedidor e recebedor em abas no celular; são gravados pelas rotas de sempre (POST /api/coletas, PATCH /api/coletas/[id]), e por isso só se alteram com a carga fora de viagem, como a chave da NF-e. A minuta de despacho imprime o tipo de serviço, a chave do CT-e anterior, o expedidor e o recebedor.

  • Por que na carga, e não na conferência do CT-e: a conferência só lê (o pedido de emissão leva o id da carga e nada mais), e expedidor, recebedor e documento anterior são dados da carga, que a minuta também imprime. O que cada tipo exige é cobrado na conferência, não ao salvar: a carga pode nascer antes de a chave do CT-e anterior existir.
  • No celular, o bloco é uma etapa. No formulário de minuta (/dashboard/coletas), os três blocos opcionais ("Janela, prioridade e cubagem", "Endereço da entrega" e "Redespacho / subcontratação") abrem, no celular, como uma etapa em tela cheia: o resto do formulário e o "Salvar Minuta" saem da frente, o título vira o da etapa e o botão Concluir volta ao formulário com o que foi digitado guardado (salvar é lá). De volta, o bloco fica numa linha só ("editar ›"). No computador nada mudou: o bloco abre e fecha no lugar. A tela decide pelo tamanho da janela na hora do toque (matchMedia, abaixo de 768 px).
  • Quem contrata é um cliente. A transportadora contratante é o cliente pagador da carga: o cadastro de clientes já tem o que o documento pede dela (CNPJ, IE, nome e endereço), e não há tabela nova. O cadastro de parceiros de transporte é para o caso inverso (quem esta empresa contrata).
  • Tomador: é sempre o cliente pagador, e fora do serviço normal a SEFAZ exige que ele seja quem emitiu o CT-e anterior. Por isso não há escolha na tela: a chave do CT-e anterior tem de ser de um CT-e do cliente pagador (mesmo CNPJ base), senão é pendência. Se o cliente pagador é também o expedidor ou o recebedor, o tomador vai nesse papel (toma3 1 ou 2); senão, como "outro" (toma4), com o endereço do cadastro.
  • Documento anterior (infCTeNorm/docAnt): um emissor (emiDocAnt) com o CNPJ que está na chave, a IE do cadastro do cliente (só quando a chave é desse mesmo CNPJ, e não de outra filial), a UF da chave e o nome do cliente; e a chave (idDocAnt/idDocAntEle/chCTe). Uma chave por carga.
  • Notas: na subcontratação e no redespacho o infDoc continua obrigatório, como no serviço normal; no redespacho intermediário ele não vai (o CT-e sai sem NF-e), e remetente e destinatário podem faltar.
  • Início e fim da prestação: continuam a origem e o destino da carga. O MOC não amarra cMunIni/cMunFim ao expedidor nem ao recebedor (não há regra de validação que os compare), e na subcontratação exige que sejam os do CT-e da contratante. Quando a cidade do expedidor ou do recebedor difere da origem ou do destino, a conferência avisa. O ICMS (resolverIcms) não muda.
  • CFOP: fora do serviço normal, o documento sai com 5351 ou 6351 ("prestação de serviço de transporte para execução de serviço da mesma natureza"), e não com o CFOP da configuração, que é o do tomador de costume da empresa; a conferência avisa e manda confirmar com o contador. A prestação que começa em outra UF continua 5932/6932.
  • ICMS da subcontratação: o sistema NÃO decide. Vários estados têm regra própria (diferimento, dispensa de destaque para o subcontratado), que não foi conferida em fonte oficial. O CT-e sai com a situação tributária da configuração da empresa, e a conferência avisa: "Na subcontratação, confirme com o contador se o ICMS é devido por você ou pela contratante".
  • Serviço normal com tomador "outro" contribuinte: se esse tomador for uma transportadora, a SEFAZ rejeita (746); o sistema não sabe se ele emite CT-e, e avisa.
  • MDF-e: não muda. O MDF-e do prestador de serviço (tpEmit 1) relaciona o CT-e próprio pela chave, qualquer que seja o tpServ dele: o MOC do MDF-e 3.00b (Visão Geral e Anexo I) e as Notas Técnicas lidas não têm regra nem campo sobre subcontratação, redespacho ou tipo de serviço do CT-e.
  • O caso inverso (esta empresa subcontratar outra transportadora) está em Parceiros e frete contratado, "Subcontratação contratada": o CT-e desta empresa continua sendo o normal, e o contrato de frete da viagem guarda a chave do CT-e de subcontratação dela.

Fontes lidas em 12/10/2026: MOC do CT-e 4.00, Anexo I (leiaute e regras G) e Visão Geral (itens 3.7.1, 3.7.2 e 6.8), o XSD cteTiposBasico_v4.00.xsd do pacote PL_CTe_400_NT2026.004 RTC_1.00, as NT 2025.001, 2026.001, 2026.002 e 2026.004 (nenhuma mexe em tpServ, docAnt, exped ou receb), a Tabela de CFOP do Portal Nacional da NF-e (IT 2023.002 v2.10, pela ferramenta consultar_cfop) e o MOC do MDF-e 3.00b. Não lidos: o Convênio SINIEF 06/89 (a definição legal de subcontratação e de redespacho) e o Ajuste SINIEF 09/07: o site do CONFAZ recusou a conexão em 12/10/2026, e o arquivo da web não tinha cópia nos endereços tentados. Por isso a tela diz o que o documento exige em cada tipo, e não quando a operação é de um tipo ou de outro. Testes em tests/redespacho.test.ts (regras, XML no XSD e rotas) e tests/redespacho-tela.test.tsx.

Regra Norma Código Teste
tpServ: 0 normal, 1 subcontratação, 2 redespacho, 3 redespacho intermediário (4, multimodal, fora) MOC, Anexo I, campo 25 do ide; XSD TP_SERV, tipoDeServico em src/lib/redespacho.ts; montarCte "os códigos do tpServ são os do MOC..."
Documento anterior obrigatório nos tipos 1, 2 e 3 e proibido no normal Anexo I, regras G059 (521) e G060 (747) prepararCte (semCteAnterior, AVISO_DA_CHAVE_SEM_USO), documentoAnteriorDoXml "fora do serviço normal a chave do CT-e anterior é obrigatória..."; "serviço normal não leva documento anterior..."; rotas "a conferência lista o que falta em cada tipo..."
Estrutura do docAnt: emiDocAnt (CNPJ, IE + UF juntas ou nenhuma, xNome) e idDocAnt/idDocAntEle/chCTe, depois do infDoc Anexo I, campos 337 a 353; XSD (infCTeNorm/docAnt) documentoAnteriorDoXml, DocumentoAnteriorDoCte em src/lib/cte/montar.ts "subcontratação (tpServ 1): documento anterior depois das notas..."; "redespacho intermediário (tpServ 3)..." (sem IE, sem UF)
Chave do CT-e anterior: 44 posições, dígito, modelo 57, UF, ano de 2009 ao atual, mês, CNPJ e número Anexo I, regra G065 (844) problemaDaChaveDoCteAnterior, CAMPOS_DO_REDESPACHO "a chave do CT-e anterior: 44 posições..."; "os schemas da carga limpam a chave..."; rotas "validação (400) e permissão..."
Quem emitiu o CT-e anterior é o CNPJ da chave e é o tomador (CNPJ base) Anexo I, regras G062 (733) e G063 (745) emissorDaChave, prepararCte (CTE_ANTERIOR_DE_OUTRA_EMPRESA) "fora do serviço normal a chave..." (outra empresa; outra filial)
Tomador que é o expedidor ou o recebedor: toma3 1 e 2 Anexo I, campo 36 e regras G019 (461) e G020 (462) tomadorDoXml (TOMA3), prepararCte "redespacho (tpServ 2)... toma3 = 1"; "serviço normal... toma3 = 2"; "expedidor e recebedor: opcionais, mas completos..."
Expedidor e recebedor: opcionais; obrigatórios no redespacho intermediário; ordem rem, exped, receb, dest Anexo I, campos 136 a 171 e regras G055 (474) e G056 (475); XSD participanteDoXml, parteInformada, SEM_EXPEDIDOR, SEM_RECEBEDOR "redespacho (tpServ 2), sem e com expedidor e recebedor..."; "redespacho intermediário: exige expedidor e recebedor..."
Em homologação, o nome do expedidor e do recebedor é a frase da SEFAZ Anexo I, regras G003 (647) e G004 (648) participanteDoXml "redespacho (tpServ 2)..." (homologação e produção)
infDoc obrigatório nos tipos 0, 1 e 2 e proibido no 3; remetente e destinatário podem faltar no 3 Anexo I, regras G036 (693), G037 (694), G053 (469) e G054 (470) documentosDoXml, prepararCte (AVISO_DAS_NOTAS_NO_INTERMEDIARIO) "redespacho intermediário (tpServ 3): sem as notas..."; rotas "redespacho intermediário, do começo ao fim..."
Início e fim da prestação = origem e destino da carga; na subcontratação, os do CT-e da contratante (aviso) Anexo I, campos 26 a 31 (sem regra que os ligue a exped/receb) e regra G069 (832) prepararCte (avisoDoLocal, AVISO_DOS_MUNICIPIOS_NA_SUBCONTRATACAO) "subcontratação: o cliente pagador é o tomador..."; "expedidor e recebedor..." (aviso da cidade)
CFOP 5351/6351 quando o tomador é outra transportadora; 5932/6932 continua na frente Tabela de CFOP (IT 2023.002 v2.10), códigos 5351 e 6351; Anexo I, regras G050 e G051 cfopDaPrestacao (CFOP_DA_MESMA_NATUREZA), avisoDoCfopDaMesmaNatureza "CFOP: 5351 ou 6351 quando o tomador é outra transportadora..."
Serviço normal com tomador transportadora é rejeitado: aviso Anexo I, regra G064 (746) AVISO_DO_TOMADOR_TRANSPORTADORA "serviço normal não leva documento anterior..."
ICMS da subcontratação: situação da configuração, com aviso; o sistema não decide sem fonte lida (regra de cada estado): decisão do contador AVISO_DO_ICMS_NA_SUBCONTRATACAO; resolverIcms sem mudança "subcontratação: o cliente pagador é o tomador..."
Comprovante de entrega de CT-e que não é de serviço normal vai sem as NF-e MOC, Visão Geral, item 6.8, regra O10 (871) chavesDeNfeDaEntrega "comprovante de entrega: as NF-e só são relacionadas..."
Carga, minuta e telas (não é norma) CAMPOS_DO_REDESPACHO, servicoDaCarga; montarMinuta; coletas/page.tsx, fiscal/cte/page.tsx, minuta/folha.tsx rotas "a carga nasce como serviço normal..."; "isolamento entre empresas..."; "a minuta de despacho mostra..."; telas em redespacho-tela.test.tsx

Conferência no serviço fiscal (12/10/2026, pelo MCP da casa; o endereço fiscal.avilaops.com deixou de existir nesse dia). validar_xml_fiscal (tipo: "cte", pacote PL_CTe_400_NT2026.004 RTC_1.00, esquemas cte_v4.00.xsd e do modal rodoviário): o CT-e de redespacho intermediário (tpServ 3, com expedidor, recebedor e docAnt, sem infDoc) passou, válido, 0 erros. Os outros seis cenários novos (subcontratação; redespacho sem e com expedidor e recebedor; intermediário com remetente e destinatário; normal com expedidor e recebedor, e com tomador recebedor) passam no mesmo pacote de esquemas na suíte (tests/cte.test.ts, "o cenário ... passa no esquema oficial"), mas não foram passados um a um pelo validador do serviço. gerar_dacte com o cteProc de um CT-e de redespacho com expedidor, recebedor e documento anterior: gerou o PDF (uma página, autorizado: true). Não foi possível ler o conteúdo do PDF nesta conferência (a ferramenta devolve o arquivo em Base64, sem texto): não se sabe se o DACTE mostra o expedidor, o recebedor e o documento anterior. É item a conferir no serviço fiscal; nada foi alterado nele.

O que NÃO foi provado no redespacho e na subcontratação. Nenhum desses CT-e foi enviado à SEFAZ. Em particular: as regras que só ela aplica (se o CT-e anterior existe, não está cancelado e tem os mesmos municípios, G066 a G069; se o tomador é emitente de CT-e, G064); se o CFOP 5351/6351 é o que o contador de cada empresa quer; e o tratamento do ICMS em cada estado.

Retorno da SEFAZ, conferido contra o MOC 4.00 em 12/10/2026 (a tabela dos códigos tratados, com o item do manual de cada um, está no topo de src/lib/cte/sefaz.ts). Uma correção saiu da conferência: na consulta pela chave, o protCTe é sempre o protocolo da autorização (com cStat 100 dentro), mesmo quando o CT-e já foi cancelado; quem diz a situação atual é o cStat do topo da resposta (Visão Geral, itens 4.5.2 e 4.5.6). O sistema olhava o protocolo primeiro, e um CT-e cancelado fora dele, depois de um envio sem resposta, seria gravado como autorizado. Agora o topo decide: 101 na consulta (e 218 na recepção) fazem o CT-e ficar "rejeitado", com o número dado como usado, e nada é gravado como autorizado. Os endereços dos oito autorizadores (MT, MS, MG, PR, RS, SP, SVRS e SVSP) foram comparados de novo com as páginas do Portal do CT-e em 12/10/2026 (48 URL por ambiente): nenhuma diferença.

DACTE: o PDF é gerado pelo serviço fiscal da casa (FISCAL_MCP_URL, ferramenta gerar_dacte, o mesmo serviço do DANFE), só para CT-e autorizado, a partir do cteProc guardado, e só quando o serviço confirma o protocolo. O arquivo de homologação leva HOMOLOGACAO-SEM-VALOR-FISCAL no nome. Sem a variável o botão não aparece.

Normas conferidas (Portal do CT-e, em 10/10/2026):

Norma Data O que muda aqui
MOC CT-e 4.00, Visão Geral e Anexo I fev/2023 Leiaute, assinatura, serviços, regras do tomador, CFOP (G051), homologação (G002/G005), duplicidade (204/539), cancelamento (168 h), QR Code (item 9)
NT Conjunta 2025.001 (CNPJ alfanumérico) 25/04/2025; produção 06/07/2026 CNPJ e chave de acesso com letras; dígito verificador com ASCII menos 48
NT 2025.001 RTC v1.14b 30/04/2026 Grupo IBSCBS, vTotDFe, alíquotas de 2026 (0,1% / 0% / 0,9%), regras 014, 022 e 029
NT 2026.001 v1.01 02/03/2026 Vínculo de pagamento (pgtoVinc, eventos): facultativo, não usado
NT 2026.002 v1.01 04/08/2026 Rejeição 310 (IBS/CBS obrigatório para o regime normal): em homologação desde 01/07/2026; em produção, "implementação futura"
NT 2026.004 v1.00 01/10/2026; homologação 13/10, produção 16/11/2026 vTotDFe repete vTPrest; vTPrestLiq (facultativo, não usado); fim de EPEC e FS-DA e criação da contingência off-line, tpEmis 2 (o sistema emite em modo normal e pela SVC, tpEmis 7 e 8; a regra G008, sem dhCont/xJust na SVC, continua)
Resolução do Senado Federal 22/1989 texto do Planalto, lido em 10/10/2026 Alíquota interestadual: 12% (art. 1º) e 7% (parágrafo único, II)
Decreto 2.784/1913, art. 2º, na redação da Lei 12.876/2013 texto do Planalto, lido em 10/10/2026 Fuso de cada UF
Tabela de Classificação Tributária do IBS e da CBS (IT 2025.002 v1.70) Portal da NF-e, 01/10/2026 (lida pela ferramenta consultar_classificacao_tributaria do serviço fiscal) Os 12 pares CST × cClassTrib do CT-e e o percentual de redução de cada um
Tabela de CFOP (IT 2023.002 v2.10) Portal da NF-e, 04/09/2026 (lida pela ferramenta consultar_cfop) Os CFOP de prestação de serviço de transporte
Ajuste SINIEF 09/07 (CT-e) não lido no texto oficial (o site do CONFAZ recusou a conexão em 10/10/2026 e de novo em 12/10/2026, desta máquina e do servidor de testes) A ordem dos documentos: o CT-e autorizado antes do início da prestação. Por não ter sido lido, o sistema não afirma quando o comprovante de entrega é obrigatório nem o prazo da carta de correção: o que está implementado nos eventos e na SVC vem do MOC, dos XSD e do Portal do CT-e
Convênio SINIEF 06/89, art. 58-B (carta de correção) lido na transcrição do MOC 4.00 (Visão Geral, item 6.4) e na literal do evCCeCTe_v4.00.xsd, não no site do CONFAZ O que a carta de correção não pode corrigir
Convênios do diferencial de alíquota (EC 87/2015) não lidos (mesmo motivo) Nada foi implementado: o caso fica bloqueado

Correções da revisão contra o MOC 4.00 (norma → código → teste). Os testes estão em tests/cte.test.ts (bloco "correções da revisão contra o MOC 4.00 e as Notas Técnicas" e "da carga aos dados do CT-e"), tests/cte-rotas.test.ts e tests/cte-tela.test.tsx; os cenários de XML ficam em CENARIOS_DO_CTE (tests/cte-apoio.ts) e passam todos no esquema oficial.

Regra Norma Código Teste
ICMS devido à UF de início quando ela não é a do emitente MOC 4.00, Anexo I, campo 225 (ICMSOutraUF) e regra G051; TImp do XSD resolverIcms e icmsDoXml em src/lib/cte/montar.ts; aviso em preparar.ts "prestação que começa em outra UF (CFOP 6932)..."; cenários outra-uf-6932-mg-rj-12 e outra-uf-6932-pr-ba-7
Sem alíquota confiável (prestação inteira dentro de outra UF) ou situação 20/40/41/60 fora da UF do emitente: bloqueia mesma; a alíquota interna é de cada estado bloqueioDaAliquotaDeOutraUf, bloqueioDaSituacaoEmOutraUf "começa em outra UF sem alíquota confiável..."; "prestação que começa em outra UF: interestadual emite..."
Alíquota interna (configurada) separada da interestadual (calculada) Resolução do Senado 22/1989, art. 1º e parágrafo único aliquotaInterestadual em src/lib/cte.ts; resolverIcms "alíquota interestadual (Resolução do Senado 22/1989)..." (os 702 pares); "a alíquota da configuração é a interna..."; cenários interestadual-7-sp-ba, interestadual-12-sp-mg, interno-aliquota-interna
Tomador não contribuinte em prestação interestadual: bloqueia EC 87/2015; MOC 4.00, Anexo I, campo 236 (ICMSUFFim) BLOQUEIO_DO_DIFAL em montar.ts, aplicado em preparar.ts "cliente pagador que é o destinatário da nota..."
Responsável técnico MOC 4.00, Anexo I, regras G209 a G211 (rejeição 867, a critério da UF); TRespTec do XSD src/lib/cte/responsavel-tecnico.ts; montarCte; aviso em preparar.ts "responsável técnico: vem do cadastro da empresa..."; "infRespTec: com os dados o grupo fecha o infCte..."; "sem os dados do responsável técnico a conferência avisa..."; cenário com-responsavel-tecnico
CST × cClassTrib do IBS/CBS; o CST 400 não vale em CT-e NT 2025.001 v1.14b, regras 003, 004 e 004a (312, 313, 388); tabela IT 2025.002 v1.70 CST_DO_IBSCBS, CLASSIFICACOES_DO_IBSCBS, problemaDoIbsCbs em src/lib/cte.ts; pendência em preparar.ts; tela src/app/dashboard/empresa/fiscal.tsx "CST x cClassTrib do IBS/CBS: só os 12 pares..."; "a configuração valida o par..."; "situação 60 avisa...; configuração antiga com CST 400..."; tela "tributos: CFOP e classificação em lista..."
CST 200 com o grupo de redução de alíquota NT 2025.001 v1.14b, regras 009, 009a, 009b, 010, 017, 017b, 024 e 024b; TRed do XSD ibsCbsDaPrestacao, reducaoDoXml em montar.ts "IBS/CBS com CST 200: grupo gRed em cada tributo..."; cenários ibscbs-200-aliquota-zero e ibscbs-200-reducao-40
Fuso da UF do emitente no dhEmi e no dhEvento, com o AAMM da chave coerente MOC 4.00, Visão Geral, item 3.8; Decreto 2.784/1913 com a Lei 12.876/2013 fusoDaUf, dataHoraDoXml, anoEMes, dataHoraDoEvento em src/lib/cte/chave.ts "fuso por UF (Lei 12.876/2013)..."; "emitente em UF de UTC-4 e de UTC-5..."; cenários emitente-utc-4-mt e emitente-utc-5-ac
ICMS20 (redução de base) e ICMS60 (substituição tributária) MOC 4.00, Anexo I, campos 204 a 217; TImp do XSD resolverIcms, icmsDoXml; coluna icmsBaseReduction (prisma/sql/030-cte-correcoes.sql) "ICMS20: base reduzida..."; "ICMS60: substituição tributária..."; cenários icms-20-reducao-de-base e icms-60-substituicao
Redução de base (situação 20) na interestadual: explícita, não presumida O benefício é da legislação de cada estado ou de convênio (decisão do contador); o leiaute é o do ICMS20 BLOQUEIO_DA_REDUCAO_NA_INTERESTADUAL e resolverIcms em montar.ts; campo reducaoNaInterestadual (src/lib/cte.ts, tela fiscal.tsx); coluna icmsReductionInterstate (prisma/sql/031-cte-reducao-interestadual.sql) "ICMS20 na interestadual: só com a redução confirmada..."; "situação 20: a carga interestadual fica pendente..."; rotas "o campo nasce desligado e é gravado com auditoria..."; tela "tributos: CFOP e classificação em lista..."; cenários icms-20-reducao-de-base e icms-20-reducao-de-base-interna
CFOP da configuração só de transporte; 5932/6932 são do sistema Tabela de CFOP (IT 2023.002 v2.10); MOC 4.00, regras G051 e G052 CFOP_DE_TRANSPORTE e o esquema dadosFiscaisSchema em src/lib/cte.ts "a configuração valida o par do IBS/CBS, o CFOP de transporte e a redução da base"
CT-e antes do início da prestação Ajuste SINIEF 09/07 CARGA_QUE_RECEBE_CTE e cargaRecebeCte em src/lib/nfe.ts rotas: "carga alocada numa viagem em montagem recebe CT-e ANTES da saída..."
Saída só com o CT-e de cada carga e um MDF-e por UF de descarregamento (em produção) Ajustes SINIEF 09/07 e 21/10 (cláusula terceira, § 2º: um MDF-e por UF de descarregamento) faltasParaSair, fraseDoBloqueioDaSaida, avisoDeViagemSemMdfe em src/lib/mdfe.ts; lerSaida, documentosDaSaida, exigenciaSemMdfe em src/lib/mdfe-db.ts; rota manifestos/[id]/liberar tests/mdfe.test.ts "um MDF-e por UF de descarregamento: o de uma UF não cobre a outra"; tests/mdfe-rotas.test.ts "um MDF-e por UF de descarregamento: com duas UF e um MDF-e só, bloqueia..."; tests/saida-tela.test.tsx

Conferência no validador do serviço fiscal. Os 21 cenários de CENARIOS_DO_CTE (os 10 que já existiam e os 11 das correções) e o evento de cancelamento de um emitente em UTC-4 foram passados, à mão, em 10/10/2026, por validar_xml_fiscal (tipo: "cte", pacote PL_CTe_400_NT2026.004 RTC_1.00): 22 de 22 válidos (esquema principal, modal rodoviário e detalhe do evento; a ferramenta não aplica as regras de negócio da SEFAZ nem confere a assinatura). Os 702 pares de UF de aliquotaInterestadual foram comparados com consultar_aliquota_icms: nenhuma diferença.

Bloqueado, à espera do contador (a emissão para, com a frase na conferência):

  • prestação que começa e termina em outra UF (CFOP 5932): o ICMS é devido àquela UF pela alíquota interna de lá, que o sistema não tem de fonte confiável;
  • prestação que começa em outra UF com situação 20, 40, 41 ou 60 configurada: a redução, a isenção, a não tributação e a substituição são da legislação da UF do emitente e não se presumem na UF de início;
  • prestação interestadual com tomador não contribuinte (sem inscrição estadual no cadastro): pode haver diferencial de alíquota para a UF de término (grupo ICMSUFFim), que o sistema não calcula. Vale para todos os regimes, inclusive o Simples Nacional: as exceções (frete por conta do remetente, Simples) dependem de convênio que não foi possível ler na fonte;
  • prestação interestadual na situação 20 sem a confirmação de que a redução da base vale fora do estado: o contador confirma e o administrador marca a caixa em Empresa → Fiscal;
  • classificação 410026 do IBS/CBS: exige o grupo de estorno de crédito (gEstornoCred), que o sistema não monta (a configuração a recusa).

Decisões que o contador precisa confirmar (o sistema emite, com aviso): na situação 60 o ICMS retido por substituição tributária não é descontado da base do IBS/CBS; no ICMSOutraUF a alíquota é a interestadual da Resolução 22/1989 a partir da UF de início, e o recolhimento à outra UF fica por conta da empresa; a classificação 200050 é do transporte aéreo regional (está na tabela do CT-e, mas este sistema só emite o modal rodoviário).

Rotas

Rota Quem O que faz
GET/PUT /api/empresa/fiscal administrador Dados fiscais do emitente (a leitura traz do certificado só titular, CNPJ e validade)
PUT/DELETE /api/empresa/fiscal/certificado administrador Envia ({ arquivo, senha }, arquivo em base64) e remove o certificado A1
GET /api/fiscal/cte/situacao quem lê o fiscal A empresa está pronta para emitir? Ambiente e o que falta
GET /api/fiscal/cte/emissao?collectionId= quem lê o fiscal Conferência: o que vai no documento, pendências e avisos
POST /api/fiscal/cte/emissao quem escreve no fiscal Monta, assina e transmite; devolve o CT-e como ficou e a mensagem da SEFAZ
GET /api/fiscal/cte/emissao/[id]/xml quem lê o fiscal O cteProc autorizado, como anexo
GET /api/fiscal/cte/emissao/[id]/dacte quem lê o fiscal O DACTE em PDF do CT-e autorizado
POST /api/fiscal/cte/emissao/[id]/cancelar quem escreve no fiscal Cancela ({ justificativa })
POST /api/fiscal/cte/emissao com svc: { justificativa } quem escreve no fiscal Emite pela SVC (contingência), nas condições descritas acima
GET /api/fiscal/cte/emissao/[id]/eventos quem lê o fiscal Os eventos registrados do CT-e e a situação do comprovante de entrega (sem XML, documento nem imagem)
POST /api/fiscal/cte/emissao/[id]/correcao quem escreve no fiscal Carta de correção ({ correcoes: [{ grupo, campo, valor, item? }] })
POST /api/fiscal/cte/emissao/[id]/entrega quem escreve no fiscal Registra o comprovante de entrega na SEFAZ (sem corpo)
DELETE /api/fiscal/cte/emissao/[id]/entrega quem escreve no fiscal Cancela o comprovante de entrega em vigor
POST /api/fiscal/cte/emissao/[id]/situacao quem escreve no fiscal "Conferir na SEFAZ": consulta pela chave e atualiza a situação e os eventos
GET /api/fiscal/cte/status-servico quem lê o fiscal Pergunta à SEFAZ se o serviço está em operação

A lista (GET /api/fiscal/cte) passou a trazer, de cada carga, o CT-e dela (emitido: o autorizado que a carga aponta; senão o da SVC; senão outro autorizado; senão o mais recente), os que pedem providência (aResolver) e se há comprovante aprovado (comprovanteAceito).

Eventos para o n8n: cte.autorizado e cte.cancelado (com número, chave, ambiente, protocolo e a carga; sem o XML). Aviso no sininho para quem lê o fiscal. Auditoria: dados fiscais, certificado (sem o conteúdo), autorização, rejeição, cancelamento, carta de correção, comprovante de entrega e o cancelamento dele, envio à SVC (com a justificativa) e situação atualizada pela consulta. As tabelas são criadas por prisma/sql/027-cte.sql e prisma/sql/033-cte-eventos.sql (rode npm run db:rls depois); os esquemas oficiais usados nos testes estão em fiscal/esquemas/cte-4.00.

Ainda não existe no CT-e: a primeira autorização real; CT-e complementar, de substituição e simplificado; a contingência off-line (tpEmis 2, criada pela NT 2026.004), o EPEC e o FS-DA; a importação do XML do CT-e da subcontratada e a conferência da chave dela na SEFAZ (só a chave digitada é guardada; ver "Subcontratação contratada" em Parceiros); o serviço vinculado a multimodal (tpServ 4); mais de um CT-e anterior por carga, documento anterior em papel e a leitura do XML do CT-e anterior (só a chave digitada); a decisão do ICMS da subcontratação (diferimento ou dispensa de destaque: fica com o contador); alterar os dados de redespacho com a carga já em viagem; o evento de insucesso na entrega e os demais eventos do emitente; aviso no sininho e evento para o n8n dos eventos novos; a conferência automática (sem ninguém clicar) do envio que ficou sem resposta; a escolha, no MDF-e, entre dois CT-e autorizados da mesma carga enquanto o que sobrou não é cancelado (ele pega o de autorização mais recente); a partilha do ICMS com a UF de término (ICMSUFFim) e a alíquota interna das outras UF; o crédito outorgado (vCred), o ICMS desonerado e o código de benefício (cBenef), o diferimento (CST 51) e o pRedBC do ICMS90; diferimento, devolução, estorno de crédito, tributação regular e compras governamentais do IBS/CBS; o fuso do oeste do Amazonas e das ilhas oceânicas (a UF inteira vai no fuso da capital); cliente e destinatário frequente com CNPJ alfanumérico no cadastro (o cadastro de clientes ainda só aceita dígitos).

MDF-e

O MDF-e (modelo 58, modal rodoviário, leiaute 3.00) é emitido a partir da viagem: em Manifestos, a tela da viagem tem a aba MDF-e, com um documento para cada UF de descarregamento. Ali se confere o que vai no documento, se informa o que o cadastro não sabe (percurso, CIOT, averbação do seguro, produto predominante, pagamento do frete, vale-pedágio, reboques, lacres) e se emite. A tela /dashboard/fiscal/mdfe (botão "MDF-e" em Notas fiscais) lista os MDF-e emitidos e é de onde se baixa o XML e o DAMDFE, se encerra, se cancela, se inclui condutor, se informa o pagamento do frete por evento e se transmite o MDF-e emitido em contingência; nela ficam também a consulta ao status da SEFAZ, a consulta dos MDF-e não encerrados e, para o administrador, a configuração (série, próximo número, tipo de emitente e seguro padrão). O motorista vê, no app, a chave e a situação do MDF-e da viagem dele.

Regra da casa: nada é "autorizado", "encerrado" ou "cancelado" sem o protocolo da SEFAZ. Um MDF-e só fica autorizado com cStat 100 e número de protocolo, para a chave, o ambiente e o resumo (digVal) do XML que foi enviado; um evento só vale com cStat 135 e protocolo, para a chave e o tipo enviados.

O que NÃO foi provado. Não havia certificado A1 nem acesso à SEFAZ: nenhum MDF-e foi autorizado, encerrado ou cancelado de verdade, nem em homologação. O que existe está provado até onde dá sem a SEFAZ: o XML de cada cenário é validado contra o pacote oficial de esquemas (o MDF-e, o grupo rodo, o mdfeProc, cada evento com o seu detEvento, o procEventoMDFe e as três consultas), a assinatura é conferida de volta, e a conversa SOAP (autenticação mútua em TLS 1.2, GZip + Base64 na recepção) roda contra um servidor HTTPS local que imita a SVRS (tests/mdfe-apoio.ts). Os mesmos XML foram passados, à mão, pela ferramenta validar_xml_fiscal do serviço fiscal da casa (15 de 15 válidos na primeira rodada; depois que o serviço passou a conferir o detEvento da inclusão de DF-e, o cenário 110115 foi passado de novo, em 10/10/2026, e deu válido com evInclusaoDFeMDFe_v3.00.xsd, junto com os cenários das correções: MDF-e com e sem infRespTec, emitente em UTC-4 e em UTC-5 e eventos nesses fusos, 9 de 9 válidos) e o mdfeProc de exemplo gerou DAMDFE em gerar_damdfe. Em 12/10/2026 foram passados os XML das pendências fechadas (pacote PL_MDFe_300b_NT012025_1.05, e PL_CTe_400_NT2026.004 RTC_1.00 para o CT-e): MDF-e em contingência (homologação), MDF-e em contingência de produção com o código de segurança, MDF-e e CT-e normais com idCSRT e hashCSRT, evento de pagamento à vista, evento de pagamento a prazo com dois pagadores e evento de alteração do pagamento: 7 de 7 válidos (os eventos, com evPagtoOperMDFe_v3.00.xsd e evAlteracaoPagtoServMDFe_v3.00.xsd). Os dois MDF-e de contingência geraram DAMDFE em gerar_damdfe, com "EMISSÃO EM CONTINGÊNCIA" e o prazo de 168 horas. Da contingência, dos eventos de pagamento e do código de segurança, nada foi aceito pela SEFAZ: a assinatura do QR Code (sign), a transmissão de um MDF-e com tpEmis 2, as regras K dos eventos de pagamento e o hashCSRT (que depende de um CSRT de verdade, fornecido pelo fisco) só se provam com certificado e SEFAZ. Só com certificado e SEFAZ de verdade se prova: o aperto de mão TLS e o endereço de cada serviço, o nome do campo e a ação SOAP aceitos pelo servidor real, as regras de negócio que só a SEFAZ tem (cadastro do emitente, RNTRC e CIOT na ANTT, existência dos CT-e e das NF-e relacionados, placa no SENATRAN, seguro, MDF-e em aberto), a assinatura aceita com a cadeia da ICP-Brasil e o DAMDFE de um documento real.

Normas conferidas (em 10/10/2026; as do Portal do MDF-e foram lidas no próprio portal, https://dfe-portal.svrs.rs.gov.br/Mdfe):

Norma Versão e data O que saiu dela
MOC do MDF-e, Visão Geral 3.00b, 07/12/2022 Serviços e métodos (MDFeRecepcaoSinc, MDFeConsulta, MDFeConsNaoEnc, MDFeStatusServico, MDFeRecepcaoEvento), SOAP 1.2 sem cabeçalho, campo mdfeDadosMsg, GZip + Base64 só na recepção, assinatura RSA-SHA1 com C14N e Enveloped, eventos (itens 5 e 6, inclusive o 5.1.7 e os eventos de pagamento, 6.5 e 6.7), a situação na consulta (4.3), QR Code (itens 9.2.1 e 9.2.2) e contingência (item 11)
MOC do MDF-e, Anexo I (leiaute e regras de validação) 3.00b, 07/12/2022 Ordem dos campos e as regras F (documentos por tipo de emitente, proprietário e tpTransp, seguro, contratante, percurso, duplicidade, MDF-e não encerrado)
Pacote de esquemas PL_MDFe_300b_NT012025_1.05 25/04/2026 (item "Schemas NT 2025.001 v 1.04" do portal) Os XSD de fiscal/esquemas/mdfe-3.00/
NT 2026.001 do MDF-e v1.00, maio de 2026 (homologação 21/09/2026, produção 23/11/2026) Grupo do CIOT exigido da transportadora (rejeição 684)
NT 2025.001 do MDF-e v1.03, agosto de 2025 (produção 06/10/2025) NCM e pagamento na carga lotação (301 e 302), tpCarga 12, tpComp 04, vale-pedágio 01 e 04, CIOT sem o código, cStat de 4 dígitos, CNPJ alfanumérico nos tipos
NT Conjunta 2025.001 (CNPJ alfanumérico) v1.00, 07/05/2025 CNPJ e chave com letras
NT 2024.001 do MDF-e v1.02, 12/03/2024 Cavalo mecânico com reboque (523), fim do serviço assíncrono (30/06/2024), até 20.000 documentos por município
NT 2023.001 do MDF-e v1.00, março de 2023 Placa com 7 posições; encerramento e consulta de não encerrados sem a rejeição 203
NT 2020.001 (MDF-e Integrado), 2021.001, 2021.002 e 2022.001 v1.04 (03/2020), v1.02 e v1.03 (05/2021) e v1.00 (02/2022) Grupos infPag, prodPred e infLotacao (2020.001); Pix e categoria de combinação veicular (2021); contrato e antecipação (2022.001)
Ajuste SINIEF 21/10 e alterações (45/23, 03/26 e 05/26) texto lido em republicações (o site do CONFAZ recusou a conexão em 10/10/2026 e de novo em 12/10/2026): cláusula terceira, caput e § 2º; cláusula décima quarta, I MDF-e emitido ao fim do carregamento e antes do início do transporte; um MDF-e por UF de descarregamento; encerramento ao término do último descarregamento; CIOT no MDF-e com efeitos a partir de 01/06/2026. A cláusula décima segunda (contingência) não foi lida no texto oficial: a contingência segue o MOC (item 11), e só o que ele diz foi implementado
Ajuste SINIEF 21/10, cláusula terceira, § 6º (acrescentado pelo Ajuste 06/14) lido em republicação (LegisWeb), em 12/10/2026; o site do CONFAZ não respondeu Na subcontratação o MDF-e é só de quem gerencia o transporte: a chave do MDF-e da transportadora subcontratada, registrada no contrato de frete, cobre a UF na liberação da saída
Resolução ANTT 5.862/2019 (CIOT), alterada em 2026 (6.078 e 6.090, segundo notícias) não lida no texto oficial Nenhuma regra de código saiu dela diretamente: o sistema aplica a regra de validação que a SEFAZ publicou (NT 2026.001)

O MDF-e 3.00 não tem grupo de IBS/CBS: o pacote de 25/04/2026 e as NTs do portal não trazem nada da Reforma Tributária para este documento (as tabelas de classificação tributária publicadas no portal são do CT-e e dos outros DF-e). Não há o que conferir em consultar_classificacao_tributaria.

O que o sistema decide, e por quê

  • Tipo de emitente (tpEmit): é da configuração da empresa. 1 (transportadora) relaciona os CT-e das cargas; 2 (carga própria) relaciona as NF-e. O tipo 3 (CT-e globalizado) não é emitido.
  • Um MDF-e por UF de descarregamento, com os documentos agrupados por município de descarga. Na transportadora, município de início e de fim, valor, peso e produto saem do XML do próprio CT-e autorizado (o MDF-e precisa fechar com ele); CT-e registrado à mão, sem XML, só vale em produção e usa origem e destino da carga.
  • Percurso: mesma UF ou UFs vizinhas não levam percurso; com um único caminho mais curto pelas divisas (SP → SC passa pelo PR), o sistema o sugere; com mais de um (SP → GO, por MG ou por MS), a pessoa informa e a emissão fica pendente até lá. O percurso informado é conferido pelas divisas.
  • Seguro: obrigatório para a transportadora (seguradora, CNPJ, apólice e averbação: rejeições 698 e 699). Seguradora e apólice vêm da configuração; a averbação é de cada viagem.
  • CIOT: pendência em homologação desde 21/09/2026 e, em produção, a partir de 23/11/2026 (antes disso, aviso).
  • Carga lotação (um documento só): pede NCM, CEP de carregamento e de descarga e o pagamento do frete.
  • Veículo de terceiro: o proprietário cadastrado no veículo vira o grupo prop, o tpTransp (CPF = TAC, CNPJ = ETC) e o contratante passa a ser o próprio emitente (regra F65). Cooperativa (CTC) não é montada.
  • Antes de enviar, a emissão consulta os MDF-e não encerrados do emitente. Se um deles é desta empresa, da mesma placa, tipo de emitente e UF de descarregamento, a emissão para ali e diz qual encerrar (a SEFAZ rejeitaria com 611). Os emitidos fora do sistema aparecem na consulta da tela, e quem decide é a SEFAZ.
  • Numeração: própria do MDF-e (MdfeNumbering, por empresa, série e ambiente), com a mesma disciplina do CT-e: rejeição não consome o número; envio sem resposta guarda o XML e consulta pela chave antes de reenviar.
  • Encerramento: a norma manda encerrar ao término do último descarregamento, sem prazo em dias. Os prazos que existem são os bloqueios da SEFAZ: MDF-e em aberto há mais de 5 dias bloqueia novo MDF-e da placa (rejeição 462) e há mais de 30 dias bloqueia o emitente (686). O sininho avisa quem lê o fiscal quando um MDF-e passa de DIAS_PARA_AVISAR_ENCERRAMENTO (3) dias autorizado sem encerrar, e a tela de Manifestos oferece o encerramento ao finalizar a viagem.
  • Cancelamento: até 24 horas da autorização e sem o transporte ter começado. O sistema recusa quando a viagem foi finalizada ou já tem entrega feita, pede a confirmação de que o veículo não saiu, e a SEFAZ confere a circulação.
  • Ordem dos documentos e liberação da saída. O CT-e tem de estar autorizado antes do início da prestação (Ajuste SINIEF 09/07) e o MDF-e, antes do início do transporte (Ajuste SINIEF 21/10). A ordem no sistema é: alocar a carga na viagem → emitir o CT-e → emitir o MDF-e → liberar a saída. O CT-e é emitido com a carga já alocada numa viagem em montagem (antes ela precisava estar "em rota", e o veículo saía sem documento). Ao liberar a saída (POST /api/manifestos/[id]/liberar):
    • empresa com emitente fiscal em produção: a saída só é liberada com o CT-e de cada carga que precisa dele (transportadora, carga que sai do município; vale o autorizado por aqui ou o registrado à mão) e, quando a viagem exige MDF-e (sai do município ou do estado), com um MDF-e autorizado para cada UF de descarregamento das cargas embarcadas. As UF são as mesmas da aba MDF-e da viagem (ufsDeDescarga), comparadas com a UF de descarregamento (Mdfe.unloadState) dos MDF-e de produção autorizados e não cancelados: numa viagem de SP para MG e RJ, o MDF-e de MG não libera a saída. O de homologação e o cancelado não contam. Faltando, a resposta é 409 com faltas ({ ctes: [{ id, codigo }], mdfe: { exigencia, ufs } }, em que ufs são as UF sem MDF-e), e o cartão da viagem em Manifestos mostra o que falta ("MDF-e com descarga em RJ"), com o atalho para emitir o CT-e e o MDF-e. A empresa de carga própria (tipo de emitente 2) não emite CT-e: só o MDF-e é exigido;
    • empresa em homologação ou sem emitente configurado (emite em outro sistema): nada bloqueia; a resposta leva só o aviso, que segue a mesma conta e diz as UF de descarregamento que ficaram sem MDF-e de produção;
    • viagem no veículo de uma transportadora subcontratada (veículo de parceiro do tipo transportadora, com o contrato de frete dela valendo): cada UF de descarregamento fica coberta pelo MDF-e desta empresa ou pela chave do MDF-e dela registrada no contrato (modelo 58, dígito verificador e o CNPJ dela), porque na subcontratação o MDF-e é emitido só por quem gerencia o transporte (Ajuste SINIEF 21/10, cláusula terceira, § 6º). O CT-e desta empresa continua exigido de cada carga. Sem chave registrada a regra é a de sempre, e faltas leva também subcontratada (o nome dela), para a tela dizer que a chave dela serve. Com TAC agregado ou independente nada muda. Os detalhes e a tabela das normas estão em Parceiros e frete contratado, "Subcontratação contratada".
  • Datas com hora no fuso da UF do emitente (dhEmi, dhIniViagem e o dhEvento de todos os eventos), com o AAMM da chave no mesmo relógio: a mesma regra do CT-e (src/lib/cte/chave.ts).
  • Responsável técnico (infRespTec, depois de infAdic): o mesmo critério do CT-e (cadastro da empresa em Empresa → Fiscal; sem contato, e-mail ou telefone o grupo não é montado e a conferência avisa: a UF pode rejeitar com 720, regra F120 do MOC).
  • Responsável técnico com código de segurança: o mesmo do CT-e (RESPTEC_CSRT_ID e RESPTEC_CSRT): idCSRT e hashCSRT fecham o grupo, com o hash da chave do próprio MDF-e.
  • Homologação e produção: o ambiente é o mesmo do CT-e (Empresa → Fiscal). MDF-e de homologação fica só na tabela do documento: não conta como MDF-e da viagem.

Contingência off-line (tpEmis 2; MOC do MDF-e 3.00b, Visão Geral, itens 9.2.2 e 11)

Para quando não se consegue a autorização em tempo real. O manual a trata como exceção ("a decisão pela entrada em contingência é exclusiva do contribuinte", sem autorização prévia do fisco, que pode restringir quem a usa em demasia).

  • Nunca é automática. Na aba MDF-e da viagem a opção só aparece depois de uma emissão que ficou sem resposta ou encontrou o serviço paralisado (108, 109), e pede a confirmação "não consigo a autorização da SEFAZ agora". No servidor (POST /api/fiscal/mdfe/emissao com contingencia: true), ela é recusada (409) quando a SEFAZ responde que o serviço está em operação e a viagem não tem um envio normal sem resposta.
  • O que muda no documento (item 11.1 e 11.2): tpEmis 2; chave de acesso nova, com o tipo de emissão na posição 35 e o mesmo cMDF; QR Code com o parâmetro sign, a chave assinada (RSA-SHA1, Base64) com o certificado que assina o MDF-e (regras F117 e F119 do Anexo I, rejeições 482 e 496). O número é sempre novo: o manual recomenda avançar a numeração ao entrar em contingência.
  • Nada é transmitido na emissão. O MDF-e fica na situação própria CONTINGENCY ("emitido em contingência, a transmitir"): assinado, sem protocolo, e portanto não "autorizado". A tela mostra o prazo (168 horas contadas da emissão, item 11.1) e o botão Transmitir à SEFAZ; passado o prazo, a frase muda e manda consultar o contador (o sistema não apaga nem bloqueia o documento).
  • Transmitir (POST /api/fiscal/mdfe/emissao/[id]/transmitir): manda o mesmo XML, com a mesma chave ("deve-se manter a mesma chave de acesso, inclusive com a manutenção do mesmo código numérico original"). Só então, com o protocolo, o MDF-e fica autorizado. Rejeitado, parado ou sem resposta, continua em contingência, com o código e o motivo; a retomada de uma transmissão sem resposta consulta pela chave antes de reenviar. Se a contingência veio depois de um envio normal sem resposta, a transmissão primeiro consulta aquele envio pela chave: se a SEFAZ não o tem (217), ele vira rejeitado; se ela o tinha autorizado, ele é gravado com o protocolo e a transmissão para, avisando que a viagem ficou com dois MDF-e para a UF (a SEFAZ rejeitaria o segundo: 611).
  • DAMDFE de contingência: sai pelo serviço fiscal (gerar_damdfe) a partir do XML assinado, sem protocolo. O serviço imprime "EMISSÃO EM CONTINGÊNCIA" e o prazo de 168 horas a partir do tpEmis do XML (conferido à mão, em 12/10/2026, com um XML de exemplo). O sistema só entrega esse PDF para MDF-e na situação de contingência, com chave de contingência, e quando o serviço confirma que leu aquela chave; o arquivo leva CONTINGENCIA no nome. Para qualquer outro MDF-e a regra continua: sem protocolo reconhecido não há DAMDFE. O serviço fiscal carimbava "SEM VALOR FISCAL" também no MDF-e de contingência de produção (tpAmb 1 e tpEmis 2), onde o DAMDFE é o documento que acompanha a carga; a equipe do serviço informou a correção em 12/10/2026 (commit bfa1b33 do serviço: o carimbo fica só em homologação e na emissão normal sem protocolo). Não foi reconferido deste lado: conferir o PDF no primeiro uso real da contingência em produção.
  • Liberação da saída em produção: o MDF-e em contingência cobre a UF dele, como o autorizado. Fonte: o item 11 do MOC, que descreve a contingência como "emiti-lo em contingência Off-line, imprimir o DAMDFE e depois de superado o problema técnico, transmitir o arquivo XML do MDFe para autorização" e avisa que a rejeição posterior acontece quando "a carga já está em circulação no trânsito". O de homologação continua sem contar.
  • O que não foi feito na contingência: refazer o MDF-e de contingência que a SEFAZ rejeitou na transmissão por um motivo do próprio documento (a providência do Ajuste SINIEF 21/10, cláusula décima segunda, não foi lida no texto oficial; fica com o contador, e a tela diz isso); descartar um MDF-e de contingência que não vai ser usado (a viagem não saiu); aviso no sininho quando o prazo está acabando (o aviso é o da tela); e o Regime Especial da Nota Fiscal Fácil (tpEmis 3).

Pagamento do frete por evento (MOC do MDF-e 3.00b, Visão Geral, itens 6.5 e 6.7; NTs 2020.001 e 2022.001)

  • Pagamento da operação de transporte (110116): informa, depois da emissão, o pagamento do TAC agregado "a ocorrer no final do período conforme a relação de viagens realizadas", com o mesmo grupo infPag do MDF-e mais o total de viagens e o número desta. Quando é exigido: só para o MDF-e cujo veículo de tração tem proprietário TAC agregado (tpProp 0: regra K06 do evento, rejeição 723) e que não levou o pagamento na emissão. Quem informa o infPag no próprio MDF-e não precisa dele. Um por MDF-e (sequência 1, regra K01). A tela diz isso em uma linha, e o sistema recusa, sem enviar, o MDF-e cujo veículo não é de TAC agregado.
  • Alteração do pagamento do serviço de transporte (110118): troca os dados do pagamento "em relação a um contratante nos casos em que for necessário"; até 99 por MDF-e. Na tela, a mesma ação "Pagamento" vira alteração depois que o pagamento foi informado.
  • Os dois valem para o MDF-e autorizado ou já encerrado (as regras do evento só barram o cancelado: 218), só para quem tem a capacidade fiscal, e só existem para o sistema quando a SEFAZ registra o evento (135, ou 134 com o evento vinculado). Ficam em MdfeEvent, com o XML assinado e o retorno, e na auditoria (mdfe.pagamento).
  • Não feito: mais de um pagador (infPag) ou mais de uma parcela pela tela (a rota aceita as parcelas); indAntecipaAdiant e tpAntecip; o evento de confirmação do serviço (110117), que é do contratante.

Retorno da SEFAZ e endereços, conferidos contra o MOC 3.00b em 12/10/2026

A tabela dos códigos tratados, com o item do manual de cada um, está no topo de src/lib/mdfe/sefaz.ts. O que a conferência achou e corrigiu:

  • Consulta de MDF-e cancelado ou encerrado. A consulta devolve a situação atual no cStat do topo (100, 101 ou 132) e, junto, o protMDFe da autorização, que tem cStat 100 dentro (Visão Geral, itens 4.3.2 e 4.3.6). O sistema olhava o protocolo primeiro: um MDF-e cancelado ou encerrado fora dele, depois de um envio sem resposta, seria gravado como autorizado. Agora o topo decide (decidirMdfe): 101 e 132 na consulta, e 218 e 609 na recepção (Anexo I, regras F83 e F84), deixam o MDF-e "rejeitado", com o número dado como usado e a frase que diz o que a SEFAZ informou. Antes, o 132 deixava a emissão presa em "sem resposta" para sempre.
  • Evento guardado com ressalva (134 e 136). O item 5.1.7 diz que nos dois casos o evento foi guardado: 134 vinculado ao MDF-e, com alerta de que ele não está "autorizado"; 136 sem vínculo. O sistema respondia "a SEFAZ recusou". Agora: encerramento e cancelamento só valem se a consulta pela chave confirmar (132 ou 101); inclusão de condutor e pagamento valem com o 134 (o evento está no MDF-e) e não com o 136; e, sem confirmação, a frase diz que o evento ficou guardado lá, com o protocolo.
  • Sem mudança: 100 com protocolo, 204 e 539 (duplicidade), 108 e 109 (serviço paralisado), 111 e 112 (não encerrados), 217, 462, 611, 662 e 686. Os códigos de lote (103, 104, 105) eram do serviço assíncrono, desligado em 30/06/2024, e não são tratados.
  • Endereços: os cinco serviços da SVRS (o autorizador nacional do MDF-e, para todas as UF), nos dois ambientes, e o QR Code foram comparados de novo com a página "Relação de Serviços Web" do Portal do MDF-e: nenhuma diferença. Nome e método de cada serviço e o espaço de nomes são os do manual (itens 3.4, 4.2 a 4.5 e 5.1). Não conferido: o WSDL (a SVRS responde 403 a quem pede ?wsdl sem certificado) e, por isso, a ação SOAP, que o manual não escreve.

Pendências fechadas em 12/10/2026 (norma → código → teste). Os testes puros estão em tests/mdfe.test.ts (blocos "revisão do retorno da SEFAZ e dos endereços do MDF-e", "código de segurança do responsável técnico", "contingência off-line do MDF-e" e "eventos de pagamento do MDF-e"); os de rota, em tests/mdfe-rotas.test.ts e tests/cte-rotas.test.ts; os de tela, em tests/mdfe-tela.test.tsx.

Regra Norma Código Teste
A situação atual é a do topo da consulta; cancelado ou encerrado não é gravado como autorizado MOC MDF-e 3.00b, Visão Geral, 4.3.2 e 4.3.6; Anexo I, F83 e F84. MOC CT-e 4.00, Visão Geral, 4.5.2 e 4.5.6; Anexo I, G159 decidir, FINALIZADOS_DO_CTE em src/lib/cte/sefaz.ts; decidirMdfe, FINALIZADOS_DO_MDFE em src/lib/mdfe/sefaz.ts; gravarFinalizado em src/lib/mdfe-db.ts; emitirCte em src/lib/cte-db.ts "consulta de MDF-e cancelado (101) ou encerrado (132)..."; "recepção de MDF-e que a SEFAZ já tem cancelado (218) ou encerrado (609)..."; rotas "envio sem resposta de MDF-e que a SEFAZ já tem cancelado (101) ou encerrado (132)...", "a recepção diz que o MDF-e da chave já está cancelado (218)..." e, no CT-e, "sem resposta de CT-e que a SEFAZ já tem CANCELADO (101)..."
134 e 136 são evento guardado com ressalva, não recusa MOC MDF-e 3.00b e CT-e 4.00, Visão Geral, 5.1.7 eventoGuardadoComRessalva, fraseDoEventoComRessalva em src/lib/cte/sefaz.ts; enviarERegistrar, vinculadoComAlerta em src/lib/mdfe-db.ts; cancelarCte "evento: 135 registra; 134 e 136 são evento guardado com ressalva..."; rotas "evento guardado com ressalva (134 e 136)..." (MDF-e e CT-e)
Endereços e métodos dos serviços do MDF-e Portal do MDF-e, "Relação de Serviços Web" (12/10/2026); MOC 3.00b, Visão Geral, 3.4, 4.2 a 4.5 e 5.1 src/lib/mdfe/enderecos.ts "endereços: os cinco serviços da SVRS, nos dois ambientes, e o QR Code..."; "os códigos tratados são os do manual"
hashCSRT = Base64(SHA-1(CSRT + chave de acesso)); idCSRT com 3 dígitos MOC CT-e 4.00, Anexo I, campos 393 e 394 e regra G211 (868, facultativa, "implementação futura"); MOC MDF-e 3.00b, Anexo I, campos 169 e 170 e regra F122 (721, idem); TRespTec dos dois pacotes codigoDeSeguranca, hashDoCsrt, responsavelTecnicoDoXml em src/lib/cte/responsavel-tecnico.ts; montarCte; montarMdfe "a conta do MOC: Base64 do SHA-1..."; "as duas variáveis valem juntas..."; "o segredo não sai..."; "MDF-e com o código de segurança..."; "CT-e com o código de segurança..."
Contingência: tpEmis 2 na chave e no XML, mesmo cMDF, QR Code com sign MOC MDF-e 3.00b, Visão Geral, 9.2.2, 11.1 e 11.2; Anexo I, campo 16 e regras F117 a F119 chaveDoMdfe (tipoDeEmissao) em src/lib/mdfe/chave.ts; montarMdfe (contingencia), urlDoQrCode; assinarTexto em src/lib/cte/assinar.ts "a chave leva o tipo de emissão 2 na posição 35..."; "o XML: tpEmis 2, Id e cDV da chave de contingência, QR Code com o sign..."; "emissão normal continua com tpEmis 1 e sem o sign"
Contingência só a pedido, sem transmitir, numa situação própria, sem protocolo; 168 horas para transmitir o mesmo XML MOC MDF-e 3.00b, Visão Geral, 11 e 11.1 emitirEmContingencia, prepararEnvio, transmitirContingencia, resolverEnvioSemResposta em src/lib/mdfe-db.ts; PRAZO_DA_CONTINGENCIA_HORAS, prazoDaContingencia, avisoDoPrazoDaContingencia em src/lib/mdfe.ts; rotas emissao e emissao/[id]/transmitir; tela painel.tsx "o prazo de 168 horas, o selo e o aviso"; rotas do bloco "contingência off-line" (cinco testes); tela "contingência: só é oferecida depois de a SEFAZ não responder..." e "MDF-e em contingência na lista..."
DAMDFE de contingência a partir do XML sem protocolo, só para MDF-e em contingência MOC MDF-e 3.00b, Visão Geral, 11.1 ("EMISSÃO EM CONTINGÊNCIA") gerarDamdfe (contingencia) em src/lib/fiscal-mcp.ts; damdfeDoMdfe rotas "produção com a SEFAZ fora do ar: o MDF-e emitido em contingência cobre a UF dele, o DAMDFE de contingência sai do XML assinado..."
A saída em produção aceita o MDF-e em contingência MOC MDF-e 3.00b, Visão Geral, 11 COBRE_A_VIAGEM, lerSaida em src/lib/mdfe-db.ts o mesmo teste de rota; "para sair, o MDF-e em contingência cobre a UF dele como o autorizado"
Evento de pagamento da operação (110116) e de alteração do pagamento (110118) MOC MDF-e 3.00b, Visão Geral, 6.5 e 6.7 (regras K01, K04 e K06); NT 2020.001 e NT 2022.001; evPagtoOperMDFe_v3.00.xsd e evAlteracaoPagtoServMDFe_v3.00.xsd montarPagamentoDaOperacao, montarAlteracaoDoPagamento em src/lib/mdfe/montar.ts; informarPagamento, alterarPagamento em src/lib/mdfe-db.ts; pagamentoDaOperacaoSchema, alteracaoDoPagamentoSchema, QUANDO_INFORMAR_O_PAGAMENTO em src/lib/mdfe.ts; rota emissao/[id]/pagamento; tela painel.tsx os cinco testes do bloco "eventos de pagamento do MDF-e"; rota "pagamento da operação (110116): só para veículo de TAC agregado..."; tela "pagamento: a janela diz em uma linha quando o evento se aplica..."

Passo a passo para a transportadora

  1. Fazer o passo a passo do CT-e (credenciamento, Empresa → Fiscal, certificado A1). O MDF-e usa o mesmo emitente e o mesmo certificado, e é autorizado pelo ambiente nacional (SVRS) para qualquer UF.
  2. Em Notas fiscais → MDF-e → Configuração: série, próximo número, tipo de emitente e o seguro padrão (seguradora, CNPJ e apólice).
  3. Em Frota, completar o veículo em "Dados para o MDF-e": RENAVAM, tara, tipo de rodado, carroceria, UF e, se for de terceiro, o proprietário (com o RNTRC dele).
  4. Montar a viagem em Manifestos e emitir o CT-e das cargas dela (tela de CT-e), com a viagem ainda em montagem.
  5. Em Manifestos → viagem → MDF-e: informar CIOT e averbação, conferir e "Emitir em homologação". Corrigir o que a SEFAZ rejeitar.
  6. Liberar a saída. Em produção ela só é liberada com o CT-e de cada carga e o MDF-e de cada UF de descarregamento autorizados.
  7. Ao fim da viagem, encerrar o MDF-e (data e município em que terminou).
Rota Quem O que faz
GET /api/fiscal/mdfe quem lê o fiscal Os MDF-e emitidos (?manifestId= para os de uma viagem)
GET /api/fiscal/mdfe/situacao quem lê o fiscal A empresa está pronta para emitir? Ambiente, tipo de emitente e o que falta
GET/PUT /api/fiscal/mdfe/configuracao administrador Série, próximo número, tipo de emitente e seguro padrão
GET /api/fiscal/mdfe/emissao?manifestId= quem lê o fiscal A aba da viagem: uma conferência por UF de descarregamento
POST /api/fiscal/mdfe/conferencia quem lê o fiscal Confere o documento com o que a pessoa informou; nada é enviado nem gravado
GET /api/manifestos/[id]/frete/sugestao-mdfe quem lê o frete contratado O que o contrato de frete sugere para o pagamento do frete (valor, adiantamento, forma, Pix, pagador); só sugestão
GET /api/manifestos/[id]/subcontratacao quem lê o fiscal A transportadora subcontratada, o CT-e dela por carga, o MDF-e dela por UF e o que falta; sem valores
POST /api/manifestos/[id]/subcontratacao e DELETE .../[documentoId] quem escreve no fiscal Registra e exclui a chave de um CT-e ou MDF-e da subcontratada; nada vai à SEFAZ
POST /api/fiscal/mdfe/emissao quem escreve no fiscal Consulta os não encerrados, monta, assina e transmite. Com contingencia: true, emite em contingência off-line, sem transmitir (409 se a SEFAZ está em operação)
POST /api/fiscal/mdfe/emissao/[id]/transmitir quem escreve no fiscal Transmite o MDF-e emitido em contingência (o mesmo XML)
GET /api/fiscal/mdfe/emissao/[id]/xml quem lê o fiscal O mdfeProc autorizado, como anexo
GET /api/fiscal/mdfe/emissao/[id]/damdfe quem lê o fiscal O DAMDFE em PDF (serviço fiscal, gerar_damdfe; só com o protocolo reconhecido, ou o de contingência enquanto o MDF-e não foi transmitido)
POST/PUT /api/fiscal/mdfe/emissao/[id]/pagamento quem escreve no fiscal Informa o pagamento da operação ({ viagens: { quantidade, numero }, pagamento }, evento 110116) e altera o pagamento ({ pagamento }, evento 110118)
POST /api/fiscal/mdfe/emissao/[id]/encerrar quem escreve no fiscal Encerra ({ dia, cidade, uf })
POST /api/fiscal/mdfe/emissao/[id]/cancelar quem escreve no fiscal Cancela ({ justificativa, transporteNaoIniciado: true })
POST /api/fiscal/mdfe/emissao/[id]/condutor quem escreve no fiscal Inclui condutor ({ nome, cpf })
GET /api/fiscal/mdfe/status-servico quem lê o fiscal Pergunta à SVRS se o serviço está em operação
GET /api/fiscal/mdfe/nao-encerrados quem lê o fiscal Os MDF-e do emitente que a SEFAZ tem em aberto

Eventos para o n8n: mdfe.autorizado, mdfe.encerrado e mdfe.cancelado (número, chave, ambiente, protocolos, placa, UFs e a viagem; sem o XML). Avisos no sininho para quem lê o fiscal: os três e mdfe.encerrar (em aberto há dias). Auditoria: configuração, emissão, emissão em contingência, rejeição, encerramento, cancelamento, inclusão de condutor e pagamento. A contingência não mudou o banco: a situação CONTINGENCY é um valor novo da coluna de texto Mdfe.status, e o tipo de emissão se lê da chave. Tabelas: Mdfe, MdfeEvent (o XML assinado e o retorno de cada evento registrado) e MdfeNumbering (prisma/sql/029-mdfe.sql).

Ainda não existe no MDF-e: a primeira autorização real; a inclusão de DF-e pela tela (o evento 110115 é montado e validado no esquema, mas a norma só o aceita em MDF-e de carga própria emitido com "carregamento posterior", que o sistema não emite); encerrar pela tela um MDF-e emitido em outro sistema; na contingência, refazer o documento rejeitado na transmissão, descartar o que não foi usado e o aviso de prazo no sininho; nos eventos de pagamento, mais de um pagador pela tela e o evento de confirmação do serviço (110117, do contratante); emitente pessoa física; emitente com CNPJ alfanumérico (a chave e os campos já aceitam, mas o atributo Id do pacote de esquemas vigente ainda é só de dígitos: ver fiscal/esquemas/mdfe-3.00/README.md); CT-e globalizado; produtos perigosos; mais de um CIOT, de um dispositivo de vale-pedágio ou de uma parcela pela tela (a rota aceita); cooperativa (CTC) como proprietária; os outros modais.

Ainda não existe nas notas: consulta à SEFAZ (situação da nota, download pela chave), manifestação do destinatário, leitura de XML de CT-e ou de NFC-e, importação por e-mail ou em arquivo compactado, mais de uma NF-e criando uma carga só, desfazer a ligação entre nota e carga e apagar nota importada. Anexar a nota a uma carga não muda o valor da NF nem o frete dela.

Empresa

Em /dashboard/empresa, só para o administrador: o nome e o símbolo que aparecem no topo do painel, do portal do cliente e do app do motorista (GET e PATCH /api/empresa). O símbolo é uma imagem PNG, JPEG ou WebP, reduzida no navegador para 192 pixels antes de enviar e guardada no cadastro da empresa (Tenant.logo); sem símbolo, aparece o caminhão. As regras ficam em src/lib/empresa.ts.

Na mesma tela, a seção Cobrança guarda a multa (% do valor) e os juros (% ao mês) que a baixa de um título vencido sugere (GET e PATCH /api/empresa/cobranca, só administrador; padrão de 2% e 1%, colunas Tenant.lateFinePct e Tenant.lateInterestPct). A mesma seção e a mesma rota guardam o recebimento por Pix: tipo da chave, chave, nome do recebedor e cidade (colunas Tenant.pixKeyType, pixKey, pixName e pixCity; no corpo, pix com tipo, chave, nome e cidade, null para remover, ausente para não mexer). Ver "Cobrança por Pix".

Na aba Identidade fica também o perfil do comprovante de entrega (LIVRE, ECOMMERCE ou B2B; GET e PATCH /api/empresa/comprovantes, coluna Tenant.podProfile): ver Comprovante de entrega.

A aplicação só lê a tabela de empresas; a gravação vai pelo dono do banco, presa ao id da empresa da sessão. O portal do cliente e o app do motorista mostram o mesmo nome e símbolo (a leitura é liberada a todo perfil da empresa).

Integração (eventos para n8n e outros sistemas)

Em /dashboard/empresa, o administrador cadastra um endereço (um Webhook do n8n, por exemplo). A partir daí, cada troca de status de carga (inclusive a criação), cada fatura emitida, paga, reaberta ou cancelada, cada título que vence e cada chamado aberto ou com status trocado é avisado nesse endereço por POST, em até 15 segundos. Empresa sem endereço não gera evento.

{
  "id": "…",
  "tipo": "coleta.status",
  "criadoEm": "2026-10-09T18:00:00.000Z",
  "empresa": { "id": "…", "slug": "mello", "nome": "Mello Transportes" },
  "dados": {
    "de": "ROUTE",
    "para": "DELIVERED",
    "coleta": {
      "id": "…", "status": "DELIVERED", "remetente": "…", "destinatario": "…", "origem": "…", "destino": "…",
      "volumes": 3, "peso": 63.5, "frete": 180,
      "rastreio": { "codigo": "1234567890", "link": "https://tms.avilaops.com/rastreio?cnpj=…&codigo=…" },
      "cliente": { "id": "…", "nome": "…", "cnpj": "…", "contato": "…", "email": "…", "telefone": "…" },
      "motorista": { "id": "…", "nome": "…", "telefone": "…" }
    }
  }
}
  • Cabeçalhos: X-TMS-Evento (o tipo), X-TMS-Entrega (o id, para o destino descartar repetição) e X-TMS-Assinatura (sha256= + HMAC-SHA256 do corpo com o segredo). O segredo aparece uma vez, ao cadastrar ou trocar.
  • Garantia de entrega: o evento é gravado na mesma transação da mudança (tabela OutboxEvent): os de carga e de fatura por gatilho do banco (prisma/sql/010-rls.sql), os de chamado pela própria rota (src/lib/ocorrencias-db.ts). Resposta fora de 2xx ou sem resposta em 8 segundos é tentada de novo em 1, 2, 4… minutos, até 8 vezes. O mesmo evento pode chegar mais de uma vez; use o id.
  • Endereço: só https e só endereço público. O servidor recusa IP interno ao salvar e de novo a cada entrega (src/lib/url-publica.ts).
  • Quem entrega: o próprio servidor, a cada 15 segundos (src/instrumentation.ts, src/lib/eventos.ts). TMS_EVENTOS=off desliga.
  • Rotas: GET/PUT /api/empresa/webhook e POST /api/empresa/webhook/teste, só para o administrador.

Tipos de aviso (o tipo do corpo e o cabeçalho X-TMS-Evento):

Tipo Quando dados
coleta.status Carga criada ou com status trocado de, para, coleta
fatura.emitida, fatura.paga, fatura.reaberta, fatura.cancelada Fatura criada ou com status trocado fatura (número, total, vencimento, cliente, link do portal; em fatura.emitida, pixCopiaECola)
cobranca.vencida Título a receber venceu e segue em aberto titulo (valor, vencimento, dias de atraso, cliente ou pagador, fatura, pixCopiaECola)
ocorrencia.aberta, ocorrencia.status Chamado aberto (painel, portal ou motorista) ou com status trocado ocorrencia (número, tipo, título, status, prioridade, quem abriu, cliente, carga com código e link de rastreio, link do painel)
teste Botão "Enviar teste" mensagem fixa

Nos avisos de chamado, status é o de agora (lido na entrega, como nos de fatura), tipo é DELAY, DAMAGE, LOSS, BILLING, REDELIVERY ou OTHER, e abertaPor é CLIENT (portal) ou STAFF (equipe ou motorista). No chamado do motorista, cliente é o dono da carga. A descrição e a conversa não saem no aviso. Trocar prioridade ou responsável e escrever mensagem não avisam.

O campo pixCopiaECola (texto do Pix Copia e Cola estático, com o valor do título) só vai quando a empresa tem chave Pix cadastrada e a fatura ou o título segue em aberto na hora da entrega; sem chave, os avisos saem sem o campo. O destino pode mandá-lo ao cliente, mas pagar não dá baixa no TMS: a baixa é manual. Em fatura.emitida, quando a fatura já tem cobrança do Mercado Pago em aberto, pixCopiaECola é o do Pix dinâmico (pagar dá baixa sozinho) e vem junto cobranca, com pix (copiaECola, link, venceEm) e boleto (link, linhaDigitavel, venceEm): Cobrança pelo Mercado Pago.

O aviso de título vencido sai uma vez por título e por vencimento; a procura roda a cada 10 minutos. Ao cadastrar o endereço, os títulos que já estavam vencidos são avisados nessa primeira procura.

Exemplo de destino: n8n/avisos-por-whatsapp.js é o fluxo do n8n em uso na Mello, que transforma cada aviso num resumo de WhatsApp para o responsável: status de carga, fatura (na emissão, com o Pix copia e cola quando há chave cadastrada), título vencido (também com o Pix) e chamado aberto ou com status trocado. O arquivo é a referência; o fluxo publicado no n8n é atualizado à parte.

Ainda não há escolha de quais tipos receber, nem repetição do aviso de título vencido (o lembrete periódico fica por conta do fluxo no destino).

Mensageria

Em /dashboard/mensagens (menu Sistema, só ADMIN) fica o histórico dos avisos acima: o que o TMS gerou para o endereço da Integração, do mais novo para o mais antigo. Cada linha traz o tipo com rótulo em português, quando foi gerado e a situação: entregue, na fila, falhou (com o motivo e a próxima tentativa) ou desistiu (gastou as 8 tentativas). Filtros por tipo e por situação, e uma página de 30 por vez. As regras ficam em src/lib/mensageria.ts.

  • Tentar de novo: no aviso que falhou ou de que o despachante desistiu, o botão zera as tentativas e marca a próxima para agora; ele sai na volta seguinte do despachante (até 15 segundos). Aviso entregue ou ainda na fila não é reenviado.
  • Sem endereço cadastrado a tela diz isso e aponta para Empresa → Integração: sem endereço o TMS não gera aviso nenhum.
  • A tela mostra se o aviso chegou ao endereço, não se alguém leu uma mensagem. O TMS não manda WhatsApp, SMS nem e-mail por conta própria: quem escreve para a pessoa é o sistema que recebe o aviso (o fluxo do n8n, por exemplo).
Rota Quem O que faz
GET /api/eventos?tipo=&situacao=&cursor= administrador Avisos da empresa e o endereço da integração; proximo é o cursor da página seguinte
POST /api/eventos/[id]/reenviar administrador Devolve à fila um aviso que falhou; 409 se entregue, na fila ou sem endereço cadastrado

Ainda não existe: ver o conteúdo que foi enviado em cada aviso, reenviar vários de uma vez e apagar aviso antigo. O aviso direto do TMS para as pessoas (equipe, cliente e motorista) é o das Notificações, logo abaixo; ele não aparece nesta tela.

Notificações (sininho e push)

Além de avisar sistemas de fora (Integração, acima), o TMS avisa as pessoas dentro dele: um sininho com a lista de avisos no painel (cabeçalho do celular e do computador), no portal do cliente e no app do motorista, e notificação push no navegador para quem ativar. As regras e os textos ficam em src/lib/notificacoes.ts; o envio por push, em src/lib/notificacoes-push.ts; a tela, em src/components/notificacoes/Sininho.tsx.

Quem é avisado de quê. Cada aviso é de uma pessoa, nasce na mesma transação da ação que o causou e nunca vai para quem fez a ação.

Quem recebe Quando Abre
Motorista A viagem dele foi liberada A viagem no app
Motorista Uma carga foi retirada da viagem que ele já está fazendo A viagem no app
Usuários do portal do cliente dono da carga Pedido de coleta confirmado ou recusado; carga saiu para entrega; carga entregue A carga no portal
Usuários do portal do cliente Fatura emitida (número, quantidade de cargas e vencimento; o valor fica na tela de faturas) Faturas
Usuários do portal do cliente dono do chamado A transportadora respondeu no atendimento (nota interna não avisa) O atendimento
Equipe com coletas Pedido de coleta novo pelo portal Coletas pendentes
Equipe com ocorrencias Chamado novo aberto pelo cliente (portal) ou pelo motorista O chamado
Responsável do chamado (sem responsável: equipe com ocorrencias) O cliente respondeu no chamado O chamado
Equipe com comprovantes O motorista deu baixa e mandou o comprovante O comprovante
Equipe com comprovantes O motorista mandou fotos novas de um comprovante devolvido O comprovante
Equipe com ocorrencias A entrega foi feita com ressalva (só o tipo; a descrição fica no comprovante) O comprovante
Motorista da viagem O comprovante dele foi devolvido para refazer (com o motivo) A tela de refazer no app
Equipe com financeiro O motorista lançou despesa na viagem Manifestos

"Equipe com…" são os usuários cujo perfil tem a capacidade na matriz. O texto do aviso para o cliente só leva o que é dele (destinatário, destino e código de rastreio da carga dele; número e vencimento da fatura; número e título do chamado): nunca valor de frete, nome de outro cliente, nota interna nem o texto da mensagem.

Sininho. Mostra o número de não lidos e, ao tocar, os últimos 20 avisos (título, texto, há quanto tempo), com "Ver avisos mais antigos". Tocar num aviso abre a tela dele e marca como lido; "Marcar tudo como lido" marca todos. Consulta o servidor ao abrir a tela, ao abrir a lista e a cada 60 segundos (não há websocket). A lista cabe numa tela de celular: só ela rola.

Push no navegador (Web Push). Dentro do sininho, "Ativar notificações neste aparelho" pede a permissão do navegador (só depois do toque) e inscreve o aparelho; "Desligar" desfaz, só naquele aparelho. A preferência é por pessoa e por aparelho; não há escolha por tipo de aviso. O despachante do servidor (o mesmo dos eventos, a cada 15 segundos) manda por push os avisos ainda não enviados. É uma tentativa por aviso e por aparelho: se o serviço de push recusar, o aviso segue no sininho e não é repetido; resposta 404 ou 410 (inscrição que não existe mais) apaga a inscrição. Aviso já lido, ou com mais de uma hora (servidor parado), não toca o celular. Tocar na notificação foca a aba que já está naquela tela, ou abre uma, e marca o aviso como lido.

  • Um aparelho, uma pessoa. O endereço da inscrição é do navegador: quem entrar nele depois e ativar leva a inscrição (na mesma empresa ou em outra). Ao sair do sistema, a inscrição é apagada do servidor, e volta sozinha quando a mesma pessoa entrar de novo naquele aparelho.
  • Service worker. É o mesmo arquivo (public/sw.js) com três registros possíveis. O da raiz é o do app do motorista e continua cuidando do modo offline. Os do painel (/dashboard) e do portal (/portal) só são criados quando a pessoa ativa as notificações, recebem push e mais nada: não interceptam requisição nem guardam página.
  • Endereços aceitos. A inscrição só é aceita se o endereço for de um serviço de push de navegador conhecido (Chrome e derivados, Firefox, Safari, Edge), por HTTPS: o servidor faz um POST nesse endereço.

Variáveis. VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY e VAPID_SUBJECT (contato de quem opera, mailto: ou https://). Gere o par uma vez com npx web-push generate-vapid-keys e guarde: trocar as chaves desliga o push de todos os aparelhos já inscritos (cada pessoa precisa ativar de novo). Sem as três, o push fica desligado e nada quebra: o botão de ativar não aparece e só o sininho funciona.

iPhone e iPad. O Safari só entrega push para o sistema instalado na Tela de Início (iOS 16.4 ou mais novo): Compartilhar → "Adicionar à Tela de Início", e abrir por lá. Aberto no Safari comum, o sininho explica isso em vez de mostrar o botão. Para a instalação funcionar, o painel e o portal têm manifesto próprio (public/painel.webmanifest e public/portal.webmanifest), como o app do motorista já tinha. O número no ícone do app (o selo) não é atualizado: o contador fica no sininho.

Rota Quem O que faz
GET /api/notificacoes?cursor= qualquer usuário logado Os avisos da própria pessoa, 20 por página, com naoLidos e proximo
POST /api/notificacoes/lidas qualquer usuário logado Marca como lido: { "ids": [...] } ou { "todas": true }; só os próprios
GET /api/notificacoes/chave qualquer usuário logado { ativo, chave }: a chave pública do push, ou ativo: false com o push desligado
POST /api/notificacoes/aparelho qualquer usuário logado Inscreve o aparelho (corpo: a inscrição que o navegador entrega); 409 com o push desligado
DELETE /api/notificacoes/aparelho qualquer usuário logado Desinscreve o aparelho ({ "endpoint": "..." }); só a própria inscrição

Essas rotas valem para qualquer perfil (como GET /api/empresa) e respondem sempre só com o que é de quem está logado: ninguém lê, marca nem recebe aviso de outra pessoa, nem o administrador. A leitura de aviso não entra na auditoria. As tabelas são criadas por prisma/sql/023-notificacoes.sql; rode npm run db:rls depois dela.

Ainda não existe: SMS, e-mail, preferência por tipo de aviso, push nativo de loja (App Store e Google Play), apagar aviso, limpeza automática de avisos antigos e aviso ao motorista de carga acrescentada com a viagem já em rota (o sistema só deixa acrescentar carga em viagem em montagem). O WhatsApp continua saindo pelo n8n, a partir dos eventos da Integração.

Auditoria

Toda ação importante grava uma linha em AuditLog: quem fez (o id e, guardados na hora, o nome e o perfil, porque o usuário pode ser apagado depois), quando, de onde (IP e um resumo do aparelho, como "Safari no iPhone"), a ação (cliente.criar, fatura.pagar, usuario.perfil…), o registro alterado e, quando há, o antes e o depois só dos campos que mudaram. O helper e as regras ficam em src/lib/auditoria.ts.

  • Só inserção. O papel da aplicação não tem UPDATE nem DELETE na tabela (prisma/sql/010-rls.sql): nem uma rota com defeito consegue alterar ou apagar a trilha. Não há rota de alteração.
  • Na mesma transação da mudança quando a rota tem uma (registrarAuditoria): ação recusada pela regra não deixa linha, e linha que não pôde ser gravada desfaz a mudança. Rota que grava sem transação registra logo depois (registrarAuditoriaDepois); ali, se a linha falhar, o erro vai para o log do servidor e a resposta segue a mesma.
  • O que nunca entra no antes e depois: senha, hash, segredo da integração, token, o símbolo da empresa, foto e assinatura de comprovante e XML de nota (lista CAMPOS_PROIBIDOS, que vale em qualquer profundidade). Imagem embutida é omitida mesmo num campo de outro nome, e texto longo é cortado em 500 caracteres.
  • IP: o primeiro valor de x-forwarded-for (o proxy na frente do sistema precisa preenchê-lo); sem cabeçalho, fica vazio.
  • Tela /dashboard/auditoria (menu Sistema, só ADMIN): da mais recente para a mais antiga, 30 por vez ("Carregar mais"), com filtro por período, usuário, tipo de registro, ação e id do registro. Abrir uma linha mostra antes e depois lado a lado.

Quem assina. A equipe assina com o usuário dela. O motorista (aplicativo) e o cliente (portal) também: requireDriver e requirePortalClient devolvem o ator da linha. O que vem do cadastro de empresas da plataforma fica na trilha da empresa afetada, sem userId (a conta do login único não é usuário de empresa nenhuma), com o nome e o e-mail da conta e o perfil PLATAFORMA.

O que é registrado: criar, alterar, desativar e reativar cliente, motorista e ajudante, e trocar o percentual de comissão do motorista; registrar, alterar e excluir ausência; registrar, acertar e reabrir adiantamento; os parâmetros de cobrança da empresa; criar e alterar veículo, e registrar abastecimento e documento dele; criar e alterar usuário, trocar perfil, pedir a liberação de acesso e revogar o do e-mail antigo de um motorista; criar, alterar e mudar status de carga, e informar frete à mão; criar, alterar, liberar, cancelar e finalizar manifesto, e retirar carga dele; emitir, pagar, reabrir e cancelar fatura; criar, alterar, pagar, reabrir e excluir lançamento; aprovar e devolver ao motorista o comprovante, e o reenvio dele pelo motorista; o perfil do comprovante de entrega da empresa; nome e símbolo da empresa e o endereço da integração; criar e alterar tabela de frete e trocar as cidades dela (quantas havia e quantas ficaram, não cada preço); abrir chamado e mudar status, prioridade ou responsável; concluir conferência no depósito e criar ou alterar posição; importar nota fiscal, criar carga a partir dela, ligá-la a uma carga que já existe e registrar ou desfazer CT-e; registrar cotação pelo painel, alterá-la no funil (status, valor, responsável, motivo da perda), registrar contato com o cliente e convertê-la em carga; registrar manutenção, registrar, alterar e excluir pneu, excluir abastecimento e alterar ou excluir documento de veículo; o que o motorista faz no aplicativo (baixa de entrega, só com o nome de quem recebeu e o tipo da ressalva; tentativa de entrega sem sucesso, só com o motivo; ocorrência; checklist, com os itens com problema); o que o cliente faz no portal (pedir coleta e abrir atendimento); criar, alterar, desativar e reativar empresa pela plataforma; e reenviar aviso.

Rota Quem O que faz
GET /api/auditoria?de=&ate=&usuario=&entidade=&acao=&id=&cursor= administrador A trilha da empresa; de/ate em AAAA-MM-DD (dias do Brasil), id aceita só o começo, proximo é o cursor da página seguinte

A tabela é criada por prisma/sql/018-auditoria.sql; rode npm run db:rls depois dela.

Ainda não é registrado: mensagem em chamado (painel e portal); cotação e lead recebidos do site; leitura de volume e troca de posição de volume no depósito; checklist de veículo lançado pelo painel; despesa de viagem lida pelo motorista, o perfil dele e os destinatários frequentes do portal; e o teste da integração. A troca de status de carga continua também no histórico de status da carga (CollectionStatusHistory), com o usuário. Também não existe: entrada e saída do sistema (login), exportar a trilha e prazo de guarda com descarte.

Portal do cliente

Quem tem perfil CLIENT entra em /portal e vê só os dados da empresa a que o cadastro dele está vinculado: pede coleta, acompanha as que pediu, consulta faturas e abre atendimento (/portal/atendimento, na seção Atendimento e ocorrências). Em /portal/coletas/[id] ficam o andamento com a hora de cada etapa, o link público de rastreio pronto para mandar a quem vai receber, o XML das notas fiscais ligadas à carga, para baixar (e o DANFE em PDF, quando o serviço fiscal está ligado), e o comprovante de entrega (recebedor, foto e assinatura), que dá para imprimir ou salvar em PDF. O comprovante só aparece depois de aprovado na conferência da transportadora; em conferência ou devolvido ao motorista, o cliente só vê que ainda não há comprovante liberado. A ressalva da entrega (tipo e descrição) aparece desde o registro: ver Comprovante de entrega.

Além disso, o cliente tem (regras em src/lib/portal-cliente.ts):

  • Cotação (/portal/cotacao, POST /api/portal/cotacao): informa destino, peso, volumes, valor da nota e, se quiser, a cubagem em m³, e vê o valor e o prazo pela tabela de frete dele (a do cadastro; senão a padrão). É a mesma conta do simulador do painel, mas a resposta é fechada: valor, prazo e avisos, sem a composição, o nome da tabela ou os percentuais. Nada é gravado; "Pedir coleta com estes dados" abre o pedido de coleta preenchido.
  • Tabela de frete (/portal/tabela-frete, GET /api/portal/tabela-frete): só leitura, com as cidades atendidas, o frete mínimo e o prazo de cada uma, e busca por cidade.
  • Destinatários frequentes (/portal/destinatarios; GET e POST /api/portal/destinatarios, PATCH e DELETE /api/portal/destinatarios/[id]; tabela ClientReceiver): nome, CNPJ/CPF opcional, cidade-UF, endereço e contato, até 200 por cliente. No pedido de coleta, escolher um deles preenche o destinatário e a cidade de destino.
  • Pedido de coleta (POST /api/portal/coletas): além dos dados da carga, aceita, tudo opcional, a data e a janela de horário da coleta, a prioridade (normal ou urgente), a cubagem em m³ e uma observação. Os mesmos campos existem na minuta do painel (criação e edição, com auditoria) e aparecem na lista de minutas, nas solicitações pendentes e na viagem do motorista. A cubagem entra no frete quando a tabela tem fator de cubagem; sem ela, a conta é a de sempre.
  • Baixar (GET /api/portal/coletas/exportar?de=AAAA-MM-DD&ate=AAAA-MM-DD): as cargas pedidas no período (padrão: últimos 30 dias; no máximo 366 dias e 5.000 linhas) em CSV montado no servidor, com ;, vírgula decimal e marca de UTF-8 para abrir no Excel: código, data, destino, destinatário, volumes, peso, frete e situação. Texto que começa com =, +, - ou @ sai com apóstrofo na frente, para a planilha não o executar como fórmula.
  • Pix nas faturas: com a chave cadastrada pela transportadora, cada título em aberto traz o Pix Copia e Cola (ver "Cobrança por Pix"). Quando a transportadora gerou a cobrança pelo Mercado Pago, a fatura traz no lugar o Pix dinâmico, com QR Code e baixa automática, e o boleto (ver "Cobrança pelo Mercado Pago").

Cada rota do portal filtra pelo cliente da sessão: destinatário, carga ou título de outro cliente da mesma transportadora responde 404 ou simplesmente não aparece, e o clientId que vier no corpo é ignorado. Entre transportadoras, quem separa é o banco.

Ainda não existe no portal: QR Code do Pix estático, o cliente gerar a própria cobrança do Mercado Pago, exportação em PDF ou XLSX, o cliente alterar ou cancelar um pedido já enviado, endereço e contato do destinatário gravados na carga (o destinatário frequente preenche só o nome e a cidade), cotação gravada como histórico e registro, na trilha de auditoria, das mensagens de atendimento e dos destinatários frequentes (pedir coleta e abrir atendimento já são registrados).

As colunas e a tabela deste módulo (pedido de coleta, destinatários e chave Pix) são criadas por prisma/sql/021-portal-pix.sql; rode npm run db:rls depois dela.

Integração Lojas

O contrato servidor a servidor fica em /api/integracoes/lojas/v1/empresas/{slug}. A chave é criada em Empresa → Integração → Integração Lojas, aparece uma vez e deve ser enviada como Authorization: Bearer .... Toda mutação também exige Idempotency-Key com 8 a 120 caracteres.

Operação Rota
Cotar POST /cotacoes
Confirmar e separar por origem POST /ordens
Consultar rastreio, ocorrências, POD e SLA GET /ordens/{externalId}
Cancelar antes do início operacional POST /ordens/{externalId}/cancelamento
Criar reversa ligada ao RMA POST /reversas
Registrar inspeção final POST /reversas/{externalId}/inspecao

O corpo da cotação aceita CIF, FOB, PICKUP e OWN_CARRIER, uma ou mais origens e volumes com quantidade, peso, dimensões e indicação de pallet. npm run db:push aplica o schema, as constraints do contrato e o RLS; nenhum segredo é salvo em texto aberto.

Estrutura

src/
  app/            rotas (dashboard, driver, portal, login, api)
  components/     UI, motorista, provedores
  data/           cidades atendidas (mapa do motorista) e municípios do Brasil com coordenadas (roteirização)
  lib/            auth, prisma, permissões, cadastros, coletas, rastreio, fila offline
prisma/           schema, seed e migrações pontuais
tests/            suíte Vitest contra Postgres
public/           ícones, manifestos (motorista, painel e portal), service worker (offline do motorista e push)

O que ainda é da Mello aqui dentro

A separação tirou o site, não a marca. Para virar produto de mercado ainda falta decidir:

  • Nomes e ícones: telas, manifesto do PWA e mensagens dizem "Mello".
  • Cidades atendidas: src/data/serviceAreas.csv é a malha da Mello, usada no mapa do motorista. Existe uma cópia no repositório do site.
  • Tabelas Article, SocialPost e ContentMetric: guardam a distribuição do blog da Mello. Continuam no schema para não mexer em dado; o script que as alimentava (prisma/sincronizar-artigos.ts) saiu junto com as matérias.
  • Material comercial: TABELA FRETES 2026 A ABRIL 2027.xlsx, tabela_frete_serilon*.csv, regras_frete_serilon.txt, Cidades.pdf e pdf-generator/ são da operação da Mello e ficaram aqui porque o repositório do site é público.
  • Um banco, um transportador: não há separação por empresa (multi-tenant).

Deploy

O sistema roda no servidor applications, em /opt/tms-avilaops-com, atrás do Caddy, e segue a Norma de Plataforma da Ávila Ops (avilaops/infra, NORMA-PLATAFORMA.md): todo nome sai do domínio e nenhuma porta é publicada no host.

Recurso Nome
Diretório /opt/tms-avilaops-com
Projeto compose tms-avilaops-com
Container tms-avilaops-com-web (rede edge, 172.31.0.11:3000)
Banco e role tms_avilaops_com, no PostgreSQL compartilhado do servidor
Imagem ghcr.io/avilaops/tms, por digest
Saúde GET /api/health (consulta o banco e devolve o commit em execução)

O banco não é um container deste projeto: é o PostgreSQL do próprio servidor, o mesmo dos outros sistemas, coberto pelo backup diário de lá. O container o alcança por host.docker.internal.

Automático (GitHub Actions)

.github/workflows/deploy-production.yml:

  • Todo push e PR em main roda typecheck e testes contra um Postgres real; se passarem, a imagem Docker é construída. Fora de PR, ela é publicada no GHCR.
  • O deploy por SSH só roda na main, com a variável DEPLOY_ENABLED do repositório em true e os segredos DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY e DEPLOY_KNOWN_HOSTS. A chave só tem permissão para publicar a aplicação tms.avilaops.com.
  • No servidor, o deploy baixa a imagem pelo digest, troca o container e confere /api/health; se a versão nova não responder, volta para a anterior.

Os jobs de imagem e deploy são cópia dos de avilaops/infra porque este repositório é público e aquele é privado: o GitHub não deixa repositório público chamar workflow reutilizável de repositório privado.

Banco em produção

Não há migrações versionadas: o schema é aplicado com npm run db:push (o prisma db push seguido do isolamento por empresa), da sua máquina, por túnel SSH. O deploy não faz isso sozinho, então mudança de schema precisa ser aplicada antes de o código que depende dela chegar à main.

ssh -N -L 5433:127.0.0.1:5432 applications &
DATABASE_URL="postgresql://tms_avilaops_com:<senha>@127.0.0.1:5433/tms_avilaops_com?schema=public" npm run db:push

O papel tms_app precisa existir no servidor, com o dono do banco como membro. O dono não pode criar papel, então um superusuário faz isso uma única vez: CREATE ROLE tms_app NOLOGIN; GRANT tms_app TO tms_avilaops_com;.

Empresa nova entra pelo seed, com a DATABASE_URL de produção no ambiente (TENANT_SLUG, TENANT_NAME, ADMIN_EMAIL, ADMIN_PASSWORD).

Coluna tenantId em tabela que já existe. O Postgres calcula o valor padrão ao criar a coluna, mesmo com a tabela vazia, e sem app.tenant_id o db push para com unrecognized configuration parameter "app.tenant_id". Informe a variável na própria conexão, com o id da empresa que deve ficar com as linhas existentes:

DATABASE_URL="postgresql://…/tms_avilaops_com?schema=public&options=-c%20app.tenant_id%3D<id-da-empresa>" npx prisma db push

Foi assim que o banco de produção, criado antes de haver empresas, recebeu o schema em 08/10/2026 (estava vazio).

  • Variáveis: /opt/tms-avilaops-com/.env (modo 600), a partir de .env.example.
  • Voltar versão: republicar o commit anterior pela main. Não há cópia de código nem de build guardada no servidor.

Desenvolvido e mantido por Ávila Ops.

About

TMS: sistema de gestão de transportes terrestres, carga fracionada e dedicada. Coletas, manifestos, frota, financeiro, fiscal/CT-e, app do motorista (PWA) e portal do cliente. Next.js 16 + Prisma + PostgreSQL.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages