Ir para o conteúdo

Sistema de Bilhetagem Serprobots

Visão Geral

O sistema de bilhetagem é responsável por contabilizar o consumo de recursos dos chatbots e enviar os bilhetes para dois sistemas externos de faturamento: ERP (legado) e Bilhetador Corporativo (novo). O processo é dividido em três etapas principais executadas mensalmente: Cálculo, Sincronização e Verificação.

Fluxo Completo Mensal

Dia 22 (01h) → CÁLCULO
  ├─ Consulta Elasticsearch (período: dia 21 mês anterior até dia 20 mês atual)
  ├─ Contabiliza consumo por chatbot e tipo de insumo
  ├─ Aplica mapeamento de insumos (BillingInputMapper)
  └─ Grava bilhetes no PostgreSQL (status: CREATED/INTERNAL)

Dia 25 (01h) → SINCRONIZAÇÃO
  ├─ Envia bilhetes para ERP (statusERP: CREATED → SUBMITTED → RECEIVED)
  └─ Envia bilhetes para Bilhetador (statusBilhetador: CREATED → SUBMITTED → RECEIVED)
      └─ Exceto: CNPJ 33683111000107 (marcado como INTERNAL, não enviado)

Dia 27 (08h) → VERIFICAÇÃO
  ├─ Consulta bilhetes não confirmados (status != RECEIVED)
  ├─ Ignora bilhetes INTERNAL do Bilhetador (válidos)
  └─ Envia email com pendências

1. Cálculo de Consumo

1.1 Origem dos Dados

O consumo de cada chatbot é contabilizado a partir dos logs armazenados no Elasticsearch. O sistema utiliza o componente TicketService para consultar o índice de consumo e contabilizar as interações.

Fonte de Dados: Elasticsearch
Índice: billing-{mnemonic}-* (padrão: billing-<mnemônico>-*)
Exemplo: billing-dbot-robgol-* para o chatbot com mnemônico "dbot-robgol"
Wildcard: O * no final permite buscar em qualquer ambiente (desenvolvimento, homologação, produção)
Serviço: TicketService.getMapOfInputsTotal(mnemonic, startDate, endDate)
Período de Apuração: Do dia 21 do mês anterior até o dia 20 do mês atual
Frequência de Cálculo: Mensal (dia 22 às 01:00)

Estrutura dos dados no índice: - date: Data do ticket (usado para filtrar período) - inputType: Tipo de insumo (texto, áudio, vídeo, etc.) - quantity: Quantidade consumida - chatbotModule: Módulo do chatbot

1.2 Tipos de Insumo

O sistema contabiliza diferentes tipos de consumo que são posteriormente mapeados para os insumos de cobrança:

Insumos do Elasticsearch (Originais)

Insumo Original Descrição Origem
SERPROBOTS-CONVERSAS Conversas tradicionais via API Watson Chatbots padrão
SERPROBOTS-GPT35TURBO16K Conversas usando modelo GPT-3.5 Turbo 16K Chatbots com IA generativa
SERPROLLM-* Tokens consumidos por LLMs (qualquer modelo) Chamadas a modelos de linguagem

Mapeamento para Insumos de Cobrança

O componente BillingInputMapper aplica as seguintes regras de transformação:

⚠️ IMPORTANTE: A franquia de 5000 conversas é calculada por Account (soma de todos os chatbots da conta), não mais por chatbot individual.

Insumo Elasticsearch Condição Insumo Cobrado Quantidade Cobrada
SERPROBOTS-CONVERSAS Account >= 5000 conversas SERPROBOTS-CONVERSAS Quantidade total da Account
SERPROBOTS-CONVERSAS Account < 5000 conversas SERPROBOTS-FRANQUIA 1 (franquia fixa)
SERPROBOTS-GPT35TURBO16K Qualquer quantidade SERPROBOTS-GPT35TURBO16K Quantidade total da Account
SERPROLLM-* Qualquer quantidade SERPROBOTS-TOKEN Quantidade total da Account

Código do Produto: 12736 (fixo para todos os insumos)

Observação: O campo relatorio do bilhete armazena os valores agregados e detalhados por chatbot para rastreabilidade:

conta=<account_name>;cnpj=<billing_cnpj>;chatbots=N;insumoInterno=<insumo_elasticsearch>;qtdOriginal=<quantidade_total>;[chatbot=<mnemonic1>;insumo=<tipo>;qtd=<valor>;][chatbot=<mnemonic2>;insumo=<tipo>;qtd=<valor>;]...

1.3 Processo de Cálculo

Job: BillingCalculationJob
Cron: 0 0 1 22 * ? (dia 22 de cada mês, às 01:00)
Tasklet: BillingCalculationTasklet

Fluxo de Execução (Agregado por Account):

  1. Define Período de Apuração
  2. Data inicial: Dia 21 do mês anterior às 00:00:00
  3. Data final: Dia 20 do mês atual às 23:59:59

  4. Busca Chatbots Ativos

  5. Consulta: chatBotRepository.findActiveChatBots()
  6. Apenas chatbots com status ativo são processados

  7. Agrupa Chatbots por Account

  8. Agrupa: chatbots.groupBy { it.account.id }
  9. Cada grupo representa uma Account com todos os seus chatbots

  10. Para Cada Account:

  11. 4.1. Coleta Consumo de Cada Chatbot:

    • Consulta Elasticsearch: ticketService.getMapOfInputsTotal(mnemonic, initialDate, endDate)
    • Retorna mapa: Map<String, Long> (tipo de insumo → quantidade)
    • Acumula consumo em estrutura: Map<InsumoOriginal, List<ChatbotConsumption>>
  12. 4.2. Agrega Consumo por Tipo de Insumo:

    • Soma as quantidades de todos os chatbots da Account
    • Exemplo: Se chatbot1 tem 3000 conversas e chatbot2 tem 2500 conversas, total da Account = 5500
  13. 4.3. Aplica Mapeamento:

    • Executa BillingInputMapper.map(inputType, totalAccountQuantity)
    • A franquia de 5000 é calculada sobre o total da Account, não por chatbot
  14. 4.4. Cria Bilhete Agregado:

    • chatBotId: NULL (bilhete representa a Account, não um chatbot específico)
    • chatBotMnemonic: Lista de mnemonicos separados por ; (ex: "bot1;bot2;bot3")
    • contract: Código do primeiro chatbot (mantido para compatibilidade com Sigecom)
    • clienteId: CNPJ da Account (account.billingCnpj)
    • relatorio: Informações detalhadas da conta + consumo individual de cada chatbot
  15. Gravação no Banco de Dados

  16. Verifica se já existe bilhete: findByCnpjInsumoAndPeriod(cnpj, insumo, start, end) ⚠️ Mudança: Agora busca por CNPJ, não mais por mnemônico
  17. Se existe: Atualiza quantidade
  18. Se não existe: Insere novo bilhete

