Estrutura Java

Estrutura Java

A estrutura de pacotes Java da Keeptor segue uma arquitetura Service-Oriented. A ideia central é simples: Services são o coração — todo o resto é entrada/saída.


Visão Geral

Existem dois tipos de repositórios no nosso desenvolvimento Java:

TipoDescriçãoRepositório (Git)Exemplo de Pacote Java
Projeto do ClienteCódigo específico de personalização/integração para um clientePOLYCROM, CLIENTE_XHWcom.keeptor.integracaosalesforce, com.keeptor.integracao_xhw
Utilitários (JAR)Framework interno compartilhado entre TODOS os projetosUltilitarios_kptcom.keeptor.utilitarios

ℹ️ Info: O utilitarios é um repositório Git separado que gera um JAR compartilhado. Todos os projetos de clientes importam esse JAR como dependência. Isso garante que código comum (base classes, serviços genéricos) fique centralizado e não seja duplicado.

Nomenclatura: Repositório vs Pacote Java

⚠️ Atenção: O nome do repositório e o nome do pacote Java são coisas diferentes. Confundir os dois é um erro comum.

Repositório (Git)Pacote Java (src/com/keeptor/)
RepresentaO clienteA integração/funcionalidade sendo desenvolvida
ExemploPOLYCROMintegracaosalesforce
Quem defineNome do cliente na KeeptorO que está sendo desenvolvido para o cliente
Pode ter vários?Não — 1 repositório por clienteSim — 1 repositório pode ter N integrações

💡 Dica: O repositório POLYCROM (cliente Polycrom) contém o pacote com.keeptor.integracaosalesforce porque a integração sendo desenvolvida é com o Salesforce. Se no futuro o mesmo cliente precisar de uma integração com outro sistema, um novo pacote (ex: com.keeptor.integracao_totvs) é criado no mesmo repositório.


Criando um Novo Projeto

💡 Dica: Todo novo projeto deve partir do template oficial. Isso garante que a estrutura de pastas, as classes exemplo, os scripts de build e o CLAUDE.md já estejam configurados corretamente.

Passo 1 — Clone o template

git clone https://github.com/Keeptor-Gestao-e-Tecnologia/keeptor-java-template.git NOME_DO_CLIENTE
cd NOME_DO_CLIENTE

Ou use o botão "Use this template" diretamente no GitHub: keeptor-java-template

Passo 2 — Execute o setup

./setup.sh

O script vai pedir:

  • Nome do cliente (ex: POLYCROM) — define o nome do repositório e do JAR
  • Nome da integração (ex: integracaosalesforce) — define o pacote Java

Ele renomeia automaticamente todos os placeholders, pastas e packages.

Passo 3 — Comece a codar

Duplique os arquivos Exemplo*.java da camada desejada, renomeie com o sufixo correto e implemente sua lógica:

ArquivoDuplique paraSufixo
ExemploService.javaClienteService.javaService
ExemploAction.javaSincronizarAction.javaAction
ExemploEvento.javaCadastroEvento.javaEvento
ExemploJob.javaSincronizarJob.javaJob
ExemploRegra.javaValidarDescontoRegra.javaRegra

Cada arquivo exemplo contém Javadoc com todos os métodos disponíveis da interface Sankhya.

Passo 4 — Compile

./build.sh

Gera o JAR na raiz do projeto. Requer /rep-bibliotecasJava/ com as bibliotecas Sankhya.

ℹ️ Info: O template já inclui: .classpath, .project, .gitignore, build.sh, e um CLAUDE.md pré-configurado para Claude Code.


Estrutura de um Projeto de Cliente

Cada projeto de cliente segue esta hierarquia:

NOME_DO_CLIENTE/                        ← Repositório Git (nome do CLIENTE)
│
└── src/com/keeptor/
    │
    └── [nome-da-integracao]/           ← Pacote Java (nome da INTEGRAÇÃO)
        ├── actionbutton/               ← Ações disparadas por botões (AcaoRotinaJava)
        ├── eventos/                    ← Handlers de persistência (EventoProgramavelJava)
        ├── jobs/                       ← Tarefas agendadas (ScheduledAction)
        ├── regrasnegocio/              ← Regras comerciais/financeiras (RegraNegocioJava)
        └── services/                   ← Serviços de negócio (lógica pura)

