Guia IA BOI

Conceitos, Padrões e Convenções do Projeto

Visão Geral

O IA BOI é um sistema de gestão pecuária que utiliza inteligência artificial para análise e sugestões de manejo de gado. O projeto é dividido em três partes principais:

Projetos do IA BOI

  1. ia-boi-api (Backend Java)
    • Localização: C:/dotum/projetos/ia-boi/ia-boi-api
    • API REST em Java
    • Lógica de negócio, acesso a dados, cálculos
    • Usa o framework ia-core
  2. ia-boi-web (Frontend React/Next.js)
    • Localização: C:/dotum/projetos/ia-boi-web/dot/apps/iaboiweb
    • Interface web em React/Next.js
    • Componentes, serviços, hooks
    • Usa bibliotecas @dot/ui e @dot/core
  3. ia-core (Framework Backend)
    • Localização: C:/dotum/projetos/ia-boi/ia-core
    • Framework Java interno
    • Usado pelo ia-boi-api
    • Não deve ser editado diretamente

Bibliotecas e Frameworks


IA BOI API (Backend)

Estrutura de Pastas

src/main/java/
├── bo/                    # Business Objects (lógica de negócio)
├── controller/            # Controllers REST (endpoints da API)
│   └── dto/              # Data Transfer Objects
├── helper/               # Classes auxiliares (cálculos, etc.)
├── model/
│   ├── bean/            # Entidades do Framework (tipo Bean próprio)
│   ├── dao/             # Data Access Objects (acesso a dados)
│   └── dd/              # Default Data (tabelas com dados padrões)
└── service/              # Classes para selects utilizados com frequência

Padrões de Nomenclatura

Controllers

Endpoints REST

Controllers

Controllers são responsáveis por receber requisições HTTP e coordenar a execução da lógica de negócio através de BOs e DAOs.

Exemplo de Controller

@ServiceClass
public class SuplementoController extends AbstractController {

    @Endpoint(path = "/suplemento", method = "POST")
    public SuplementoBean cadastrar(String data, String lote, String pasto, 
                                    String suplementoTipo, BigDecimal quantidadeAlimento, 
                                    String token) throws Exception {
        TransactionDB trans = getTransaction();
        UserContext userContext = getUserContext();
        
        SuplementoBO supBO = new SuplementoBO(trans, userContext);
        SuplementoBean supBean = supBO.cadastrar(data, lote, pasto, suplementoTipo, 
                                                  quantidadeAlimento, token);
        return supBean;
    }
    
    @Endpoint(path = "/suplemento/{id}", method = "GET")
    public SuplementoBean selectById(Integer id) throws Exception {
        TransactionDB trans = getTransaction();
        
        SuplementoBean supBean = SuplementoDAO.getInstance().selectById(trans, id);
        return supBean;
    }
    
    @Endpoint(path = "/suplemento", method = "GET")
    public List<SuplementoListagemDTO> selectAll() throws Exception {
        TransactionDB trans = getTransaction();
        
        // ✅ CORRETO: DAO já trata exceções internamente e retorna lista vazia se necessário
        List<SuplementoBean> supList = SuplementoDAO.getInstance()
            .selectAllComDescricoes(trans);
        
        return SuplementoListagemDTO.convert(supList);
    }

    @Endpoint(path = "/suplemento", method = "DELETE")
    public void inativar(String token) throws Exception {
        TransactionDB trans = getTransaction();
        UserContext userContext = getUserContext();
        
        SuplementoBO supBO = new SuplementoBO(trans, userContext);
        supBO.inativar(token);
    }
}
⚠️ IMPORTANTE: Controllers NÃO devem fazer inserts/updates/deletes diretamente. Essas operações devem ser feitas SOMENTE em Business Objects (BOs).

Business Objects (BOs)

BOs contêm a lógica de negócio do sistema. Eles estendem AbstractBO e recebem TransactionDB e UserContext no construtor.

Exemplo de BO

public class ManejoBO extends AbstractBO {
    
    public ManejoBO(TransactionDB trans, UserContext userContext) {
        super(trans, userContext);
    }
    
