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):
- Define Período de Apuração
- Data inicial: Dia 21 do mês anterior às 00:00:00
-
Data final: Dia 20 do mês atual às 23:59:59
-
Busca Chatbots Ativos
- Consulta:
chatBotRepository.findActiveChatBots() -
Apenas chatbots com status ativo são processados
-
Agrupa Chatbots por Account
- Agrupa:
chatbots.groupBy { it.account.id } -
Cada grupo representa uma Account com todos os seus chatbots
-
Para Cada Account:
-
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>>
- Consulta Elasticsearch:
-
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
-
4.3. Aplica Mapeamento:
- Executa
BillingInputMapper.map(inputType, totalAccountQuantity) - A franquia de 5000 é calculada sobre o total da Account, não por chatbot
- Executa
-
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
-
Gravação no Banco de Dados
- Verifica se já existe bilhete:
findByCnpjInsumoAndPeriod(cnpj, insumo, start, end)⚠️ Mudança: Agora busca por CNPJ, não mais por mnemônico - Se existe: Atualiza quantidade
- 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:
- Índice consultado:
billing-{mnemonic}-* - Exemplo:
billing-dbot-luis-*para o chatbot dbot-luis -
Wildcard
*busca em todos os ambientes (d, p, h, etc) -
Filtros aplicados:
- Campo
dateentrestartDateeendDate(ISO 8601) -
Query:
rangeQuery("date").gte(startDate).lte(endDate) -
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
- Resultado:
```kotlin
Map
- Chave: Tipo de insumo (ex: "SERPROBOTS-CONVERSAS", "SERPROLLM-MISTRAL-SMALL")
- Valor: Soma total do campo
quantitypara 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
-
Busca TODOS os bilhetes do período:
kotlin allBillets = billingRepository.findByDate(toDate) -
Identifica bilhetes com problemas:
kotlin billetsComProblema = allBillets.filter { it.statusERP != RECEIVED || (it.statusBilhetador != RECEIVED && it.statusBilhetador != INTERNAL) } -
SEMPRE envia email mensal:
- ✅ Envia mesmo quando tudo está OK (todos os bilhetes processados com sucesso)
- 📊 Email contém relatório completo com TODOS os bilhetes
- ⚠️ Destaca pendências quando existem
-
✅ Mostra mensagem de sucesso quando não há pendências
-
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)
- Cores por Status:
- 🟢 Verde (
#d4edda): RECEIVED - 🟡 Amarelo (
#fff3cd): SUBMITTED - 🔴 Vermelho (
#f8d7da): FAILD - ⚪ Branco (
#ffffff): CREATED -
🔵 Azul (
#e7f3ff): INTERNAL -
Assunto do Email:
- Com pendências:
[Serprobots] Relatório Mensal de Faturamento - X bilhete(s) com pendências de Y processados -
Sem pendências:
[Serprobots] Relatório Mensal de Faturamento - Todos os bilhetes X processados com sucesso -
Destinatários:
- Configurado em
${BILLING_CHECK_EMAILS} -
Suporta múltiplos emails separados por vírgula
-
Formatação:
- Fonte: 11px (reduzida para melhor visualização)
- Padding: 6px nas células
- Banner de alerta (amarelo) quando há pendências
- 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):
- Processa ERP:
- Query:
findByDateAndStatusERP(date, CREATED) - Envia:
erpClient.createBillet(convertToERP(billet)) -
Log:
billingLogRepository.log(system=ERP, operation=CREATE) -
Processa Bilhetador:
- Query:
findByDateAndStatusBilhetador(date, CREATED) - Ignora automático: CNPJ 33683111000107 (statusBilhetador=INTERNAL)
- Envia:
bilhetadorClient.createBillet(convertToBilhetador(billet)) - 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
billingDateERPebillingDateBilhetadorseparadamente - Operation no log:
CONFIRM
checkFaildBillets()
fun checkFaildBillets(toDate: Date)
- Busca TODOS os bilhetes do período
- Filtra bilhetes pendentes em qualquer sistema
- Considera
statusBilhetador = INTERNALcomo 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
Billing→Bilhete(DTO do ERP) - Usa
billingDateERP
convertToBilhetador()
private fun convertToBilhetador(b: Billing): BilheteBilhetador
- Mapeia
Billing→BilheteBilhetador - 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/