⚠️ Atenção: O [nome-da-integracao] NÃO é o nome do cliente. É o nome da integração ou funcionalidade sendo desenvolvida. Ex: integracaosalesforce, integracao_xhw, customizacao_fiscal.

Exemplo real

Repositório CLIENTE_XHW (cliente) com a integração integracao_xhw (funcionalidade):

CLIENTE_XHW/                            ← Repositório (cliente)
│
└── src/com/keeptor/
    │
    └── integracao_xhw/                 ← Pacote (integração)
        ├── actionbutton/
        │   ├── SincronizarClientesAction.java
        │   └── ImportarPedidosAction.java
        │
        ├── eventos/
        │   ├── PedidoConfirmadoEvento.java
        │   └── NotaFiscalEmitidaEvento.java
        │
        ├── jobs/
        │   ├── SincronizarEstoqueJob.java
        │   └── EnviarPedidosPendentesJob.java
        │
        ├── regrasnegocio/
        │   ├── ValidarDescontoRegra.java
        │   └── ValidarEstoqueRegra.java
        │
        └── services/
            ├── ClienteIntegracaoService.java
            ├── PedidoIntegracaoService.java
            └── EstoqueIntegracaoService.java

Exemplo com múltiplas integrações

Um mesmo repositório de cliente pode conter várias integrações:

POLYCROM/                               ← Repositório (cliente Polycrom)
│
└── src/com/keeptor/
    │
    ├── integracaosalesforce/           ← Integração com Salesforce
    │   ├── actionbutton/
    │   ├── eventos/
    │   ├── jobs/
    │   ├── regrasnegocio/
    │   └── services/
    │
    └── integracao_totvs/               ← Integração com TOTVS (futuro)
        ├── actionbutton/
        ├── eventos/
        ├── jobs/
        ├── regrasnegocio/
        └── services/

Estrutura do Utilitários (JAR Compartilhado)

O repositório utilitarios tem sua própria estrutura e gera um JAR que todos os projetos consomem:

src/com/keeptor/
│
└── utilitarios/
    ├── base/                   ← Classes abstratas que todos os projetos estendem
    │   ├── BaseService.java
    │   ├── BaseActionButton.java
    │   ├── BaseEvento.java
    │   └── BaseJob.java
    │
    └── services/               ← Serviços genéricos compartilhados (CORE)
        ├── LogService.java
        ├── EmailService.java
        ├── ArquivoService.java
        └── AuditoriaService.java

📦 Por que um JAR separado?

  1. Não duplicar código: Lógica comum (log, email, auditoria) escrita uma vez, usada em todos os projetos
  2. Versionamento: Atualizar o JAR atualiza todos os projetos que o usam
  3. Padronização: Classes base (BaseService, BaseActionButton, etc.) garantem que todo projeto segue a mesma estrutura
  4. Onboarding rápido: Novo dev entende a estrutura uma vez e sabe navegar qualquer projeto

As 5 Camadas

💡 Regra de Ouro: Services NUNCA chamam ActionButtons, Eventos, Jobs ou Regras diretamente. O fluxo é sempre de fora para dentro. Nunca o inverso.

Interfaces Sankhya (SankhyaW-extensions.jar)

Cada camada implementa uma interface oficial do Sankhya. Estas são as assinaturas reais extraídas do JAR:

CamadaInterface SankhyaPacoteMétodo(s)
ActionButtonAcaoRotinaJavabr.com.sankhya.extensions.actionbuttondoAction(ContextoAcao)
EventoEventoProgramavelJavabr.com.sankhya.extensions.eventoprogramavel7 métodos (before/after CRUD + beforeCommit)
JobScheduledActionorg.cuckoo.coreonTime(ScheduledActionContext)
Regra de NegócioRegraNegocioJavabr.com.sankhya.extensions.regrasnegocioexecuta(ContextoRegra)
Service(classe Java pura)Classe simples com lógica de negócio

1. Services — O Coração ❤️

Pacote: [integracao]/services/

Responsabilidade: Toda a lógica de negócio vive aqui. Services são o ponto central — todas as outras camadas chamam Services para executar operações.

Quando criar um Service:

  • Para cada domínio de negócio do projeto
  • Quando a lógica será reutilizada por múltiplos pontos de entrada
  • Quando precisa de transação ou coordenação entre entidades