    public void cadastrar(String lote, String tipo, BigDecimal peso, 
                          LocalDateTime data) throws Exception {
        // Validações de negócio
        if (lote == null || lote.isEmpty()) {
            throw new WarningException("Lote é obrigatório.");
        }
        
        // Busca o lote
        LoteBean lotBean = LoteDAO.getInstance()
            .selectByDescricao(trans, lote);
        if (lotBean == null) {
            throw new NotFoundException("Lote não encontrado.");
        }
        
        // Cria o manejo
        ManejoBean manBean = new ManejoBean();
        manBean.setLote(lotBean);
        manBean.setData(data);
        manBean.setPeso(peso);
        // ... outros campos
        
        // Insere no banco
        trans.insert(manBean);
    }
}
✅ Responsabilidades dos BOs:

DAOs (Data Access Objects)

DAOs são responsáveis pelo acesso a dados. Eles estendem GenericDAO e seguem o padrão Singleton.

Padrão de DAO

public class SuplementoDAO extends GenericDAO<SuplementoBean> {
    private static final SuplementoDAO INSTANCE = new SuplementoDAO();
    private SuplementoDAO() { super(SuplementoBean.class); }
    public static SuplementoDAO getInstance() { return INSTANCE; }

    public SuplementoBean selectById(TransactionDB trans, Integer id) throws Exception {
        Integer uniId = trans.getUserContext().getUnidade().getId();

        WhereDB<SuplementoBean> where = new WhereDB<>(SuplementoBean.class)
                .and("unidade.id", OperatorDB.EQ, uniId)
                .and("ativo", OperatorDB.EQ, Constants.SIM)
                .and("id", OperatorDB.EQ, id);

        List<SuplementoBean> list = new ArrayList<>();
        try {
            list = select(trans, where);
            return list.get(0);
        } catch (NotFoundException e) {
            // Se não encontrar registro, retorna null ao invés de lançar exceção
            return null;
        }
    }
}
⚠️ REGRA CRÍTICA - Tratamento de Exceções em DAOs:

Exemplo CORRETO de DAO (tratando exceção):

public List<SuplementoBean> selectAll(TransactionDB trans) throws Exception {
    Integer uniId = trans.getUserContext().getUnidade().getId();

    WhereDB<SuplementoBean> where = new WhereDB<>(SuplementoBean.class)
            .and("unidade.id", OperatorDB.EQ, uniId)
            .and("ativo", OperatorDB.EQ, Constants.SIM);

    List<SuplementoBean> list = new ArrayList<>();
    try {
        list = select(trans, where);
        return list;
    } catch (NotFoundException e) {
        // ✅ CORRETO: DAO retorna lista vazia ao invés de lançar exceção
        return list;
    }
}

public List<SuplementoBean> selectAllComDescricoes(TransactionDB trans) throws Exception {
    Integer uniId = trans.getUserContext().getUnidade().getId();

    StringBuilder sb = new StringBuilder();
    sb.append("                select sup.sup_id,                                   		");
    sb.append("                       sup.emp_id,                                   		");
    // ... resto da query
    
    List<Object> params = new ArrayList<Object>();
    params.add(uniId);

    ORMMapper<SuplementoBean> orm = new ORMMapper<SuplementoBean>(SuplementoBean.class);
    orm.add(Integer.class, "sup_id", "id");
    // ... resto do mapeamento

    List<SuplementoBean> list = new ArrayList<>();
    try {
        list = selectCustom(trans, sb.toString(), params, orm);
        return list;
    } catch (NotFoundException e) {
        // ✅ CORRETO: DAO retorna lista vazia ao invés de lançar exceção
        return list;
    }
}

Exemplo ERRADO de DAO (propagando exceção):

// ❌ ERRADO: Não deve propagar exceção
public List<SuplementoBean> selectAllComDescricoes(TransactionDB trans) throws Exception {
    // ❌ ERRADO: DAO não deve propagar NotFoundException
    // Deve tratar internamente e retornar lista vazia
    return selectCustom(trans, sb.toString(), params, orm);
}

Services

Services são classes utilitárias para selects complexos ou cálculos que são utilizados com frequência. Eles geralmente contêm métodos estáticos.

⚠️ REGRA CRÍTICA - Services:

Exemplo CORRETO de Service

public class AnaliseDetalhadaService {