2. Armazenamento de Bilhetes

2.1 Tabela billing (PostgreSQL)

Localização: Banco de dados ${POSTGRES_MANAGER_DB}
Schema: public

Estrutura da Tabela

Campo Tipo Descrição
id BIGSERIAL Identificador único (PK)
chat_bot_id UUID ⚠️ NULLABLE - ID do chatbot (NULL para bilhetes agregados por Account)
chat_bot_mnemonic VARCHAR ⚠️ MODIFICADO - Mnemônico do chatbot OU lista separada por ; (ex: "bot1;bot2;bot3")
contract VARCHAR Código Sigecom (contrato) - mantém código do primeiro chatbot
initial_date TIMESTAMP Início do período de apuração
end_date TIMESTAMP Fim do período de apuração
mensagem_id VARCHAR ID único do bilhete (<codigoProduto>:<UUID>)
cliente_id VARCHAR CNPJ da Account (account.billingCnpj)
codigo_produto VARCHAR Código do produto (12736)
insumo VARCHAR Insumo de cobrança (após mapeamento)
relatorio TEXT ⚠️ MODIFICADO - Informações agregadas da conta + detalhes por chatbot
qtd BIGINT Quantidade agregada da Account a ser faturada
tag VARCHAR Tag identificadora ("serprobots")
status_erp VARCHAR(20) Status no sistema ERP
status_bilhetador VARCHAR(20) Status no Bilhetador Corporativo
billing_date_erp TIMESTAMP Data de confirmação no ERP
billing_date_bilhetador TIMESTAMP Data de confirmação no Bilhetador

Campos Legados (Deprecados): - status - Substituído por status_erp e status_bilhetador - billing_date - Substituído por billing_date_erp e billing_date_bilhetador

Mudanças na Estrutura (Bilhetagem por Account): - ⚠️ chat_bot_id: Agora é NULL para novos bilhetes (agregados por Account). Bilhetes antigos ainda possuem o ID. - ⚠️ chat_bot_mnemonic: Contém lista de mnemonicos separados por ; quando múltiplos chatbots compõem a conta. - ⚠️ cliente_id: Sempre contém o CNPJ da Account (account.billingCnpj). - ⚠️ relatorio: Formato expandido com informações da conta e detalhamento por chatbot: conta=<account_name>;cnpj=<billing_cnpj>;chatbots=N;insumoInterno=<tipo>;qtdOriginal=<total>;[chatbot=<mnemonic1>;insumo=<tipo>;qtd=<valor>;][chatbot=<mnemonic2>;insumo=<tipo>;qtd=<valor>;]...

2.2 Status do Bilhete

Cada bilhete possui dois status independentes: um para o ERP e outro para o Bilhetador Corporativo.

Status Possíveis

Status Descrição Próximo Status
CREATED Bilhete criado, aguardando envio SUBMITTED ou FAILD
SUBMITTED Bilhete enviado, aguardando confirmação RECEIVED ou FAILD
RECEIVED Bilhete confirmado pelo sistema externo (final)
FAILD Falha no envio/processamento SUBMITTED (retry)
INTERNAL Uso interno do SERPRO (só Bilhetador) (final - não enviado)

Ciclo de Vida do Status

CREATED ──┬─→ SUBMITTED ──┬─→ RECEIVED (✓ sucesso)
          │               │
          └─→ FAILD ──────┘─→ retry automático

INTERNAL (✓ não enviado ao Bilhetador, válido)

2.3 Regra Especial: CNPJ Interno do SERPRO

CNPJ: 33683111000107
Comportamento:

  • statusERP = CREATED (enviado normalmente ao ERP)
  • statusBilhetador = INTERNAL (NÃO enviado ao Bilhetador)

Razão: Uso interno do SERPRO não deve ser faturado via Bilhetador Corporativo.

Implementação:

val isInternalCNPJ = chatBot.billingCnpj == "33683111000107"
statusBilhetador = if (isInternalCNPJ) Billing.Status.INTERNAL else Billing.Status.CREATED

3. Integrações de Bilhetagem

O sistema integra com dois sistemas externos de forma independente e paralela:

3.1 Integração com ERP

Cliente: ERPClient.kt
Localização: src/main/kotlin/br/gov/serpro/serprobots/integration/infra/spring/jobs/erp/

Autenticação

Tipo: OAuth2 Client Credentials
Identity Server: ${ERP_AUTHORIZATION_ENDPOINT}
Credenciais: ${ERP_AUTHORIZATION} (Base64: client_id:client_secret)

Endpoints

Operação Método Endpoint Descrição
Criar Bilhete POST ${ERP_ENDPOINT}/bilhete Envia novo bilhete
Consultar Bilhete GET ${ERP_ENDPOINT}/consulta/bilhete/{mensagemID} Verifica confirmação

