openapi: 3.1.0

info:
  title: API Mobile — Aleff / Revo
  version: '2026-09-11'
  summary: Contrato observado em homologação, medido chamada a chamada.
  description: |
    Este documento **não é o contrato publicado**: é o contrato **medido**.

    Cada rota, cada campo obrigatório e cada código de erro daqui saiu de uma
    chamada real contra `wshomdev.grupoaleff.com.br`, feita pelo app Sitra
    Mobile 6.0.13 em 08/09/2026 — 105 chamadas, transportadora 999, romaneios
    `720/26` (coleta, 5 serviços) e `2303/26` (entrega, 9 serviços), com os
    quatro tipos de documento dentro. A baixa foi revalidada em 11/09/2026 pela
    6.0.16, já com o schema enviado pelo TMS.

    Em 11/09 à tarde o TMS respondeu os pontos em aberto: rota nova de
    alterações, corpo de posições, decimais com ponto, ids numéricos, o tipo
    do documento escrito (`NFE`) e 409 para anexo repetido. A **6.0.17**
    segue essa resposta, e foi **medida na mesma tarde**, com a homologação
    de volta: 18 chamadas, tudo bateu. Ficaram dois pontos no servidor —
    baixa com idempotência repetida responde 500 `codigo 23`, e a sessão da
    placa `ABC1020` responde `codigo 7`, sem serviço. A **6.0.18** traz os
    ajustes do app que saíram dessa medição. A **6.0.19** conserta o envio
    dos anexos, que não saía do aparelho desde a 6.0.13, e trata o `codigo 23`
    de um reenvio depois de uma queda como baixa já recebida.

    Onde o comportamento medido diverge do contrato publicado, vale o medido, e
    a divergência está anotada em `x-divergencia`. Onde a rota não funciona, a
    operação traz `x-status: defeito` com a evidência.

    ## Situação por rota

    | Rota | `x-status` |
    |---|---|
    | `POST /mobile/sessao` | `ok` |
    | `POST /mobile/anexos` | `ok` — exige id numérico, ver `x-divergencia` |
    | `POST /mobile/posicoes` | `ok` — não valida o conteúdo do lote |
    | `POST /mobile/servicos/{servicoId}/baixa` | `ok` — desde 11/09, com o schema do TMS |
    | `GET /mobile/romaneio/{transportadora}/alteracoes` | `ok` — rota nova de 11/09, romaneio no query string; medida à tarde |
    | `GET /mobile/romaneio/{transportadora}/{romaneio}/alteracoes` | **`defeito`** — substituída; inalcançável com barra |

    ## Convenções que valem para a API inteira

    - **Sem autenticação.** Nenhuma rota exige header de sessão ou token; a
      identificação viaja no corpo (`Transportadora` + `MotoristaCpf` +
      `VeiculoPlaca`).
    - **CPF e placa vão sem formatação.** `987.654.321-00` e `ABC-1020` são
      recusados com `codigo 6` ("motorista não habilitado") — a mesma resposta
      de credencial errada.
    - **Decimais vêm com vírgula** quando o campo é texto (`"176,1780"`) e com
      ponto quando é número (`176.178`). O **mesmo dado** aparece nas duas
      formas na mesma resposta. Em JavaScript `Number("176,1780")` é `NaN` e
      `parseFloat("176,1780")` é `176`: converta explicitamente.
    - **Na ida, decimal é com ponto.** Confirmado pelo TMS em 11/09 para
      `lat`/`lon` da baixa e das posições (`"-23.5505"`).
    - **Erro é sempre o mesmo envelope**, em qualquer status:
      `{ "codigo": "18", "mensagem": "...", "campo": "..." }`. O `campo` só vem
      nos erros de validação, e nomeia o campo que faltou.
    - **`HTTP 500` com `codigo 15` é exceção não tratada do servidor**, não
      validação. As duas que encontramos: `Object reference not set to an
      instance of an object.` (NullReferenceException) e `Input string was not
      in a correct format.` (falha de `int.Parse`).

  contact:
    name: Sitra Mobile
  x-medicao:
    data: '2026-09-08'
    ambiente: wshomdev.grupoaleff.com.br
    chamadas: 105
    app: Sitra Mobile 6.0.13
    revalidacao: 2026-09-11 · baixa e anexos · Sitra Mobile 6.0.16
    pendente: 6.0.17 (resposta do TMS de 11/09) · homologação fora do ar
    transportadora: '999'
    romaneios: ['720/26', '2303/26']