    /**
     * Calcula o estoque atual descontando as suplementações realizadas.
     */
    public static BigDecimal calcularEstoqueAtual(TransactionDB trans, Integer uniId, 
                                                   Integer suptId) throws Exception {
        // ✅ CORRETO: Service chama DAO (não faz consulta SQL direta)
        BigDecimal estoqueTotal = FornecedorEstoqueDAO.getInstance()
                .selectEstoqueAtualByUnidadeAndTipo(trans, uniId, suptId);
        
        // ✅ CORRETO: Service chama DAO (não faz consulta SQL direta)
        // DAO já trata exceções e retorna lista vazia se necessário
        List<SuplementoBean> suplementos = SuplementoDAO.getInstance().selectAll(trans);
        
        // ✅ CORRETO: Service faz cálculos com dados retornados pelos DAOs
        BigDecimal totalSuplementado = BigDecimal.ZERO;
        for (SuplementoBean sup : suplementos) {
            if (sup.getSuplementoTipo() != null && 
                sup.getSuplementoTipo().getId().equals(suptId)) {
                if (sup.getQuantidadeAlimento() != null) {
                    totalSuplementado = Calc.add(totalSuplementado, 
                                                 sup.getQuantidadeAlimento());
                }
            }
        }
        
        // Estoque atual = Estoque Total - Quantidades Suplementadas
        return Calc.subtract(estoqueTotal, totalSuplementado);
    }
}

Exemplo ERRADO de Service (com consulta SQL direta):

// ❌ ERRADO: Service não pode fazer consulta SQL direta
public static BigDecimal calcularEstoqueAtual(TransactionDB trans, Integer uniId, 
                                             Integer suptId) throws Exception {
    // ❌ ERRADO: Service não deve usar StringBuilder, selectCustom, ORMMapper
    StringBuilder sb = new StringBuilder();
    sb.append("select ...");
    ORMMapper<SuplementoBean> orm = new ORMMapper<SuplementoBean>(SuplementoBean.class);
    // ... consulta SQL direta no Service
    return selectCustom(trans, sb.toString(), params, orm);
}

Helpers

Helpers são classes auxiliares que contêm lógica de cálculo ou transformação de dados complexa.

Exemplo de Helper

public class ManejoCalcHelper {

    public record CalcGmdCmcVO(BigDecimal gmd, BigDecimal gmc, 
                                BigDecimal gmdQuantidadePonderada,
                                BigDecimal gmdProducaoVivo, 
                                BigDecimal gmdProducaoCarcaca) {
    };

    public static CalcGmdCmcVO calcFazendaGmdGmc(
            ManejoFazendaTotalizadoresVO mfcDTO, 
            BigDecimal producaoVivoTotal, 
            BigDecimal producaoCarcacaTotal) {
        BigDecimal fazendaGmdDias = new BigDecimal(mfcDTO.getFazendaDiasTotal());
        // ... cálculos complexos
        return new CalcGmdCmcVO(fazendaGmd, fazendaGmc, 
                               fazendaGmdQuantidadePonderada, 
                               producaoVivoTotal, producaoCarcacaTotal);
    }
}

Beans (Entidades)

Default Data (DDs)

DTOs (Data Transfer Objects)

Exemplo de DTO usando Record:

package controller.dto;

import java.util.List;

import model.bean.LoteBean;

public record PastoDTO(String pasto, String lote, Integer quantidade) {

    public static PastoDTO convert(LoteBean lotBean) {
        String lote = lotBean.getDescricao();
        String pasto = lotBean.getPasto().getDescricao();
        Integer quantidade = lotBean.getQuantidade();
        
        return new PastoDTO(pasto, lote, quantidade);
    }
    
    public static List<PastoDTO> convert(List<LoteBean> lotList) {
        return lotList.stream().map(PastoDTO::convert).toList();
    }
}

Tratamento de Exceções

O projeto utiliza duas exceções principais:

⚠️ PADRÃO OBRIGATÓRIO - Tratamento de Exceções:
  1. DAOs DEVEM capturar e tratar exceções - Retornam valores padrão (lista vazia, null, etc.)
  2. Controllers e Services NÃO precisam tratar - DAOs já tratam internamente

Exemplo CORRETO - Controller (sem try-catch):