package com.keeptor.integracao_xhw.services;

import java.math.BigDecimal;

public class PedidoIntegracaoService {

    private final EstoqueIntegracaoService estoqueService;

    public Pedido importarPedido(DadosPedidoExterno dados) throws Exception {
        validarDados(dados);
        estoqueService.validarDisponibilidade(dados.getItens());
        Pedido pedido = montarPedido(dados);
        return salvar(pedido);
    }
}

📝 Nota: Um Service pode chamar outro Service. PedidoIntegracaoService chama EstoqueIntegracaoService — isso é normal e esperado.

2. ActionButtons — Entrada do Usuário 🖱️

Pacote: [integracao]/actionbutton/ · Interface: implements AcaoRotinaJava

Responsabilidade: Ações disparadas por botões da interface. Quando o usuário clica em um botão de ação na tela, é um ActionButton que recebe essa ação.

Método obrigatório: doAction(ContextoAcao contexto)

ContextoAcao fornece:

  • getLinhas() → Registros selecionados pelo usuário
  • getLinhaPai() → Registro pai (cabeçalho)
  • getUsuarioLogado() → BigDecimal do código do usuário
  • getParam(String) → Parâmetro enviado pela tela
  • getParametroSistema(String) → Parâmetro do sistema Sankhya
  • setMensagemRetorno(String) → Feedback para o usuário
  • confirmarSimNao(String, String, int) → Diálogo de confirmação
  • novaLinha() / novaLinha(String) → Criar novo registro

Registro fornece: getCampo(String), setCampo(String, Object), save(), remove()

package com.keeptor.integracao_xhw.actionbutton;

import br.com.sankhya.extensions.actionbutton.AcaoRotinaJava;
import br.com.sankhya.extensions.actionbutton.ContextoAcao;
import br.com.sankhya.extensions.actionbutton.Registro;
import java.math.BigDecimal;

public class SincronizarClientesAction implements AcaoRotinaJava {

    @Override
    public void doAction(ContextoAcao contexto) throws Exception {
        Registro[] linhas = contexto.getLinhas();
        ClienteIntegracaoService service = new ClienteIntegracaoService();

        for (Registro linha : linhas) {
            BigDecimal codParc = (BigDecimal) linha.getCampo("CODPARC");
            service.sincronizar(codParc);
        }

        contexto.setMensagemRetorno(linhas.length + " clientes sincronizados");
    }
}

💡 Dica: ActionButton é fino. 10-30 linhas no máximo. Se está ficando grande, a lógica deveria estar no Service.

⚠️ Atenção — Transação: Actions usam a transação gerenciada pelo Sankhya. NÃO crie nova transação JDBC — isso causa o erro "Já existe uma transação em andamento".

3. Eventos — Gatilhos de Persistência 🔔

Pacote: [integracao]/eventos/ · Interface: implements EventoProgramavelJava

Responsabilidade: Interceptam operações de persistência em tabelas do Sankhya. Diferente de ActionButtons (disparados pelo usuário), Eventos são disparados automaticamente em insert/update/delete.

7 métodos obrigatórios (todos recebem PersistenceEvent):

MétodoQuando executa
beforeInsertAntes de inserir registro
beforeUpdateAntes de atualizar registro
beforeDeleteAntes de deletar registro
afterInsertApós inserir registro
afterUpdateApós atualizar registro
afterDeleteApós deletar registro
beforeCommitAntes do commit (recebe TransactionContext)

PersistenceEvent fornece: getVo() → DynamicVO do registro sendo manipulado

package com.keeptor.integracao_xhw.eventos;

import br.com.sankhya.extensions.eventoprogramavel.EventoProgramavelJava;
import br.com.sankhya.jape.event.PersistenceEvent;
import br.com.sankhya.jape.event.TransactionContext;
import br.com.sankhya.jape.vo.DynamicVO;
import java.math.BigDecimal;

public class PedidoConfirmadoEvento implements EventoProgramavelJava {

    @Override
    public void beforeInsert(PersistenceEvent evento) throws Exception { }

    @Override
    public void beforeUpdate(PersistenceEvent evento) throws Exception { }

    @Override
    public void beforeDelete(PersistenceEvent evento) throws Exception { }