servers:
  - url: https://wshomdev.grupoaleff.com.br/ApiMobile/api
    description: |
      Homologação. É para onde o app roteia quando a transportadora é 999 ou
      9999. Foi onde tudo neste documento foi medido.
  - url: https://ws.aleff.com.br/ApiMobile/api
    description: |
      Produção. **Ainda na v5 em 11/09/2026**: as rotas /mobile/* respondem
      404 até a homologação ser replicada para cá.
    x-status: indisponivel

tags:
  - name: sessão
    description: Login, carga do romaneio e dos documentos.
  - name: baixa
    description: Conclusão do serviço pelo motorista. Grava desde 11/09.
  - name: anexos
    description: Foto, assinatura e áudio.
  - name: posições
    description: Rastreamento do veículo.
  - name: romaneio
    description: Sincronização de alterações. **Bloqueado por defeito.**

paths:
  /mobile/sessao:
    post:
      tags: [sessão]
      summary: Abre a sessão e devolve romaneios, documentos e parâmetros
      operationId: abrirSessao
      x-status: ok
      description: |
        Única rota que carrega dados. Devolve, de uma vez: os dados da
        transportadora, **todos** os romaneios abertos para o par
        motorista+placa, os documentos de cada um e a tabela de ocorrências.

        ### `nAtt` — atenção, tem efeito colateral

        Com `nAtt: false` (ou ausente) **a chamada marca os documentos como
        recepcionados**. Não é uma leitura: é uma retirada. A segunda chamada
        para o mesmo motorista devolve `codigo 7` — "não existem serviços
        disponíveis" — e o romaneio não volta mais.

        Em campo isso significa que fechar o app, trocar de aparelho ou
        reinstalar faz o motorista perder o romaneio. Medido em produção no app
        5.x: 170 placas, 3.603 chamadas, HTTP 400 da segunda em diante.

        Com `nAtt: true` a resposta é a mesma **sem** marcar nada. É a forma
        correta de sondar em teste, e é o que o app usa em toda verificação.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CorpoSessao'
            examples:
              sonda:
                summary: Sondagem de teste — não consome o romaneio
                value:
                  Transportadora: '999'
                  MotoristaCpf: '98765432100'
                  VeiculoPlaca: ABC1020
                  Dispositivo:
                    So: android
                    VersaoApp: 6.0.13
                    Id: 5f2c1a9e-android
                  nAtt: true
              producao:
                summary: Login de verdade — marca os documentos como recepcionados
                value:
                  Transportadora: '999'
                  MotoristaCpf: '98765432100'
                  VeiculoPlaca: ABC1020
                  Dispositivo:
                    So: android
                    VersaoApp: 6.0.13
                    Id: 5f2c1a9e-android
                  nAtt: false
      responses:
        '200':
          description: Sessão aberta.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Sessao'
        '400':
          description: |
            `codigo 2` CPF ausente · `codigo 3` placa ausente ·
            `codigo 4` transportadora ausente · `codigo 7` sem serviços
            disponíveis, isto é, o romaneio já foi recepcionado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Erro'
              examples:
                semCpf:
                  value:
                    codigo: '2'
                    mensagem: Informe o CPF do motosita para iniciar a sessão!
                    campo: MotoristaCpf
                jaRecepcionado:
                  summary: O que se vê depois de uma chamada sem `nAtt`
                  value:
                    codigo: '7'
                    mensagem: Não existem serviços Disponíveis para motorista e veiculo na Trasnportadora Cod. 999
        '401':
          description: |
            `codigo 6`. Vem também quando o CPF ou a placa chegam **formatados**
            — a mensagem fala em habilitação, mas a causa pode ser a máscara.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Erro'
              example:
                codigo: '6'
                mensagem: Motorista não esta Habilitado para Transportar com este veiculo para Trasnportadora Cod. 999
        '500':
          description: |
            `codigo 5` — `Dispositivo` ausente derruba a requisição com
            NullReferenceException, em vez de virar erro de validação.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Erro'
              example:
                codigo: '5'
                mensagem: Object reference not set to an instance of an object.

  /mobile/servicos/{servicoId}/baixa:
    post:
      tags: [baixa]
      summary: Conclui o serviço — NÃO FUNCIONA
      operationId: enviarBaixa
      x-status: ok
      description: |
        **Grava desde 11/09, com o schema enviado pelo TMS** (`resultado` como
        objeto). Até então o app montava o corpo pelo contrato do swaggerhub
        (`resultado` em texto, `executadoEm` na raiz) e o servidor respondia
        HTTP 500 com NullReferenceException. A evidência abaixo é desse período.

        Sucesso: HTTP 200 `{sucesso, mensagem, baixaId, registradoEm}`.

        ### Validado em 11/09

        | Cenário | Serviço | HTTP |
        |---|---|---|
        | entrega com recebedor | 2416 | 200 · baixaId 1 |
        | coleta + 2 fotos + assinatura | 2440 | 200 · baixaId 6 |
        | ocorrência | 2443 | 200 · baixaId 7 |
        | ocorrência **sem** `recebedor` | 2443 | **500** · `codigo 15` |

        Defeitos que continuam no servidor: o mesmo serviço aceita várias baixas
        com idempotências diferentes (2416 recebeu os baixaIds 1 a 5; em 11/09 à
        tarde o 2440 já tinha o 10 e aceitou o 13).

        ### Medição de 11/09 à tarde, com a 6.0.17

        | Cenário | Serviço | Resultado |
        |---|---|---|
        | ocorrência com `recebedor` vazio | 2443 | 200 · baixaId 8 |
        | ocorrência **sem** `recebedor` | 2443 | 200 · baixaId 9 — o ajuste do TMS entrou |
        | coleta, `tipo: "NFE"`, chave válida | 2440 | 200 · baixaId 10 |
        | coleta com chave de CT-e (DV certo) | 2440 | 400 · `codigo 58` "Chave informada não é do tipo NF-e" |
        | **mesma `idempotencia` de novo** | 2443, 2440 | **500 · `codigo 23`** "Não possível registrar a Baixa." (3 de 3) |

        A idempotência repetida é o reenvio de uma baixa cuja resposta se
        perdeu. Pedido ao TMS: 409, ou 200 com o baixaId original. Remedido em
        11/09 à noite: continua 500 `codigo 23`. A 6.0.18 trata o 409 como já
        enviada. A 6.0.19 trata também o `codigo 23` como já enviada quando a
        tentativa anterior caiu por rede. Fora desse caso, a mensagem não diz
        que a baixa existe, e o item segue no rodízio.

        ### Evidência até 10/09, com o corpo antigo

        | `idempotencia` | HTTP | Resposta |
        |---|---|---|
        | ausente | 400 | `codigo 19` — "enviar o id de idempotencia" (validação normal) |
        | **qualquer valor** | **500** | `codigo 15` — `Object reference not set to an instance of an object.` |

        13 corpos diferentes testados: só `idempotencia`; com `resultado` e
        `executadoEm`; com `recebedor`, `posicao`, `fotos[]` e `coleta`; em
        camelCase e em PascalCase; com `Transportadora`/`VeiculoPlaca`/
        `MotoristaCpf` junto; no vocabulário do TMS com `documentoTransporte`;
        com id GUID e com id numérico. **As 13 responderam 500.**

        Reproduzido nos serviços 2479 e 2477 com o romaneio ativo, e no 2416
        depois. Nos três o serviço **é localizado** — corpo vazio devolve
        `codigo 19`, que é a validação seguinte. A exceção acontece depois da
        busca do serviço.

        ### Segundo defeito: o id da entrega não é aceito

        Varrendo os 14 documentos pela sonda que não grava:

        | Origem do id | Aceitos | Recusados com `codigo 18` |
        |---|---|---|
        | `retornoMobile[].documento`, tipos 4 e 6 | 8 | — |
        | `retornoMobile[].documento`, tipos 5 e 8 (CT-e, NF-e) | — | 6 |
        | `romaneio[].servicos[].id` das entregas | — | 9 |

        Os ids recusados (99586, 99580, 99571, 99570, 101900, 101919, 101094,
        101070, 101043) são publicados pela própria resposta da sessão. No
        romaneio de coleta os dois números coincidem; na entrega divergem, e aí
        nenhum dos dois é aceito.
      parameters:
        - name: servicoId
          in: path
          required: true
          description: |
            `retornoMobile[].documento` — é o que mais funciona, 8 de 14. Para
            NF-e e CT-e nenhuma das duas origens é aceita.
          schema:
            type: string
          example: '2479'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Baixa'
      responses:
        '200':
          description: Nunca observado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Ok'
        '400':
          description: |
            `codigo 16` corpo nulo · `codigo 18` serviço não localizado ·
            `codigo 19` idempotência ausente.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Erro'
              examples:
                naoLocalizado:
                  value:
                    codigo: '18'
                    mensagem: Serviço solicitado não localizado
                semIdempotencia:
                  summary: A sonda que não grava
                  value:
                    codigo: '19'
                    mensagem: Para realizar a baixa por favor enviar o id de idempotencia
        '500':
          description: |
            **O defeito.** Acontece em 100% das chamadas com `idempotencia`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Erro'
              example:
                codigo: '15'
                mensagem: Object reference not set to an instance of an object.

  /mobile/anexos:
    post:
      tags: [anexos]
      summary: Envia foto, assinatura ou áudio
      operationId: enviarAnexo
      x-status: ok
      x-divergencia: |
        O contrato publicado descreve `anexoId` e `baixaIdempotencia` como
        texto; a implementação faz `int.Parse` nos dois. GUID responde 500.
      description: |
        `multipart/form-data`. Funciona — mas o formato do `anexoId` não estava
        no contrato, e custou 31 tentativas para ser descoberto.

        ### `anexoId` e `baixaIdempotencia` são numéricos

        | `anexoId` | HTTP | Resposta |
        |---|---|---|
        | `b3f1c2a9-4d6b-4e3a-…` (GUID) | **500** | `Input string was not in a correct format.` |
        | `a1` | **500** | `Input string was not in a correct format.` |
        | `12,5` | **500** | `Input string was not in a correct format.` |
        | `780588` | **200** | `{"sucesso": true}` |
        | `9223372036854775807` | **200** | `{"sucesso": true}` |
        | `0` | 400 | `codigo 27` — id do anexo não informado |

        O mesmo vale para `baixaIdempotencia`. Negativo e acima de Int32 passam
        — a conversão é para 64 bits e não há faixa validada.

        ### Reenviar o mesmo anexo é erro permanente

        O par `(anexoId, servicoId)` repetido responde **500 `codigo 31`**. O
        mesmo `anexoId` em outro serviço responde 200 — a chave é o par. Em
        11/09 o TMS passou a responder **409 `codigo 31`** (medido à tarde), e o
        app trata o 409 como "já recebido".

        Num app offline-first isso é caro: se a resposta se perde no caminho, o
        anexo já está gravado no servidor mas o app não soube, e a retentativa
        vira erro permanente na tela do motorista.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/Anexo'
            encoding:
              arquivo:
                contentType: image/jpeg, image/png, audio/mp4, audio/wav
      responses:
        '200':
          description: Anexo gravado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Ok'
              example:
                sucesso: true
        '400':
          description: |
            `codigo 27` `anexoId` ausente ou zero · `codigo 28` `servicoId`
            ausente · `codigo 29` serviço não existe · `codigo 30` arquivo
            ausente · `codigo 31` `tipo` ausente ou vazio.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Erro'
              example:
                codigo: '27'
                mensagem: Informe o Id do anexo!
        '500':
          description: |
            `codigo 15` id não numérico · `codigo 31` par
            `(anexoId, servicoId)` já gravado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Erro'
              examples:
                idNaoNumerico:
                  value:
                    codigo: '15'
                    mensagem: Input string was not in a correct format.
                duplicado:
                  value:
                    codigo: '31'
                    mensagem: Anexo já registrado para este serviço

  /mobile/posicoes:
    post:
      tags: [posições]
      summary: Envia um lote de posições do veículo
      operationId: enviarPosicoes
      x-status: ok
      description: |
        Responde 200. **Mas nada dentro de `posicoes[]` é validado**: passou com
        o array vazio, com `lat` em texto, com `lon: null` e com `em: "ontem"`.
        O 200 confirma que o lote chegou; não confirma que o conteúdo está no
        formato que vocês gravam.

        Desde a 6.0.17 o corpo segue o swagger passado pelo TMS em 11/09:
        `transportadora` numérica e `lat`/`lon`/`precisaoM` em texto, com
        ponto decimal. **Ainda não medido** — a homologação caiu antes.

        O formato e o fuso do `em` continuam em aberto: mandamos
        ISO-8601 com offset (`2026-09-08T20:11:00-03:00`), e se o servidor grava
        em horário de Brasília sem fuso, o rastro fica 3 horas deslocado sem
        erro nenhum.

        `romaneio` é obrigatório e é **um só** — com dois romaneios abertos ao
        mesmo tempo, que é o caso deste motorista, não há forma de mandar os
        dois no mesmo lote.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LotePosicoes'
      responses:
        '200':
          description: Lote aceito.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Ok'
        '400':
          description: |
            `codigo 34` placa ausente · `codigo 37` romaneio não pertence à placa.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Erro'

  /mobile/romaneio/{transportadora}/alteracoes:
    get:
      tags: [romaneio]
      summary: Alterações desde um instante — rota nova
      operationId: buscarAlteracoes
      x-status: ok
      description: |
        Rota passada pelo TMS em 11/09 para resolver a barra no número do
        romaneio: o romaneio saiu do path e foi para o query string. É a que
        o app chama a partir da 6.0.17, uma vez por romaneio da sessão.

        Medida em 11/09 à tarde:

        | Romaneio | Resultado |
        |---|---|
        | `2303/26` | 200 · `{sucesso: true}` |
        | `720/26` | 200 · `{sucesso: true}` |
        | `141/26` | 400 · `codigo 13` "romaneio não localizado" |

        Sem mudança, a resposta vem só com `sucesso`, sem as listas. A 6.0.18
        pula o romaneio com `codigo 13` sem acusar falha, para ele não segurar a
        marca de "consultado até" dos outros.
      parameters:
        - name: transportadora
          in: path
          required: true
          schema: { type: string }
          example: '999'
        - name: romaneio
          in: query
          required: true
          description: Com a barra, codificada (`720%2F26`).
          schema: { type: string }
          example: '720/26'
        - name: desde
          in: query
          required: true
          schema: { type: string, format: date-time }
          example: '2026-09-11T10:00:00.000Z'
      responses:
        '200':
          description: '`{sucesso: true}` quando não há mudança.'
        '400':
          description: '`codigo 13` romaneio não localizado.'

  /mobile/romaneio/{transportadora}/{romaneio}/alteracoes:
    get:
      tags: [romaneio]
      summary: Alterações — rota antiga, INALCANÇÁVEL
      operationId: buscarAlteracoesAntiga
      x-status: defeito
      deprecated: true
      description: |
        **Substituída em 11/09** pela rota com o romaneio no query string.
        Mantida aqui pelo registro da medição.

        **O número do romaneio tem barra** (`720/26`, `2303/26`), e a barra faz
        parte do número. Não existe forma de escrevê-lo dentro do path.

        | Forma tentada | Resultado |
        |---|---|
        | `720/26` cru | 404 — a barra vira separador de rota |
        | `720%2F26` | 404 |
        | `720%252F26` | 404 do IIS, em HTML |
        | `720`, `2303`, `720-26`, `720_26`, `72026` | chega na rota: `codigo 13` "romaneio não localizado" |

        23 tentativas, 0 acertos. A rota responde e o `desde` funciona
        (`codigo 14` sem ele) — só não há como endereçar um romaneio real.

        **Sugestão:** mover o romaneio para o query string
        (`/mobile/romaneio/{transportadora}/alteracoes?romaneio=720/26&desde=…`)
        ou trocar para `POST` com corpo.
      parameters:
        - name: transportadora
          in: path
          required: true
          schema: { type: string }
          example: '999'
        - name: romaneio
          in: path
          required: true
          description: Inalcançável quando contém barra — que é sempre.
          schema: { type: string }
          example: '720/26'
        - name: desde
          in: query
          required: true
          description: Obrigatório — `codigo 14` sem ele.
          schema: { type: string, format: date-time }
          example: '2026-09-08T20:16:08.527'
      responses:
        '200':
          description: Nunca observado.
        '400':
          description: '`codigo 13` romaneio não localizado · `codigo 14` `desde` ausente.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Erro'
        '404':
          description: O que acontece com todo romaneio real.

components:
  schemas:

    Erro:
      type: object
      description: Envelope de erro. Vale para qualquer status.
      properties:
        codigo:
          type: string
          description: Vem como string na maioria das rotas e como número em algumas.
        mensagem: { type: string }
        campo:
          type: string
          description: Só nos erros de validação. Nomeia o campo que faltou.
      required: [codigo, mensagem]

    Ok:
      type: object
      properties:
        sucesso: { type: boolean }

    Dispositivo:
      type: object
      description: Obrigatório de fato — ausente, derruba a sessão com `codigo 5`.
      properties:
        So: { type: string, examples: [android, ios] }
        VersaoApp: { type: string, examples: ['6.0.13'] }
        Id: { type: string }
      required: [So, VersaoApp, Id]

    CorpoSessao:
      type: object
      description: |
        Os nomes são `MotoristaCpf` / `VeiculoPlaca` / `Transportadora`, em
        PascalCase — e não os `cpf` / `placa` / `transportadora` do contrato
        publicado.
      x-divergencia: Contrato publicado usa camelCase; a implementação exige PascalCase.
      properties:
        Transportadora: { type: string, examples: ['999'] }
        MotoristaCpf:
          type: string
          description: Só dígitos. Com máscara, responde `codigo 6`.
          examples: ['98765432100']
        VeiculoPlaca:
          type: string
          description: Só letras e dígitos. Com hífen, responde `codigo 6`.
          examples: [ABC1020]
        Dispositivo: { $ref: '#/components/schemas/Dispositivo' }
        nAtt:
          type: boolean
          description: |
            `true` não marca os documentos como recepcionados. Ausente ou
            `false`, marca — e o romaneio não volta na próxima chamada.
          default: false
      required: [Transportadora, MotoristaCpf, VeiculoPlaca, Dispositivo]

    Sessao:
      type: object
      properties:
        sucesso: { type: boolean }
        retornoMobile:
          type: array
          description: Os documentos, com endereço, destinatário e chaves fiscais.
          items: { $ref: '#/components/schemas/Documento' }
        transportadora: { $ref: '#/components/schemas/Transportadora' }
        romaneio:
          type: array
          description: |
            **É um array.** Vieram dois na mesma resposta: `720/26` com 5
            coletas e `2303/26` com 9 entregas.
          items: { $ref: '#/components/schemas/Romaneio' }
        parametros: { $ref: '#/components/schemas/Parametros' }
        ocorrencias:
          type: array
          items: { $ref: '#/components/schemas/Ocorrencia' }

    Transportadora:
      type: object
      description: |
        É **daqui** que saem "exige nome do recebedor" (`obriganomerg`) e
        "rastrear" (`capturaLocalizacao`) — e não do bloco `parametros`, que tem
        campos de nome parecido e valores diferentes.
      properties:
        transportadoraId: { type: integer, examples: [999] }
        transportadoraCnpj: { type: string }
        transportadoraNome: { type: string }
        transportadoraRazao: { type: string }
        obrigafotocomp: { type: boolean }
        obrigaassinatura: { type: boolean }
        obriganomerg:
          type: boolean
          description: Exige o nome de quem recebeu.
        capturaLocalizacao: { type: boolean }
        intervaloLocalizacao:
          type: integer
          description: |
            Unidade não declarada. Veio `5`, enquanto
            `parametros.intervaloPosicaoSeg` veio `300` — os dois só batem se
            este for minuto. **Em aberto.**
          examples: [5]
        parametrosRevoMobile:
          type: object
          description: Veio vazio (`{}`) na medição.

    Parametros:
      type: object
      properties:
        fotosObrigatorias: { type: integer, examples: [1] }
        exigeDocumento: { type: boolean }
        exigeAssinatura: { type: boolean }
        intervaloPosicaoSeg: { type: integer, examples: [300] }

    Romaneio:
      type: object
      properties:
        numero:
          type: string
          description: |
            **Contém barra.** É o que tornava a rota antiga de `/alteracoes`
            inalcançável; na nova ele vai no query string.
          examples: ['720/26']
        sincronizadoEm: { type: string, format: date-time }
        servicos:
          type: array
          items: { $ref: '#/components/schemas/Servico' }

    Servico:
      type: object
      properties:
        id:
          type: integer
          description: |
            **Não é aceito pela rota de baixa nas entregas** — `codigo 18`. Na
            coleta coincide com `documento`, e aí funciona.
          examples: [2479]
        tipo: { type: string, examples: [COLETA, ENTREGA] }
        estado: { type: string, examples: [PENDENTE] }
        janelaTexto: { type: string }
        volumesPrevistos: { type: string, examples: ['40'] }
        pesoPrevisto:
          type: string
          description: Texto com **vírgula** decimal.
          examples: ['176,1780']

    Documento:
      type: object
      properties:
        tipoDocumento:
          type: integer
          description: |
            Tabela passada pelo TMS em 11/09: **2** MDF-e · **4** Minuta ·
            **5** CT-e · **6** Coleta · **8** NF-e. Até a 6.0.16 o app lia o 4
            como MDF-e; as chaves desse tipo têm modelo 20, que é minuta.
          examples: [6]
        documento:
          type: integer
          description: O id que a rota de baixa aceita — 8 dos 14 documentos.
          examples: [2479]
        chaveDocumento: { type: string }
        notaFiscal: { type: integer }
        chavenfe: { type: string }
        documentoTransporte:
          type: string
          description: |
            **A chave que liga o documento ao romaneio.** Único campo que
            particiona os 14 documentos exatamente em 5 + 9.
          examples: ['720/26']
        tipoDocTransporte: { type: integer, examples: [7] }
        ordementrega: { type: integer, examples: [1] }
        estado: { type: string, examples: [PENDENTE] }
        pesoPrevisto:
          type: number
          description: |
            Aqui é **número com ponto** (`176.178`) — o mesmo dado que
            `Servico.pesoPrevisto` traz como texto com vírgula.
          examples: [176.178]
        destinatarioCpfCnpj: { type: string }
        destinatarioNome: { type: string }
        localEndereco: { type: string }
        localNumero: { type: string }
        localBairro: { type: string }
        localCidade: { type: string }
        localEstado: { type: string }
        localCep: { type: string }
        localComplemento: { type: string }
        remetenteCpfCnpj: { type: string }
        remetenteNome: { type: string }
        latitude:
          type: string
          description: |
            Veio `"0"` nos 14 documentos. Tratado como "sem coordenada" — senão
            todo serviço apontaria para o ponto 0,0 no Atlântico.
          examples: ['0']
        longitude: { type: string, examples: ['0'] }
        filialSigla: { type: string }
        filialDocTranspId: { type: string }
        filialDocTranspSigla: { type: string }
        transportadoraId: { type: integer }
        statusSigla: { type: string }
        statusDesc: { type: string }
        janelaTexto: { type: string }
        serieParceiro: { type: string }
        documentoParceiro: { type: string }
        chaveParceiro: { type: string }
        logDaRequisicao: { type: string }

    Ocorrencia:
      type: object
      description: |
        Vieram 13, **todas com `tipo: "ENTREGA"`** — nenhuma de COLETA, mesmo
        com um romaneio de coleta na mesma resposta. Sem elas o motorista não
        registra insucesso de coleta.
      properties:
        ocorrenciaIdMobile: { type: integer }
        descricaoMobile: { type: string }
        ocorrenciaIdRev: { type: integer }
        descricaoRev: { type: string }
        exigeFotoMobile: { type: boolean }
        transportadoraId: { type: integer }
        responsavel: { type: integer }
        ativo: { type: boolean }
        ativoDesc: { type: string }
        tipo: { type: string, examples: [ENTREGA, COLETA] }

    Baixa:
      type: object
      description: |
        **Schema enviado pelo TMS em 11/09 e validado em homologação.** É o
        que o app manda a partir da 6.0.16 (`paraCorpoBaixa`).
      properties:
        servicoId:
          type: integer
          examples: [2440]
        idempotencia:
          type: string
          description: |
            Gerado no aparelho, antes de existir rede. Sem ele, `codigo 19`.
          examples: ['1789128374423245']
        resultado:
          type: object
          properties:
            executado: { type: boolean }
            ocorrencia: { type: boolean }
            executadoEm: { type: string, format: date-time }
          required: [executado, ocorrencia, executadoEm]
        posicao:
          type: object
          description: |
            Texto com **ponto** decimal, confirmado pelo TMS em 11/09. Até a
            6.0.16 o app mandava vírgula.
          properties:
            lat: { type: string, examples: ['-23.5505'] }
            lon: { type: string, examples: ['-46.6333'] }
            precisaoM: { type: string, examples: ['12'] }
        coleta:
          type: object
          properties:
            volumesConferidos: { type: integer }
            motivoDivergencia: { type: string }
            documentos:
              type: array
              items:
                type: object
                properties:
                  tipo:
                    type: string
                    description: |
                      Sigla escrita. O TMS confirmou em 11/09 que vale `NFE`, e
                      que na coleta o documento só pode ser NF-e válida.
                    examples: [NFE]
                  numero: { type: string }
                  chave: { type: string }
        ocorrencia:
          type: object
          properties:
            codigoOcorrencia:
              type: integer
              description: '`ocorrencias[].ocorrenciaIdMobile`, numérico — confirmado pelo TMS em 11/09.'
              examples: [1]
            observacao: { type: string }
            retornaHoje: { type: boolean }
        fotos:
          type: array
          items:
            type: object
            properties:
              seq: { type: integer }
              anexoId: { type: integer, format: int64 }
        assinatura:
          type: object
          properties:
            anexoId: { type: integer, format: int64 }
        recebedor:
          type: object
          description: |
            **Sempre presente.** Sem ele o servidor responde 500 `codigo 15`;
            na ocorrência vai com os dois campos vazios. Na entrega, sem nome,
            `codigo 38`.
          properties:
            nome: { type: string }
            documento: { type: string }
      required: [servicoId, idempotencia, resultado, recebedor]

    Anexo:
      type: object
      properties:
        anexoId:
          type: string
          description: |
            **Numérico**, diferente de zero. Sofre `int.Parse` no servidor:
            GUID responde 500.
          examples: ['1757371268000456']
        servicoId:
          type: string
          description: Precisa existir — `codigo 29` se não.
          examples: ['2479']
        tipo:
          type: string
          description: |
            Obrigatório (`codigo 31` se vazio), mas **o conteúdo não é
            validado** — `"banana"` responde 200. `audio` é aceito.
          examples: [foto, assinatura, audio]
        arquivo:
          type: string
          format: binary
          description: Obrigatório (`codigo 30`). O tipo do arquivo não é validado.
        seq: { type: integer }
        baixaIdempotencia:
          type: string
          description: Numérico, pelo mesmo motivo de `anexoId`.
        capturadoEm: { type: string, format: date-time }
        campo: { type: string }
        transcricao:
          type: string
          description: Texto ditado, quando o anexo é áudio.
        duracaoSeg: { type: number }
      required: [anexoId, servicoId, tipo, arquivo]

    Posicao:
      type: object
      description: Texto com ponto decimal, como no swagger do TMS (6.0.17).
      properties:
        lat: { type: string, examples: ['-22.9035'] }
        lon: { type: string, examples: ['-47.0616'] }
        precisaoM: { type: string, examples: ['12'] }
        em:
          type: string
          format: date-time
          description: |
            **Formato e fuso em aberto** — o servidor aceita qualquer coisa,
            inclusive `"ontem"`. Enviamos ISO-8601 com offset.
          examples: ['2026-09-08T20:11:00-03:00']

    LotePosicoes:
      type: object
      properties:
        transportadora: { type: integer, examples: [999] }
        placa:
          type: string
          description: O campo é `placa`, não `veiculoPlaca`.
          examples: [ABC1020]
        romaneio:
          type: string
          description: |
            Obrigatório e único. Com dois romaneios abertos, não há como mandar
            os dois no mesmo lote. **Em aberto.**
          examples: ['720/26']
        posicoes:
          type: array
          description: O conteúdo não é validado pelo servidor.
          items: { $ref: '#/components/schemas/Posicao' }
      required: [transportadora, placa, romaneio, posicoes]