@Endpoint(path = "/suplemento", method = "GET")
public List<SuplementoListagemDTO> selectAll() throws Exception {
    TransactionDB trans = getTransaction();
    
    // ✅ CORRETO: DAO já trata exceções internamente, retorna lista vazia se necessário
    List<SuplementoBean> supList = SuplementoDAO.getInstance()
        .selectAllComDescricoes(trans);
    
    return SuplementoListagemDTO.convert(supList);
}

Exemplo CORRETO - Service (sem try-catch):

public static SuplementoParametroBean buscarParametro(TransactionDB trans, 
                                                         Integer uniId, Integer empId, 
                                                         Integer usuFornecedorId, 
                                                         Integer tipoId) throws Exception {
    // ✅ CORRETO: DAO já trata exceções internamente, retorna lista vazia se não encontrar
    List<SuplementoParametroBean> parametros = SuplementoParametroDAO.getInstance()
            .selectByPrioridade(trans, uniId, empId, usuFornecedorId, tipoId);
    
    // Retorna o primeiro elemento da lista ou null se a lista estiver vazia
    return parametros.isEmpty() ? null : parametros.get(0);
}

UserContext e TransactionDB

O UserContext identifica quem está fazendo a requisição e o TransactionDB representa uma transação no banco de dados.

Obtenção

// Em classes que estendem AbstractController
UserContext userContext = getUserContext();
TransactionDB trans = getTransaction();

// Em classes que estendem AbstractBO
// UserContext e TransactionDB já estão disponíveis através do super
// Podem ser acessados via getTransaction() e getUserContext()

Uso em DAOs

public SuplementoBean selectById(TransactionDB trans, Integer id) throws Exception {
    // ✅ CORRETO: Obter unidade do UserContext da transação
    Integer uniId = trans.getUserContext().getUnidade().getId();

    WhereDB<SuplementoBean> where = new WhereDB<>(SuplementoBean.class)
            .and("unidade.id", OperatorDB.EQ, uniId)
            .and("ativo", OperatorDB.EQ, Constants.SIM)
            .and("id", OperatorDB.EQ, id);

    List<SuplementoBean> list = new ArrayList<>();
    try {
        list = select(trans, where);
        return list;
    } catch (NotFoundException e) {
        // Se não encontrar registro, retorna null ao invés de lançar exceção
        return null;
    }
}

Queries Customizadas

Para queries complexas, use StringBuilder e ORMMapper.

⚠️ REGRA CRÍTICA - Queries SQL:
⚠️ IMPORTANTE para queries customizadas (apenas em DAOs):

Exemplo de Query Customizada