    @Override
    public void afterInsert(PersistenceEvent evento) throws Exception {
        DynamicVO vo = (DynamicVO) evento.getVo();
        BigDecimal nunota = vo.asBigDecimalOrZero("NUNOTA");
        // Delegar para Service
        PedidoIntegracaoService service = new PedidoIntegracaoService();
        service.enviarParaSistemaExterno(nunota);
    }

    @Override
    public void afterUpdate(PersistenceEvent evento) throws Exception { }

    @Override
    public void afterDelete(PersistenceEvent evento) throws Exception { }

    @Override
    public void beforeCommit(TransactionContext contexto) throws Exception { }
}

📝 Nota: Implemente todos os 7 métodos, mesmo que vazios. Use apenas os que precisa e deixe os demais com corpo vazio.

4. Jobs — Tarefas Agendadas ⏰

Pacote: [integracao]/jobs/ · Interface: implements ScheduledAction (org.cuckoo.core)

Responsabilidade: Tarefas que rodam em background, geralmente agendadas (cron) ou disparadas por filas. Processamento pesado ou operações que não precisam de resposta imediata.

Método obrigatório: onTime(ScheduledActionContext contexto)

ScheduledActionContext fornece:

  • log(String) → Registrar log de execução
  • info(String) → Informação de execução
  • getSchedulerRuntime() → Runtime do scheduler
  • getJobMetadata() → Metadados do job
package com.keeptor.integracao_xhw.jobs;

import org.cuckoo.core.ScheduledAction;
import org.cuckoo.core.ScheduledActionContext;

public class SincronizarEstoqueJob implements ScheduledAction {

    @Override
    public void onTime(ScheduledActionContext contexto) {
        try {
            EstoqueIntegracaoService service = new EstoqueIntegracaoService();
            int sincronizados = service.sincronizarCompleto();
            contexto.log(sincronizados + " produtos sincronizados");
        } catch (Exception e) {
            contexto.log("Erro: " + e.getMessage());
        }
    }
}

📝 Nota: Jobs são orquestradores. Definem QUANDO executar e COMO lidar com erros, mas a lógica é do Service.

⚠️ Atenção — Transação: Jobs devem criar própria transação JDBC.

5. Regras de Negócio — Validações Comerciais/Financeiras ⚖️

Pacote: [integracao]/regrasnegocio/ · Interface: implements RegraNegocioJava

Responsabilidade: Regras executadas automaticamente pelo motor de regras do Sankhya em operações financeiras e comerciais (notas, títulos, etc). Validam, bloqueiam ou liberam operações.

Método obrigatório: executa(ContextoRegra contexto)

ContextoRegra fornece:

  • getNunota() → Número único da nota
  • getSequencia() → Sequência do item
  • getUsuarioLogado() → BigDecimal do código do usuário
  • getJdbcWrapper() → Acesso direto ao JDBC
  • getQuery() → QueryExecutor para consultas
  • getParametroSistema(String) → Parâmetro do sistema Sankhya
  • setSucesso(boolean) → Define se a regra passou ou bloqueou
  • setMensagem(String) → Mensagem de retorno ao usuário
  • setCodUsuLib(Number) → Código do usuário que pode liberar a operação bloqueada
  • mostraErro(String) → Exibe erro e interrompe a operação
  • eMail(String, String, String) → Enviar e-mail de notificação
package com.keeptor.integracao_xhw.regrasnegocio;

import br.com.sankhya.extensions.regrasnegocio.ContextoRegra;
import br.com.sankhya.extensions.regrasnegocio.RegraNegocioJava;
import java.math.BigDecimal;

public class ValidarDescontoRegra implements RegraNegocioJava {

    @Override
    public void executa(ContextoRegra contexto) throws Exception {
        BigDecimal nunota = contexto.getNunota();

        // Delegar validação complexa para Service
        DescontoService service = new DescontoService();
        boolean valido = service.validarDesconto(nunota);

        if (!valido) {
            contexto.setSucesso(false);
            contexto.setMensagem("Desconto acima do permitido para este cliente");
            contexto.setCodUsuLib(BigDecimal.ZERO); // Supervisor pode liberar
            return;
        }

        contexto.setSucesso(true);
        contexto.setMensagem("Desconto validado");
    }
}

📝 Nota: Use setSucesso(false) + setMensagem() para bloquear a operação. Use setCodUsuLib() quando a operação pode ser liberada por um supervisor.