Payload (Bilhete ERP)

data class Bilhete(
    val tag: String,                    // "serprobots"
    val insumo: String,                 // Ex: "SERPROBOTS-CONVERSAS"
    val codigoProduto: String,          // "12736"
    val qtd: Long,                      // Quantidade faturada
    val data: String,                   // Data inicial (ISO 8601)
    val dataCadastro: String,           // Data final (ISO 8601)
    val clienteID: String,              // CNPJ
    val mensagemID: String,             // "12736:<UUID>"
    val contrato: String?,              // Código Sigecom
    val detalhe: Detalhe,               // Informações extras
    val dataInclusao: String?           // Data de envio
)

data class Detalhe(
    val mensagem: String,               // Campo relatorio
    val codigoRetorno: Int              // 200
)

Configuração

erp.identityserver.endpoint=${ERP_AUTHORIZATION_ENDPOINT}
erp.identityserver.authorization=${ERP_AUTHORIZATION}
erp.endpoint=${ERP_ENDPOINT}

3.2 Integração com Bilhetador Corporativo

Cliente: BilhetadorClient.kt
Localização: src/main/kotlin/br/gov/serpro/serprobots/integration/infra/spring/jobs/bilhetador/

Autenticação

Tipo: OAuth2 Client Credentials via Autentikus
Token Endpoint: ${AUTENTIKUS_BASE_URL}/autentikus-authn/api/v1/token
Método: Basic Auth (Base64)
Credenciais:

  • Consumer Key: ${AUTENTIKUS_CONSUMER_KEY}
  • Consumer Secret: ${AUTENTIKUS_CONSUMER_SECRET}
  • Scope: ${BILLING_BILHETADOR_SCOPE} (ex: escopo_bilhetador)

⚠️ IMPORTANTE - Header de Autorização: - Deve usar Accesskey e NÃO Bearer - Formato correto: Authorization: Accesskey <token> - O Bilhetador Corporativo rejeita tokens com Bearer (erro 401: "Token requerido")

Cache de Token:

  • Token armazenado em memória com validade (expires_in)
  • Renovação automática 5 minutos antes da expiração
  • Reduz chamadas desnecessárias ao Autentikus

Endpoints

Operação Método Endpoint Descrição
Criar Bilhete PUT ${BILLING_BILHETADOR_API_URL}/bilhete Envia novo bilhete
Consultar Bilhete GET ${BILLING_BILHETADOR_API_URL}/consulta/bilhete/{mensagemID} Verifica confirmação

⚠️ Importante: A criação de bilhete usa PUT, não POST!

Payload (Bilhete Bilhetador)

data class BilheteBilhetador(
    val tag: String,                    // "serprobots"
    val insumo: String,                 // Ex: "SERPROBOTS-CONVERSAS"
    val codigoProduto: String,          // "12736"
    val qtd: Long,                      // Quantidade faturada
    val data: String,                   // Data inicial (ISO 8601 UTC)
    val dataCadastro: String,           // Data final (ISO 8601 UTC)
    val clienteID: String,              // CNPJ
    val mensagemID: String,             // "12736:<UUID>" (PK única)
    val contrato: String?,              // Código Sigecom
    val detalhe: DetalheBilhetador,     // Informações extras
    val dataInclusao: String?           // Data de envio (ISO 8601 UTC)
)

data class DetalheBilhetador(
    val mensagem: String,               // Campo relatorio
    val codigoRetorno: Int              // 200
)

⚠️ IMPORTANTE - Formato de Datas: - Todas as datas devem estar em formato ISO 8601 UTC - Formato: yyyy-MM-dd'T'HH:mm:ss.SSS'Z' - Exemplo: 2025-11-20T23:59:59.999Z - Timezone: Sempre UTC (sufixo 'Z') - Implementação: dateFormatterBilhetador com timeZone = TimeZone.getTimeZone("UTC")

Campo mensagemID (Chave Primária): - Formato obrigatório: <codigoProduto>:<UUID type 4> - Exemplo: 12736:fa9a7743-fbc5-4e6f-a203-7078c4c2d19e - Gerado automaticamente pelo sistema

Tratamento de Erros HTTP

Código Significado Ação
400 Bad Request Marca como FAILD, log do erro
401 Unauthorized Marca como FAILD, log do erro
403 Forbidden Marca como FAILD, log do erro
404 Not Found Retorna null (bilhete não existe)
451 Token Expired Limpa cache de token, marca como FAILD
500 Server Error Marca como FAILD, log do erro

Configuração

# API do Bilhetador
billing.bilhetador.api.url=${BILLING_BILHETADOR_API_URL}

# Autenticação via Autentikus (variáveis existentes)
billing.bilhetador.autentikus.url=${AUTENTIKUS_BASE_URL}
billing.bilhetador.autentikus.consumer.key=${AUTENTIKUS_CONSUMER_KEY}
billing.bilhetador.autentikus.consumer.secret=${AUTENTIKUS_CONSUMER_SECRET}
billing.bilhetador.autentikus.scope=${BILLING_BILHETADOR_SCOPE}

Variáveis de Ambiente Necessárias:

Variável Descrição Exemplo
BILLING_BILHETADOR_API_URL URL da API do Bilhetador https://val.bilhetadorcorporativo.estaleiro.serpro.gov.br
AUTENTIKUS_BASE_URL URL do Autentikus (já existe) https://valautentikus.estaleiro.serpro.gov.br
AUTENTIKUS_CONSUMER_KEY Chave do cliente (já existe) <consumer_key>
AUTENTIKUS_CONSUMER_SECRET Secret do cliente (já existe) <consumer_secret>
BILLING_BILHETADOR_SCOPE Scope de acesso escopo_bilhetador

3.3 Logs de Integração

Tabela: billing_log (PostgreSQL)
Repositório: BillingLogRepository