public List<SuplementoBean> selectAllComDescricoes(TransactionDB trans) throws Exception {
    Integer uniId = trans.getUserContext().getUnidade().getId();

    StringBuilder sb = new StringBuilder();
    sb.append("                select sup.sup_id,                                   		");
    sb.append("                       sup.emp_id,                                   		");
    sb.append("                       sup.uni_id,                                   		");
    sb.append("                       sup.sup_utccadastro,                          		");
    sb.append("                       sup.sup_utcatualizado,                        		");
    sb.append("                       sup.usu_cadastrou_id,                         		");
    sb.append("                       sup.sup_ativo,                                		");
    sb.append("                       sup.sup_data,                                 		");
    sb.append("                       sup.supt_id,                                  		");
    sb.append("                       supt.supt_descricao,                          		");
    sb.append("                       sup.lot_id,                                   		");
    sb.append("                       lot.lot_descricao,                            		");
    sb.append("                       sup.pas_id,                                   		");
    sb.append("                       pas.pas_descricao,                            		");
    sb.append("                       sup.sup_quantidadecabeca,                     		");
    sb.append("                       sup.sup_quantidadealimento,                   		");
    sb.append("                       sup.sup_token                                 		");
    sb.append("                  from suplemento sup                                		");
    sb.append("                  join lote lot on lot.lot_id = sup.lot_id           		");
    sb.append("                  join suplementotipo supt on supt.supt_id = sup.supt_id 	");
    sb.append("             left join pasto pas on pas.pas_id = sup.pas_id          		");
    sb.append("                 where sup.uni_id = ?                                		");
    sb.append("                   and sup.sup_ativo = " + Constants.SIM + "         		");
    sb.append("              order by sup.sup_data desc, sup.sup_id desc             		");

    List<Object> params = new ArrayList<Object>();
    params.add(uniId);

    ORMMapper<SuplementoBean> orm = new ORMMapper<SuplementoBean>(SuplementoBean.class);
    orm.add(Integer.class,    			"sup_id",                 "id");
    orm.add(Integer.class,    			"emp_id",                 "empresa.id");
    orm.add(Integer.class,    			"uni_id",                 "unidade.id");
    orm.add(Integer.class,    			"usu_cadastrou_id",       "usuarioCadastrou.id");
    orm.add(Integer.class,    			"supt_id",                "suplementoTipo.id");
    orm.add(String.class,    			"supt_descricao",         "suplementoTipo.descricao");
    orm.add(Integer.class,    			"lot_id",                 "lote.id");
    orm.add(String.class,    			"lot_descricao",          "lote.descricao");
    orm.add(Integer.class,    			"pas_id",                 "pasto.id");
    orm.add(String.class,    			"pas_descricao",          "pasto.descricao");
    orm.add(LocalDateTime.class,       	"sup_utccadastro",        "utcCadastro");
    orm.add(LocalDateTime.class,       	"sup_utcatualizado",      "utcAtualizado");
    orm.add(Integer.class,    			"sup_ativo",              "ativo");
    orm.add(LocalDateTime.class,     	"sup_data",               "data");
    orm.add(Integer.class,   		   	"sup_quantidadecabeca",   "quantidadeCabeca");
    orm.add(BigDecimal.class, 			"sup_quantidadealimento", "quantidadeAlimento");
    orm.add(String.class,     			"sup_token",              "token");

    List<SuplementoBean> list = new ArrayList<>();
    try {
        list = selectCustom(trans, sb.toString(), params, orm);
        return list;
    } catch (NotFoundException e) {
        // Se não encontrar registros, retorna lista vazia ao invés de lançar exceção
        return list;
    }
}

IA BOI Web (Frontend)

Estrutura de Pastas

src/
├── app/
│   └── (private)/          # Rotas privadas (requerem autenticação)
│       └── [modulo]/       # Módulos da aplicação
│           ├── page.tsx    # Página principal do módulo
│           └── components/ # Componentes específicos do módulo
├── components/             # Componentes reutilizáveis
├── hooks/                  # Hooks customizados
└── services/              # Serviços de API
    └── [modulo]/
        ├── index.ts       # Funções de API
        └── schemas/       # Schemas TypeScript (interfaces/types)

Padrões de Nomenclatura

Serviços de API

Schemas TypeScript

Componentes

Serviços de API

Serviços de API são funções que fazem chamadas HTTP para o backend.

Exemplo de Serviço

// services/analise-detalhada/index.ts
import { api, DotumResponse } from "@dot/core";
import { AnaliseDetalhadaResponse } from "./schemas/AnaliseDetalhadaResponse";

const URL_PATH = "/analise-detalhada";

/**
 * Lista todas as análises detalhadas com filtro opcional
 */
export async function listarAnalises(
    filtro?: string
): Promise<DotumResponse<AnaliseDetalhadaResponse[]>> {
    // Sempre envia o parâmetro filtro (mesmo que vazio) para o ApiServlet fazer o matching correto
    const res = await api.get<DotumResponse<AnaliseDetalhadaResponse[]>>(URL_PATH, {
        params: { filtro: filtro || "" },
    });

    return res.data;
}

Exemplo CORRETO de Schema (usando Zod):

// services/analise-detalhada/schemas/AnaliseDetalhadaResponse.ts
import { z } from "zod";

export const analiseDetalhadaResponseSchema = z.object({
    unidadeId: z.number(),
    unidadeDescricao: z.string(),
    suplementoTipoId: z.number(),
    suplementoTipoDescricao: z.string(),
    ultimoTratamento: z.string().nullable(),
    animaisTratados: z.number(),
    animaisTotal: z.number(),
    consumoIdeal: z.number(),
    percentualConsumoIdeal: z.number().nullable(),
    estoqueAtual: z.number(),
    statusEstoque: z.enum(["ZERADO", "BAIXO", "NORMAL"]),
    ultimaCompra: z.string().nullable(),
    sugestao: z.string(),
    vendedores: z.array(z.string()),
});