Fluxo de Chamadas

 ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
 │  INTERFACE    │  │ PERSISTÊNCIA │  │   CRON/FILA  │  │ MOTOR REGRAS │
 │  (Tela)      │  │  (Tabelas)   │  │  (Agendador) │  │  (Comercial) │
 └──────┬───────┘  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘
        │                 │                  │                  │
        ▼                 ▼                  ▼                  ▼
 ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
 │ ActionButton │  │   Evento     │  │     Job      │  │    Regra     │
 │ AcaoRotina   │  │ EventoProg.  │  │ Scheduled    │  │ RegraNegocio │
 │    Java      │  │    Java      │  │   Action     │  │    Java      │
 └──────┬───────┘  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘
        │                 │                  │                  │
        └─────────────────┼──────────────────┼──────────────────┘
                          │                  │
                          ▼                  │
              ┌───────────────────────┐      │
              │      SERVICES         │◄─────┘
              │   (Lógica de Negócio) │
              │      ★ CORAÇÃO ★      │
              └───────────┬───────────┘
                          │
                          ▼
              ┌───────────────────────┐
              │    BANCO DE DADOS     │
              └───────────────────────┘

A regra é clara: ActionButton, Evento, Job e Regra são pontos de entrada. Eles recebem a demanda e delegam para o Service. O Service é quem faz o trabalho.


Convenções de Nomenclatura

Classes

TipoSufixoInterface SankhyaExemplo
ServiceServiceClasse Java puraPedidoIntegracaoService, EstoqueService
Action ButtonActionimplements AcaoRotinaJavaSincronizarClientesAction, ImportarPedidosAction
EventoEventoimplements EventoProgramavelJavaPedidoConfirmadoEvento, NotaEmitidaEvento
JobJobimplements ScheduledActionSincronizarEstoqueJob, EnviarPedidosJob
Regra de NegócioRegraimplements RegraNegocioJavaValidarDescontoRegra, ValidarEstoqueRegra

Métodos

AçãoPrefixoExemplo
Buscar umbuscarbuscarPorId(Long id)
Listar várioslistarlistarPorStatus(String status)
CriarcriarcriarPedido(DadosPedido dados)
AtualizaratualizaratualizarStatus(Long id, String status)
RemoverremoverremoverItem(Long itemId)
ValidarvalidarvalidarDados(DadosPedido dados)
CalcularcalcularcalcularTotal(List itens)
SincronizarsincronizarsincronizarClientes(Long empresaId)
EnviarenviarenviarParaSistemaExterno(Long id)
ImportarimportarimportarPedido(DadosExternos dados)

Variáveis e Constantes

// Variáveis: camelCase
String nomeCliente;
BigDecimal valorTotal;
List<ItemPedido> itensPedido;

// Constantes: UPPER_SNAKE_CASE
public static final int MAX_TENTATIVAS = 3;
public static final String STATUS_APROVADO = "APROVADO";
public static final int TIMEOUT_SEGUNDOS = 30;

// Pacotes: sempre minúsculo, underscore para separar palavras
package com.keeptor.integracao_xhw.services;
package com.keeptor.customizacao_abc.actionbutton;

Erros Comuns

⚠️ Atenção: Revise esta tabela antes de submeter código. Estes são os erros mais frequentes encontrados em code reviews.

ErradoCorretoPor quê
Lógica de negócio no ActionButtonLógica no Service, ActionButton só delegaService é reutilizável, ActionButton não
Service chamando ActionButtonActionButton chama ServiceFluxo sempre de fora para dentro
Job com lógica complexa inlineJob chama ServiceMesma lógica pode ser usada por ActionButton
Classe com 500+ linhasDividir em múltiplos ServicesManutenibilidade e responsabilidade única
Duplicar código que já existe no JAR utilitariosImportar do JARPara isso o JAR existe
Nome genérico: Manager, Helper, ProcessorNome específico: ClienteIntegracaoServiceClareza sobre o que faz
Criar projeto sem os 5 pacotesSempre criar actionbutton/, eventos/, jobs/, regrasnegocio/, services/Mesmo que fiquem vazios inicialmente, mantém a estrutura
Usar nome do cliente como pacote Java: com.keeptor.polycromUsar nome da integração: com.keeptor.integracaosalesforceO repositório é o cliente, o pacote é a integração