Todos os envios e consultas aos sistemas externos são registrados para auditoria e troubleshooting.

Estrutura do Log

Campo Descrição
id Identificador único
event_date Timestamp da operação
message_id ID do bilhete (mensagemID)
system Sistema de destino: "ERP" ou "BILHETADOR"
operation Tipo de operação: "CREATE", "RETRY", "CONFIRM"
payload JSON enviado ao sistema externo
response Resposta recebida
success Boolean (true/false)

Exemplo de Consulta:

-- Logs do ERP
SELECT * FROM billing_log WHERE system = 'ERP' ORDER BY event_date DESC;

-- Logs do Bilhetador
SELECT * FROM billing_log WHERE system = 'BILHETADOR' ORDER BY event_date DESC;

-- Erros recentes
SELECT * FROM billing_log WHERE success = false AND event_date > NOW() - INTERVAL '7 days';

3. Aferição de Consumo no Elasticsearch

3.1 Serviço Responsável

Componente: TicketService
Localização: src/main/kotlin/br/gov/serpro/serprobots/integration/infra/repository/elasticsearch/billing/TicketService.kt
Invocado por: BillingService.measureChatbotConsumption()

3.2 Método Principal: getMapOfInputsTotal()

Assinatura:

fun getMapOfInputsTotal(mnemonic: String, startDate: DateTime, endDate: DateTime): Map<String, Long>

Funcionamento:

  1. Índice consultado: billing-{mnemonic}-*
  2. Exemplo: billing-dbot-luis-* para o chatbot dbot-luis
  3. Wildcard * busca em todos os ambientes (d, p, h, etc)

  4. Filtros aplicados:

  5. Campo date entre startDate e endDate (ISO 8601)
  6. Query: rangeQuery("date").gte(startDate).lte(endDate)

  7. Agregação por tipo de insumo: kotlin terms("inputType").field("inputType.keyword").size(10) .subAggregation(sum("total").field("quantity"))

⚠️ IMPORTANTE: Usa inputType.keyword (não inputType direto) - inputType é campo text com subcampo .keyword - Agregação terms requer campo keyword

  1. Resultado: ```kotlin Map
    • Chave: Tipo de insumo (ex: "SERPROBOTS-CONVERSAS", "SERPROLLM-MISTRAL-SMALL")
    • Valor: Soma total do campo quantity para aquele tipo ```

Exemplo de retorno:

Map(
  "SERPROBOTS-CONVERSAS" -> 160,
  "SERPROBOTS-GPT35TURBO16K" -> 2571,
  "SERPROLLM-MISTRAL-SMALL-3.2-24B" -> 10604
)

3.3 Estrutura dos Documentos no Elasticsearch

Campos do índice billing-{mnemonic}-*:

Campo Tipo Descrição Exemplo
date date Data/hora do consumo 2025-12-02T10:19:03-03
inputType text + keyword Tipo de insumo consumido SERPROBOTS-CONVERSAS
quantity long Quantidade consumida 1
chatbotModule text Módulo do chatbot com ambiente dbot-luis-d
mnemonic text Mnemônico do chatbot dbot-luis
environment text Ambiente (d/p/h) d
conversationId text ID da conversa UUID
source text Origem do registro MESSAGE
unit text Unidade de medida message ou token
description text Descrição do consumo Texto livre
component text Componente que gerou command-replier

Exemplo de documento:

{
  "date": "2025-12-02T10:19:03-03",
  "inputType": "SERPROBOTS-CONVERSAS",
  "quantity": 1,
  "chatbotModule": "dbot-luis-d",
  "mnemonic": "dbot-luis",
  "environment": "d",
  "conversationId": "f4604b7c-6f2c-416b-bdf2-d09d2ae92bfd",
  "source": "MESSAGE",
  "unit": "message",
  "description": "Mensagem consumida no módulo dbot-luis-d.",
  "component": "command-replier"
}

3.4 Fluxo Completo de Consulta

BillingService.measureChatbotConsumption(start, end)
    ↓
1. Define período (21/mês-1 a 20/mês-atual)
    ↓
2. Busca chatbots ativos: chatBotRepository.findActiveChatBots()
    ↓
3. Para cada chatbot:
    ↓
4. TicketService.getMapOfInputsTotal(mnemonic, startDate, endDate)
   ├─ Query Elasticsearch: billing-{mnemonic}-*
   ├─ Filtra por date (range query)
   ├─ Agrega por inputType.keyword
   └─ Soma campo quantity
    ↓
5. Para cada tipo de insumo retornado:
    ↓
6. BillingInputMapper.mapInputToBilling(inputType, quantity)
   ├─ SERPROBOTS-CONVERSAS → CONVERSAS ou FRANQUIA (regra >= 5000)
   ├─ SERPROBOTS-GPT35TURBO16K → GPT35TURBO16K (mantém)
   └─ SERPROLLM-* → SERPROBOTS-TOKEN (mapeia)
    ↓
7. BillingService.createOrUpdateBilling()
   └─ Grava em PostgreSQL (tabela billing)

3.5 Configuração do Elasticsearch

application.properties:

# Conexão Elasticsearch
elasticsearch.log.client.name=elasticsearch-log
elasticsearch.log.hosts=${ELASTICSEARCH_LOG_HOSTS:http://10.139.73.167:9200}
elasticsearch.log.user=${ELASTICSEARCH_LOG_USER:elastic}
elasticsearch.log.password=${ELASTICSEARCH_LOG_PASSWORD:<senha>}

Observações: - O padrão billing-{mnemonic}-* é hard-coded no TicketService - Não há variável de configuração para alterar o prefixo "billing" - O cliente Elasticsearch é construído via ElasticRestClientManager

3.6 Mapeamento para Insumos de Cobrança

Após obter os dados do Elasticsearch, o BillingInputMapper converte para insumos faturáveis:

Origem Elasticsearch Tipo Insumo Final Regra
SERPROBOTS-CONVERSAS Mensagem SERPROBOTS-CONVERSAS ou SERPROBOTS-FRANQUIA >= 5000 = CONVERSAS
< 5000 = FRANQUIA (qtd=1)
SERPROBOTS-GPT35TURBO16K Token LLM SERPROBOTS-GPT35TURBO16K Quantidade original (mantém)
SERPROLLM-* (qualquer modelo) Token LLM SERPROBOTS-TOKEN Quantidade original (unifica)

Implementação:

// BillingInputMapper.kt
when {
    inputType == "SERPROBOTS-CONVERSAS" -> {
        if (quantity >= 5000) Pair("SERPROBOTS-CONVERSAS", quantity)
        else Pair("SERPROBOTS-FRANQUIA", 1L)
    }
    inputType == "SERPROBOTS-GPT35TURBO16K" -> {
        Pair("SERPROBOTS-GPT35TURBO16K", quantity)
    }
    inputType.startsWith("SERPROLLM-") -> {
        Pair("SERPROBOTS-TOKEN", quantity)
    }
    else -> Pair(inputType, quantity) // Fallback: mantém original
}

3.7 Campo relatorio - Rastreabilidade

O sistema mantém rastreabilidade dos valores originais no campo relatorio:

chatbot=<mnemonic>;insumoInterno=<inputType_original>;qtdOriginal=<quantidade_elasticsearch>;

Exemplo:

chatbot=dbot-luis;insumoInterno=SERPROBOTS-CONVERSAS;qtdOriginal=160;
chatbot=dbot-luis;insumoInterno=SERPROLLM-MISTRAL-SMALL-3.2-24B;qtdOriginal=10604;

Isso permite auditoria e verificação dos cálculos posteriores.

5. Jobs de Sincronização

Configuração: BillingERPSyncJobsConfig.kt
Cron: 0 0 1 25 * ? (dia 25 de cada mês, às 01:00)
Cron Local: 0 0 3,5,7,9 22 * * (dia 22, múltiplas execuções para testes)

5.2 Processamento do Bilhetador Corporativo

O processamento do Bilhetador ocorre no mesmo job (BillingERPSyncJob), através das mesmas tasklets unificadas.

Diferenças no Processamento

1. CNPJ Interno (33683111000107) - Bilhetes automaticamente marcados com statusBilhetador = INTERNAL - NÃO são enviados ao Bilhetador - Query findByDateAndStatusBilhetador(CREATED) não retorna estes bilhetes - Considerados válidos nas verificações

2. Autenticação - ERP: OAuth2 direto - Bilhetador: OAuth2 via Autentikus (token cacheado)

3. Endpoint de Criação - ERP: POST /bilhete - Bilhetador: PUT /bilhete

4. Tratamento de Erros - Erro 451 (token expirado): Limpa cache e marca como FAILD - Outros erros: Marca como FAILD para retry

4.3 BillingCheckJob - Verificação de Pendências

Configuração: BillingCheckJobsConfig.kt
Tasklet: BillingCheckTasklet
Cron: 0 8 27 * * ? (dia 27 de cada mês, às 08:00)
Cron Local: 0 0 12 22 * * (dia 22 às 12:00)

Funcionamento

  1. Busca TODOS os bilhetes do período: kotlin allBillets = billingRepository.findByDate(toDate)

  2. Identifica bilhetes com problemas: kotlin billetsComProblema = allBillets.filter { it.statusERP != RECEIVED || (it.statusBilhetador != RECEIVED && it.statusBilhetador != INTERNAL) }

  3. SEMPRE envia email mensal:

  4. Envia mesmo quando tudo está OK (todos os bilhetes processados com sucesso)
  5. 📊 Email contém relatório completo com TODOS os bilhetes
  6. ⚠️ Destaca pendências quando existem
  7. ✅ Mostra mensagem de sucesso quando não há pendências

  8. Estrutura do Email HTML:

Seção Bilhetador Corporativo (Principal): - ✅ Bilhetes enviados com sucesso (RECEIVED) - ❌ Bilhetes não enviados/pendentes (CREATED, SUBMITTED, FAILD) - 🔒 Contagem de bilhetes uso interno (INTERNAL) - nota: "exibidos na tabela do ERP"

Seção ERP (Legado): - ✅ Bilhetes enviados com sucesso (RECEIVED) - inclui os INTERNAL - ❌ Bilhetes não enviados/pendentes (CREATED, SUBMITTED, FAILD)

Colunas das Tabelas: - ID Bilhete (mensagemID) - Mnemônico do Chatbot - CNPJ Cliente (novo) - Período (data inicial e final) - Insumo - Quantidade - Status (com cores)

  1. Cores por Status:
  2. 🟢 Verde (#d4edda): RECEIVED
  3. 🟡 Amarelo (#fff3cd): SUBMITTED
  4. 🔴 Vermelho (#f8d7da): FAILD
  5. ⚪ Branco (#ffffff): CREATED
  6. 🔵 Azul (#e7f3ff): INTERNAL

  7. Assunto do Email:

  8. Com pendências: [Serprobots] Relatório Mensal de Faturamento - X bilhete(s) com pendências de Y processados
  9. Sem pendências: [Serprobots] Relatório Mensal de Faturamento - Todos os bilhetes X processados com sucesso

  10. Destinatários:

  11. Configurado em ${BILLING_CHECK_EMAILS}
  12. Suporta múltiplos emails separados por vírgula

  13. Formatação:

  14. Fonte: 11px (reduzida para melhor visualização)
  15. Padding: 6px nas células
  16. Banner de alerta (amarelo) quando há pendências
  17. Banner de sucesso (verde) quando tudo está OK

Exemplo de Email

<h2>📊 Relatório Mensal de Faturamento - Serprobots</h2>
<p><strong>📅 Período de apuração:</strong> 21/10/2025 a 20/11/2025</p>
<p><strong>📄 Total de bilhetes:</strong> 25</p>

<!-- Banner de sucesso (ou alerta se houver problemas) -->
<div style='background-color: #d4edda; border-left: 4px solid #28a745; padding: 10px;'>
  <strong>✅ SUCESSO:</strong> Todos os bilhetes foram processados corretamente!
</div>

<h3>📊 BILHETADOR CORPORATIVO</h3>
<p>✅ Bilhetes enviados com sucesso: 20</p>
<p>❌ Bilhetes não enviados/pendentes: 0</p>
<p>🔒 Bilhetes uso interno (SERPRO): 5 (exibidos na tabela do ERP)</p>

<table style='font-size: 11px;'>
  <tr>
    <th>ID Bilhete</th>
    <th>Mnemônico</th>
    <th>CNPJ Cliente</th>
    <th>Período</th>
    <th>Insumo</th>
    <th>Quantidade</th>
    <th>Status</th>
  </tr>
  <tr>
    <td>12736:uuid-xxx</td>
    <td>chatbot-exemplo</td>
    <td>12345678000190</td>
    <td>21/10 a 20/11</td>
    <td>SERPROBOTS-CONVERSAS</td>
    <td>15000</td>
    <td style="background-color: #d4edda">RECEIVED</td>
  </tr>
</table>

<h3>📋 ERP (Sistema Legado)</h3>
<p>✅ Bilhetes enviados com sucesso: 25 (inclui os 5 INTERNAL)</p>
<p>❌ Bilhetes não enviados/pendentes: 0</p>

5. Serviço Central: BillingService

Localização: src/main/kotlin/br/gov/serpro/serprobots/integration/infra/spring/jobs/billing/BillingService.kt

O BillingService concentra toda a lógica de negócio de faturamento e orquestra as operações com ambos os sistemas.

5.1 Métodos Principais

measureChatbotConsumption()

fun measureChatbotConsumption(start: DateTime, end: DateTime)
  • Busca todos os chatbots ativos
  • Para cada chatbot, chama measureChatbotConsumption(mnemonic, start, end)
fun measureChatbotConsumption(mnemonic: String, initialDate: DateTime, endDate: DateTime)
  • Consulta Elasticsearch: ticketService.getMapOfInputsTotal()
  • Aplica mapeamento: BillingInputMapper.map()
  • Cria/atualiza bilhetes: createOrUpdateBilling()

proccessCreatedBillets()

fun proccessCreatedBillets(date: Date)

Processamento Dual (ERP + Bilhetador):

  1. Processa ERP:
  2. Query: findByDateAndStatusERP(date, CREATED)
  3. Envia: erpClient.createBillet(convertToERP(billet))
  4. Log: billingLogRepository.log(system=ERP, operation=CREATE)

  5. Processa Bilhetador:

  6. Query: findByDateAndStatusBilhetador(date, CREATED)
  7. Ignora automático: CNPJ 33683111000107 (statusBilhetador=INTERNAL)
  8. Envia: bilhetadorClient.createBillet(convertToBilhetador(billet))
  9. Log: billingLogRepository.log(system=BILHETADOR, operation=CREATE)

proccessFaildBillets()

fun proccessFaildBillets(date: Date)
  • Mesmo processo do proccessCreatedBillets(), mas para status FAILD
  • Operation no log: RETRY

proccessSubmittedBillets()

fun proccessSubmittedBillets(date: Date)
  • Consulta sistemas para confirmar recebimento
  • Atualiza billingDateERP e billingDateBilhetador separadamente
  • Operation no log: CONFIRM

checkFaildBillets()

fun checkFaildBillets(toDate: Date)
  • Busca TODOS os bilhetes do período
  • Filtra bilhetes pendentes em qualquer sistema
  • Considera statusBilhetador = INTERNAL como válido
  • SEMPRE envia email mensal (mesmo quando tudo está OK)
  • Email inclui relatório completo de TODOS os bilhetes
  • Destaca pendências quando existem
  • Mostra mensagem de sucesso quando não há problemas

5.2 Métodos Auxiliares

createOrUpdateBilling()

private fun createOrUpdateBilling(
    chatBot: ChatBot,
    initialDate: Date,
    endDate: Date,
    codigoProduto: String,
    insumo: String,
    insumoOriginal: String,
    qtd: Long,
    qtdOriginal: Long
): Billing

Regra de Negócio Importante:

val isInternalCNPJ = chatBot.billingCnpj == "33683111000107"

billing = Billing(
    // ... outros campos ...
    statusERP = Billing.Status.CREATED,
    statusBilhetador = if (isInternalCNPJ) Billing.Status.INTERNAL else Billing.Status.CREATED,
    billingDateERP = null,
    billingDateBilhetador = null
)

convertToERP()

private fun convertToERP(b: Billing): Bilhete
  • Mapeia BillingBilhete (DTO do ERP)
  • Usa billingDateERP

convertToBilhetador()

private fun convertToBilhetador(b: Billing): BilheteBilhetador
  • Mapeia BillingBilheteBilhetador
  • Usa billingDateBilhetador

6. Período de Apuração e Calendário

6.1 Regras de Período

Período de Apuração: - Início: Dia 21 do mês anterior às 00:00:00 - Fim: Dia 20 do mês atual às 23:59:59

Exemplo (Novembro 2025): - Período: 21/10/2025 00:00:00 até 20/11/2025 23:59:59 - Cálculo: 22/11/2025 às 01:00 - Envio: 25/11/2025 às 01:00 - Verificação: 27/11/2025 às 08:00

6.2 Calendário Mensal

Dia 21 (mês anterior) ────────────────┐
                                       │ Período de
Dia 20 (mês atual)    ────────────────┘ Apuração

Dia 22, 01:00 → Cálculo de Consumo
   ├─ Consulta Elasticsearch
   ├─ Aplica mapeamento
   └─ Grava bilhetes (statusERP=CREATED, statusBilhetador=CREATED/INTERNAL)

Dia 25, 01:00 → Sincronização
   ├─ Step 1: Envia bilhetes CREATED
   ├─ Step 2: Reenvia bilhetes FAILD
   └─ Step 3: Confirma bilhetes SUBMITTED

Dia 27, 08:00 → Verificação
   ├─ Consulta bilhetes pendentes
   └─ Envia email de notificação

6.3 Configurações de Cron

# Produção
billing.calculation.job.cron=0 0 1 22 * ?          # Dia 22 às 01:00
billing.proccess.job.cron=0 0 1 25 * ?             # Dia 25 às 01:00
billing.check.job.cron=0 8 27 * * ?                # Dia 27 às 08:00

# Local (testes)
billing.calculation.job.cron=0 0 1 22 * *          # Dia 22 às 01:00
billing.proccess.job.cron=0 0 3,5,7,9 22 * *       # Dia 22 (múltiplas execuções)
billing.check.job.cron=0 0 12 22 * *               # Dia 22 às 12:00

7. Execução Manual via REST API

O sistema permite execução manual dos jobs de bilhetagem através de endpoints REST, útil para testes, correções ou execuções extraordinárias.

7.1 BillingController - Endpoints Principais

Controller: BillingController.kt
Base URL: /billing
Localização: src/main/kotlin/br/gov/serpro/serprobots/integration/presentation/controller/BillingController.kt

Endpoints Disponíveis

Endpoint Método Job Executado Descrição
/billing/calculation POST billingCalculationJob Calcula consumo de todos os chatbots
/billing/proccess POST billingERPSyncJob Sincroniza bilhetes (ERP + Bilhetador)
/billing/check POST billingCheckJob Verifica pendências e envia email

Detalhamento dos Endpoints

1. POST /billing/calculation

Executa o job de cálculo de consumo manualmente.

Função: - Consulta Elasticsearch para todos os chatbots ativos - Contabiliza consumo por insumo - Aplica mapeamento de insumos - Cria/atualiza bilhetes no PostgreSQL

Resposta: - Sucesso: Job ID do Spring Batch (ex: "123456789") - Erro - Job não encontrado: "Job não encontrado" - Erro - Já executado: "A contagem de mensagens já foi solicitada para o período"

Exemplo:

curl -X POST http://localhost:8081/billing/calculation

2. POST /billing/proccess

Executa o job de sincronização com sistemas externos (ERP e Bilhetador Corporativo).

Função: - Step 1: Envia bilhetes com status = CREATED - Step 2: Reenvia bilhetes com status = FAILD (retry) - Step 3: Confirma bilhetes com status = SUBMITTED

Processamento: - Processa ERP e Bilhetador em paralelo (lógica unificada) - Ignora bilhetes com statusBilhetador = INTERNAL (CNPJ 33683111000107) - Registra logs em billing_log com discriminador de sistema

Resposta: - Sucesso: Job ID do Spring Batch - Erro - Job não encontrado: "Job não encontrado" - Erro - Já executado: "A sincronização de bilhetes com ERP já foi finalizada para o período"

Exemplo:

curl -X POST http://localhost:8081/billing/proccess

3. POST /billing/check

Executa verificação de bilhetes pendentes e envia notificação por email.

Função: - Busca bilhetes não confirmados em ambos os sistemas - Considera statusBilhetador = INTERNAL como válido - Gera email HTML com tabela de pendências - Envia para destinatários configurados

Resposta: - Sucesso: Job ID do Spring Batch - Erro - Job não encontrado: "Job não encontrado" - Erro - Já executado: "A sincronização de bilhetes com ERP já foi finalizada para o período"

Exemplo:

curl -X POST http://localhost:8081/billing/check

7.2 BotController - Endpoint Adicional

Controller: BotController.kt
Base URL: /bot

POST /bot/billing-calculation

Endpoint alternativo para executar o cálculo de consumo (mesmo job do /billing/calculation).

Localização: src/main/kotlin/br/gov/serpro/serprobots/integration/presentation/controller/BotController.kt

Função: Idêntica ao /billing/calculation

Exemplo:

curl -X POST http://localhost:8081/bot/billing-calculation

7.3 Sequência Completa de Execução Manual

Para simular o fluxo mensal completo de forma manual:

# 1. Calcular consumo (equivalente ao job do dia 22)
curl -X POST http://localhost:8081/billing/calculation
# Aguardar conclusão (verificar logs)

# 2. Sincronizar com sistemas externos (equivalente ao job do dia 25)
curl -X POST http://localhost:8081/billing/proccess
# Aguardar conclusão (verificar logs)

# 3. Verificar pendências (equivalente ao job do dia 27)
curl -X POST http://localhost:8081/billing/check
# Email será enviado se houver pendências

7.4 Monitoramento de Execução

Via Logs da Aplicação:

tail -f logs/application.log | grep -E "BillingController|BillingService|BilhetadorClient|ERPClient"

Via Banco de Dados:

-- Verificar última execução do job
SELECT * FROM batch_job_execution 
WHERE job_instance_id IN (
    SELECT job_instance_id FROM batch_job_instance 
    WHERE job_name IN ('billingCalculationJob', 'billingERPSyncJob', 'billingCheckJob')
)
ORDER BY start_time DESC LIMIT 10;

-- Verificar bilhetes criados
SELECT COUNT(*), status_erp, status_bilhetador 
FROM billing 
WHERE end_date = (SELECT MAX(end_date) FROM billing)
GROUP BY status_erp, status_bilhetador;

-- Verificar logs de integração
SELECT system, operation, success, COUNT(*) 
FROM billing_log 
WHERE event_date > NOW() - INTERVAL '1 hour'
GROUP BY system, operation, success;

7.5 Casos de Uso Comuns

Recalcular bilhetagem de um período específico: 1. Deletar bilhetes existentes (se necessário): sql DELETE FROM billing WHERE end_date = '2025-11-20'; 2. Executar cálculo: bash curl -X POST http://localhost:8081/billing/calculation

Reenviar bilhetes que falharam: 1. Verificar bilhetes com falha: sql SELECT * FROM billing WHERE status_erp = 'FAILD' OR status_bilhetador = 'FAILD'; 2. Executar sincronização (retry automático): bash curl -X POST http://localhost:8081/billing/proccess

Forçar verificação de pendências:

curl -X POST http://localhost:8081/billing/check

7.6 Observações Importantes

⚠️ Atenção: - Os jobs usam o período atual definido automaticamente (dia 21 mês anterior até dia 20 mês atual) - Para alterar o período, seria necessário modificar os parâmetros do job ou a lógica no BillingService - Jobs Spring Batch podem impedir execuções duplicadas para o mesmo período (por isso a mensagem de erro "já foi solicitada") - Para executar novamente o mesmo job, pode ser necessário limpar a tabela batch_job_execution ou usar parâmetros diferentes

⚠️ CNPJ Interno: - Bilhetes do CNPJ 33683111000107 são automaticamente marcados como INTERNAL no Bilhetador - Não serão enviados ao Bilhetador Corporativo (comportamento esperado) - Serão enviados normalmente ao ERP

⚠️ Segurança: - Em produção, estes endpoints devem ser protegidos (autenticação/autorização) - Recomenda-se adicionar @PreAuthorize ou similar - Limitar acesso apenas a usuários administradores

8. Monitoramento e Troubleshooting

8.1 Consultas Úteis

Bilhetes pendentes de envio ao ERP:

SELECT * FROM billing 
WHERE end_date <= CURRENT_DATE 
AND status_erp != 'RECEIVED'
ORDER BY end_date DESC;

Bilhetes pendentes de envio ao Bilhetador:

SELECT * FROM billing 
WHERE end_date <= CURRENT_DATE 
AND status_bilhetador NOT IN ('RECEIVED', 'INTERNAL')
ORDER BY end_date DESC;

Bilhetes com falha:

SELECT * FROM billing 
WHERE status_erp = 'FAILD' OR status_bilhetador = 'FAILD'
ORDER BY end_date DESC;

Bilhetes internos (CNPJ 33683111000107):

SELECT * FROM billing 
WHERE status_bilhetador = 'INTERNAL'
ORDER BY end_date DESC;

Logs de erro recentes:

SELECT * FROM billing_log 
WHERE success = false 
AND event_date > NOW() - INTERVAL '7 days'
ORDER BY event_date DESC;

Comparação ERP vs Bilhetador:

SELECT 
    chat_bot_mnemonic,
    COUNT(*) as total,
    SUM(CASE WHEN status_erp = 'RECEIVED' THEN 1 ELSE 0 END) as erp_ok,
    SUM(CASE WHEN status_bilhetador IN ('RECEIVED', 'INTERNAL') THEN 1 ELSE 0 END) as bilhetador_ok
FROM billing
WHERE end_date >= '2025-11-20'
GROUP BY chat_bot_mnemonic;

8.2 Logs da Aplicação

Padrão de Logs:

[BillingService] Iniciando contagem de conversas para o periodo: <inicio> a <fim>
[BillingService] Contagem para o chatbot <mnemonic>
[BillingService] Inserindo novo bilhete (CNPJ interno - não será enviado ao Bilhetador)
[BillingService] Total de billets para ERP: <N>
[BillingService] Enviando bilhete <id> ao ERP
[BillingService] Total de billets para Bilhetador: <N>
[BillingService] Enviando bilhete <id> ao Bilhetador
[BilhetadorClient] Obtendo novo token do Autentikus
[BilhetadorClient] Token obtido com sucesso. Expira em: 3600s
[BilhetadorClient] Usando token em cache
[BilhetadorClient] Bilhete criado com sucesso: <mensagemID>
[BillingLogRepository] Registrando log [ERP]: CREATE
[BillingLogRepository] Registrando log [BILHETADOR]: CREATE

8.3 Checklist de Validação Pós-Execução

Após Cálculo (Dia 22): - [ ] Verificar bilhetes criados: SELECT COUNT(*) FROM billing WHERE end_date = '<dia_20_atual>' - [ ] Confirmar status inicial: statusERP = CREATED, statusBilhetador = CREATED ou INTERNAL - [ ] Validar CNPJ interno: SELECT * FROM billing WHERE cliente_id = '33683111000107' AND status_bilhetador = 'INTERNAL'

Após Sincronização (Dia 25): - [ ] Verificar envios bem-sucedidos: status = SUBMITTED ou RECEIVED - [ ] Checar falhas: SELECT * FROM billing WHERE status_erp = 'FAILD' OR status_bilhetador = 'FAILD' - [ ] Consultar logs: SELECT * FROM billing_log WHERE event_date > '<data_execucao>'

Após Verificação (Dia 27): - [ ] Verificar se email foi enviado (se houver pendências) - [ ] Confirmar bilhetes recebidos: status_erp = RECEIVED e status_bilhetador = RECEIVED ou INTERNAL

9. Contatos e Referências

Componentes Principais: - BillingService - Lógica de negócio central - BillingInputMapper - Mapeamento de insumos - ERPClient - Integração com ERP - BilhetadorClient - Integração com Bilhetador Corporativo - BillingLogRepository - Logs de integração - TicketService - Consulta Elasticsearch

Documentação Relacionada: - Swagger Bilhetador Corporativo: https://val.bilhetadorcorporativo.estaleiro.serpro.gov.br/swagger-ui.html - Autentikus: https://valautentikus.estaleiro.serpro.gov.br/autentikus-authn/