export type AnaliseDetalhadaResponse = z.infer<typeof analiseDetalhadaResponseSchema>;

Exemplo ERRADO de Schema (sem Zod):

// ❌ ERRADO: Não usar apenas interface ou type
export interface AnaliseDetalhadaResponse {
    unidadeId: number;
    unidadeDescricao: string;
    // ...
}

Componentes React

Componentes React são a interface visual da aplicação.

Exemplo de Componente

// app/(private)/analise-detalhada/components/analise-detalhada-table.tsx
"use client";

import { DotumTable, useDotumTable } from "@dot/ui/dotum-table";
import { listarAnalises } from "~/services/analise-detalhada";
import { AnaliseDetalhadaResponse } from "~/services/analise-detalhada/schemas/AnaliseDetalhadaResponse";

interface AnaliseDetalhadaTableProps {
    filtro: string;
}

export function AnaliseDetalhadaTable({ filtro }: AnaliseDetalhadaTableProps) {
    const table = useDotumTable<AnaliseDetalhadaResponse>({
        queryKey: ["analise-detalhada", filtro],
        queryFn: async () => {
            const filtroFinal = filtro === "TODOS" ? undefined : filtro;
            const res = await listarAnalises(filtroFinal);
            return res.data ?? ([] as AnaliseDetalhadaResponse[]);
        },
    });

    return (
        <DotumTable.Root {...table}>
            <DotumTable.Columns>
                <DotumTable.Column
                    label="Produtor/Fazenda"
                    name="unidadeDescricao"
                />
                <DotumTable.Column
                    label="Tipo de Nutriente"
                    name="suplementoTipoDescricao"
                    render={({ row }) => {
                        const data = row.original as AnaliseDetalhadaResponse;
                        return (
                            <div>
                                <div className="font-medium">{data.suplementoTipoDescricao}</div>
                                {data.ultimoTratamento && (
                                    <div className="text-sm text-muted-foreground">
                                        Último trato: {formatarData(data.ultimoTratamento)}
                                    </div>
                                )}
                            </div>
                        );
                    }}
                />
            </DotumTable.Columns>
        </DotumTable.Root>
    );
}

DotumTable

DotumTable é um componente de tabela genérico do @dot/ui.

⚠️ IMPORTANTE - Tipagem no DotumTable:

Exemplo CORRETO - Com type casting:

<DotumTable.Column
    label="Tipo de Nutriente"
    name="suplementoTipoDescricao"
    render={({ row }) => {
        // ✅ CORRETO: Type casting explícito quando precisa acessar múltiplas propriedades
        const data = row.original as AnaliseDetalhadaResponse;
        return (
            <div>
                <div className="font-medium">{data.suplementoTipoDescricao}</div>
                {data.ultimoTratamento && (
                    <div className="text-sm text-muted-foreground">
                        Último trato: {formatarData(data.ultimoTratamento)}
                    </div>
                )}
            </div>
        );
    }}
/>

Tipagem TypeScript

⚠️ REGRAS CRÍTICAS - TypeScript:

Hooks Customizados

Hooks customizados encapsulam lógica reutilizável.

Exemplo de Hook

// hooks/use-sidebar-data.ts
import { useQuery } from "@tanstack/react-query";
import { SidebarApi, staticSidebarDataResponse } from "~/services/sidebar";

export function useSidebarData() {
    const query = useQuery({
        queryKey: ["sidebar"],
        queryFn: SidebarApi.doDataSidebarByUser,
        placeholderData: staticSidebarDataResponse,
        initialData: staticSidebarDataResponse,
    });
    
    return query;
}

IA Core (Framework)

O ia-core é o framework Java interno usado pelo ia-boi-api. Ele fornece:

Framework Core

Exceções

Utilitários

Database (TransactionDB, DAOs)

ℹ️ IMPORTANTE: O ia-core é um framework interno e não deve ser editado diretamente. Se precisar de funcionalidades novas, discuta com a equipe antes de modificar.

N8N - Automações e Integrações

O N8N é usado para automações e integrações do IA BOI, especialmente para o chatbot via WhatsApp que utiliza inteligência artificial para processar mensagens dos usuários.

Visão Geral

O workflow principal "IABoi V2" é responsável por:

Workflow Principal: IABoi V2

Estrutura do Fluxo

1. WhatsApp Trigger → Recebe mensagem
2. Normaliza → Padroniza dados da mensagem
3. Auth → Autentica usuário na API
4. Get Context → Busca contexto do usuário (unidade, onboarding, etc.)
5. Tipo Mensagem → Identifica tipo (texto, áudio, interativa)
6. Processamento:
   - Texto: Direto para buffer
   - Áudio: Transcreve → Buffer
   - Interativa: Processa interação (botões, flows)
7. Agente IA → Processa mensagem e decide qual tool chamar
8. Tools → Executam workflows específicos (manejo, relatório, CRUD, etc.)
9. Resposta → Formata e envia via WhatsApp
10. Histórico → Registra mensagem no banco

Fluxo de Mensagens

Tipos de Mensagem Suportados

Processamento de Mensagens

// Nó: normaliza
- Extrai dados da mensagem do WhatsApp
- Cria systemKey único: "ia_boi_{{ remoteJid }}"
- Normaliza número de telefone, tipo de mensagem, etc.
- Prepara dados para processamento

Buffer de Mensagens

O sistema usa um buffer no Redis para agrupar múltiplas mensagens enviadas rapidamente:

Agente de IA

O agente de IA é o coração do sistema. Ele utiliza:

System Message (Prompt Principal)

O agente recebe um system message extenso que define:

Prompt de Resposta

O prompt de resposta define regras para formatação:

Ferramentas (Tools)

O agente de IA possui acesso a várias ferramentas que são, na verdade, workflows do n8n:

1. agente_manejo

2. agente_relatorio

3. agente_crud

4. agente_suplemento

5. agente_onboarding

6. limpar_base_dados

7. limpar_base_dados_codigo

Integrações

WhatsApp Business Cloud API

OpenAI API

Redis

IA BOI API

Memória e Buffer

Sistema de Memória (Redis Chat Memory)

A memória do agente é gerenciada pelo Redis:

Sistema de Buffer

O buffer agrupa mensagens enviadas rapidamente:

1. Mensagem chega → Adiciona ao buffer Redis (lista)
2. Verifica buffer → Se tem mais de 1 mensagem, aguarda 2 segundos
3. Timer → Após 2 segundos, busca todas as mensagens do buffer
4. Normaliza → Junta todas as mensagens em uma única string
5. Processa → Envia para o agente de IA
6. Limpa → Remove buffer após processamento

Tratamento de Erros

Erros de Autenticação

auth → qual_erro → parse_erro/parse_erro_externo → envia_mensagem_erro

Mensagem padrão: "🤖 Iaaa boi, estamos com uns probleminhas aqui 
para te atender agora, tenta de novo mais tarde"

Erros do Agente de IA

agente_ia → formata_erro → envia_mensagem

- Se erro contém "Received tool input did not match expected schema":
  "⚠️ Ocorreu um erro ao processar sua solicitação. 
   Por favor, revise os dados e tente novamente."
- Caso contrário: Retorna mensagem de erro padrão

Erros de Operação (Cancelamento)

normaliza_interacao → do_action → mensagem_historico_inativar

- Quando usuário cancela uma operação via flow
- Registra no histórico como "cancelar {tipo}"
- Envia mensagem de confirmação

Fluxos Especiais

1. Primeira Vez / Termos de Uso

verificacoes → Primeira vez:
  - Envia mensagem de boas-vindas
  - Envia vídeo tutorial
  - Envia flow de termos de uso

verificacoes → Aceitou termo uso:
  - Registra aceite
  - Continua fluxo normal

2. Tutorial

aceita_pula_tutorial → aceita_tutorial:
  - Envia mensagem: "Já preparei uma base..."
  - Registra início do tutorial
  - Envia exemplos de perguntas

aceita_pula_tutorial → pula_tutorial:
  - Registra que pulou tutorial
  - Envia flow de termos de uso

3. Cancelamento de Operações

is_flow → Switch → send_flow_whatsapp:
  - Detecta se agente chamou agente_manejo ou agente_suplemento
  - Envia resposta com flow interativo para cancelar
  - Flow: "iaboi_cancelar_manejo" ou "iaboi_cancelar_suplemento"

normaliza_interacao → do_action:
  - Processa cancelamento via DELETE na API
  - Endpoint: /manejo-inativar ou /suplemento

4. Limpeza de Dados

limpa_base_webhook → has_codigo:
  - Se não tem código: Gera código de 6 dígitos
  - Armazena no Redis com TTL 300s
  - Envia código para usuário
  
  - Se tem código: Busca código no Redis
  - Valida código
  - Se válido: Limpa dados e memória
  - Se inválido: Retorna erro

Boas Práticas N8N

⚠️ IMPORTANTE:

Fluxo de Dados

Backend → Frontend

  1. Frontend: Componente React chama serviço de API
  2. Serviço: Faz requisição HTTP usando api.get/post/delete do @dot/core
  3. Backend: Controller recebe requisição, chama BO ou DAO
  4. BO/DAO: Executa lógica de negócio ou busca dados
  5. Controller: Converte Bean para DTO e retorna
  6. Frontend: Recebe DotumResponse<DTO> e atualiza UI

Exemplo Completo

1. Backend - Controller

@ServiceClass
public class AnaliseDetalhadaController extends AbstractController {

    @Endpoint(path = "/analise-detalhada", method = "GET")
    public List<AnaliseDetalhadaDTO> selectAll(@QueryParam String filtro) throws Exception {
        TransactionDB trans = getTransaction();
        UserContext userContext = getUserContext();
        
        List<AnaliseDetalhadaDTO> resultado = AnaliseDetalhadaService
            .gerarAnaliseDetalhada(trans, userContext, filtro);
        
        return resultado;
    }
}

2. Backend - Service

public class AnaliseDetalhadaService {
    
    public static List<AnaliseDetalhadaDTO> gerarAnaliseDetalhada(
            TransactionDB trans, UserContext userContext, String filtro) throws Exception {
        // Lógica de negócio...
        return resultado;
    }
}

3. Frontend - Serviço de API

// services/analise-detalhada/index.ts
export async function listarAnalises(
    filtro?: string
): Promise<DotumResponse<AnaliseDetalhadaResponse[]>> {
    const res = await api.get<DotumResponse<AnaliseDetalhadaResponse[]>>(
        "/analise-detalhada",
        { params: { filtro: filtro || "" } }
    );
    return res.data;
}

4. Frontend - Componente

export function AnaliseDetalhadaTable({ filtro }: AnaliseDetalhadaTableProps) {
    const table = useDotumTable<AnaliseDetalhadaResponse>({
        queryKey: ["analise-detalhada", filtro],
        queryFn: async () => {
            const res = await listarAnalises(filtro);
            return res.data ?? [];
        },
    });

    return <DotumTable.Root {...table}>...</DotumTable.Root>;
}

Boas Práticas

IA BOI API

⚠️ REGRAS CRÍTICAS - NUNCA VIOLAR:
  1. Controllers NÃO devem fazer inserts/updates/deletes - Essas operações devem ser feitas SOMENTE em Business Objects (BOs)
  2. DAOs DEVEM capturar e tratar exceções - Retornam valores padrão (lista vazia, null, etc.) ao invés de propagar
  3. Services NÃO podem fazer consultas SQL diretas - Apenas DAOs podem fazer consultas SQL. Services devem apenas chamar métodos dos DAOs
  4. SEMPRE use UserContext para segurança e auditoria
  5. Use StringBuilder para queries complexas (apenas em DAOs)
  6. Mantenha indentação nas queries SQL e ORM
  7. Todos os DTOs devem usar Record do Java
  8. Todos os responses devem ser tipados como DTOs (não retornar Bean diretamente)

IA BOI Web

⚠️ REGRAS CRÍTICAS - NUNCA VIOLAR:

Histórico de Atualizações

13/01/2026 - Vinicius

📝 Criação completa da documentação do projeto IA BOI

Seções criadas:

🔧 Padrões estabelecidos:

Nota: Este documento deve ser atualizado conforme o projeto evolui e novos padrões são estabelecidos.