# 12 · Rumo à produção

Fonte: https://muriloht.com/idempiere/12-rumo-a-producao (Guia iDempiere, Murilo H. Torquato). Versão alvo: iDempiere 14.

Objetivos:
- Compilar o plugin como jar, fora do Eclipse, com Maven e Tycho
- Instalar e atualizar o plugin num servidor iDempiere
- Proteger as regras do plugin com testes automatizados
- Evitar os problemas de desempenho mais comuns em plugins
- Contribuir de volta com o projeto iDempiere

## Antes de começar

- Módulos 00 a 11 concluídos. O repositório `guia-idempiere-projeto` tem o plugin, os 2Packs de 1.0.0 a 1.0.7 e os scripts de migração.
- Para a parte 2, um segundo iDempiere 14 instalado a partir do instalador ou do Docker, fora do Eclipse. Serve de "produção" de teste.

## O que você vai construir

Até aqui, o plugin só existe dentro do Eclipse. Neste módulo ele vira software de verdade:

- um **jar** gerado por linha de comando, reproduzível em qualquer máquina e num servidor de integração contínua;
- **instalado** num iDempiere que nunca viu o seu Eclipse;
- com **testes** que rodam sem ninguém clicar em nada;
- e você fecha o guia do outro lado: **contribuindo** com o projeto que usou até aqui.

## Parte 1: o jar

### 1. O pom do plugin

Plugins do iDempiere compilam com **Tycho**, a extensão do Maven que entende OSGi: ele lê o `MANIFEST.MF` e resolve as dependências a partir de um repositório **p2**, e não do Maven Central. O repositório p2 do core é gerado pelo `./mvnw verify` que você rodou no módulo 00, em `org.idempiere.p2/target/repository`. É o mesmo mecanismo que o plugin REST usa.

Crie `com.gardenworld.visitas/pom.xml`:

```xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.gardenworld</groupId>
  <artifactId>com.gardenworld.visitas</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>eclipse-plugin</packaging>

  <properties>
    <tycho.version>4.0.8</tycho.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <idempiere.core.repository.url>file://${user.home}/sources/idempiere/org.idempiere.p2/target/repository</idempiere.core.repository.url>
  </properties>

  <repositories>
    <repository>
      <id>idempiere-core</id>
      <url>${idempiere.core.repository.url}</url>
      <layout>p2</layout>
    </repository>
  </repositories>

  <build>
    <plugins>
      <plugin>
        <groupId>org.eclipse.tycho</groupId>
        <artifactId>tycho-maven-plugin</artifactId>
        <version>${tycho.version}</version>
        <extensions>true</extensions>
      </plugin>
      <plugin>
        <groupId>org.eclipse.tycho</groupId>
        <artifactId>target-platform-configuration</artifactId>
        <version>${tycho.version}</version>
        <configuration>
          <executionEnvironment>JavaSE-17</executionEnvironment>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
```

Três detalhes que quebram o build se esquecidos:

- **A versão do pom acompanha a do manifesto.** `1.0.0-SNAPSHOT` no pom corresponde a `1.0.0.qualifier` no `Bundle-Version`. O Tycho recusa se forem diferentes.
- **`tycho.version` igual à do core** (a do `org.idempiere.parent/pom.xml`). Misturar versões gera erros difíceis de entender.
- **Tudo o que o jar precisa levar está no `build.properties`.** Confira que o `bin.includes` tem `META-INF/`, `OSGI-INF/` e `reports/` (módulo 07).

### 2. Compile

Use o mesmo Maven Wrapper do core, para não depender de um Maven instalado. Copie para a raiz do plugin:

```bash
cd ~/sources/guia-idempiere-projeto/com.gardenworld.visitas
cp ~/sources/idempiere/mvnw .
cp -r ~/sources/idempiere/.mvn .
./mvnw verify
```

O jar sai em `target/com.gardenworld.visitas-1.0.0-SNAPSHOT.jar`. Antes de instalar em qualquer lugar, confira o conteúdo:

```bash
unzip -l target/com.gardenworld.visitas-1.0.0-SNAPSHOT.jar | grep -E "OSGI-INF|2Pack|reports|MANIFEST"
```

Tem de aparecer: os arquivos de `OSGI-INF/` (as factories e o event manager), os `META-INF/2Pack_1.0.x.zip` e o `reports/VisitasPorTecnico.jrxml`. Faltou algo, volte ao `build.properties`. Este é o erro mais comum de quem sai do Eclipse: lá tudo funciona porque o Eclipse lê a pasta do projeto; o jar só leva o que foi declarado.

Dentro do jar, abra o `META-INF/MANIFEST.MF`: o `qualifier` virou uma data e hora (`1.0.0.202610151430`). Cada build gera uma versão maior que a anterior, e é assim que o OSGi sabe qual é a mais nova.

> No `.gitignore`, inclua `target/`. O jar é gerado, nunca versionado.

## Parte 2: instalar num servidor

### 3. Instale pelo Felix Web Console

Todo iDempiere tem um console web de administração do OSGi, o **Felix Web Console**, em:

```
https://seu-servidor:8443/osgi/system/console/bundles
```

Entre com um usuário que tenha o perfil **System Administrator** (no banco de demonstração, `SuperUser`). Na lista de bundles:

1. Clique em **Install/Update...**.
2. Escolha o jar, marque **Start Bundle** e confirme.
3. Procure `com.gardenworld.visitas` na lista. O status deve ser **Active**.

Não precisa reiniciar o servidor. Ao iniciar, o plugin aplica os 2Packs de `1.0.0` a `1.0.7`, em ordem (módulo 04). Confira como System na aba **Package Installation** da janela **Pack In**: uma linha por versão, todas com status de concluído. Depois, como GardenAdmin, rode **Role Access Update** e abra a **Visita Técnica**.

Para atualizar: gere um jar novo e repita **Install/Update...**. O OSGi troca a versão, e o 2Pack novo (se houver) é aplicado; os já aplicados não rodam de novo.

> Faça a primeira instalação num ambiente de homologação com uma cópia do banco de produção, nunca direto em produção. O 2Pack altera o Dicionário e o banco, e só uma cópia fiel mostra se algo conflita com as customizações que já existem lá.

### 4. Distribuindo para outras pessoas

Se o plugin for útil para outras empresas, há dois caminhos oficiais:

- **Repositório de extensões do iDempiere** (módulo 11): um `metadata.json` com o link do jar, enviado por pull request para o [idempiere-extension-repository](https://github.com/idempiere/idempiere-extension-repository). O plugin passa a aparecer no **Extension Management** de qualquer iDempiere.
- **Repositório p2 próprio**, publicado num servidor web, para instalação por linha de comando. É o que o plugin REST faz, com o script `update-rest-extensions.sh` do repositório dele.

## Parte 3: testes

### 5. Um projeto de testes

O core tem um framework de testes pronto no projeto `org.idempiere.test`. A classe `AbstractTestCase` faz o que nenhum teste do iDempiere pode esquecer: entra como GardenAdmin na GardenWorld antes de cada teste e **desfaz tudo (rollback)** depois. O banco fica como estava.

Crie um segundo plugin, `com.gardenworld.visitas.test` (mesmo assistente do módulo 04, sem ativador), com estas dependências no manifesto:

```
Require-Bundle: org.adempiere.base;bundle-version="14.0.0",
 org.idempiere.test;bundle-version="14.0.0",
 com.gardenworld.visitas
Import-Package: org.junit.jupiter.api;version="5.9.0"
```

O plugin de testes é separado para que o jar de produção não leve teste nem dependa do JUnit.

O plugin de testes só enxerga as classes que o plugin principal **exporta**. No `MANIFEST.MF` de `com.gardenworld.visitas`, acrescente:

```
Export-Package: com.gardenworld.visitas.model
```

Exporte só o que outro plugin precisa usar. O resto (factories, event handlers, formulário) continua interno.

E o teste:

```java
package com.gardenworld.visitas.test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.compiere.process.DocAction;
import org.compiere.util.Env;
import org.compiere.util.TimeUtil;
import org.idempiere.test.AbstractTestCase;
import org.idempiere.test.DictionaryIDs;
import org.junit.jupiter.api.Test;

import com.gardenworld.visitas.model.MVisita;
import com.gardenworld.visitas.model.X_EDU_VisitaLinha;

public class MVisitaTest extends AbstractTestCase {

    private MVisita novaVisita(boolean urgente) {
        MVisita visita = new MVisita(Env.getCtx(), 0, getTrxName());
        visita.setC_BPartner_ID(DictionaryIDs.C_BPartner.JOE_BLOCK.id);
        visita.setSalesRep_ID(DictionaryIDs.AD_User.GARDEN_ADMIN.id);
        visita.setEDU_DataVisita(TimeUtil.getDay(System.currentTimeMillis()));
        visita.setEDU_TipoVisita("MA");
        visita.setEDU_IsUrgente(urgente);
        visita.saveEx();
        return visita;
    }

    private void incluirLinha(MVisita visita) {
        X_EDU_VisitaLinha linha = new X_EDU_VisitaLinha(Env.getCtx(), 0, getTrxName());
        linha.set_ValueOfColumn("EDU_Visita_ID", visita.getEDU_Visita_ID());
        linha.set_ValueOfColumn("Line", 10);
        linha.set_ValueOfColumn("M_Product_ID", DictionaryIDs.M_Product.OAK.id);
        linha.set_ValueOfColumn("Qty", Env.ONE);
        linha.saveEx();
    }

    @Test
    public void naoCompletaSemLinhas() throws Exception {
        MVisita visita = novaVisita(false);

        boolean ok = visita.processIt(DocAction.ACTION_Complete);

        assertFalse(ok, "Visita sem linhas não pode completar");
        assertNotNull(visita.getProcessMsg(), "Deve explicar o motivo");
    }

    @Test
    public void completaComLinha() throws Exception {
        MVisita visita = novaVisita(false);
        incluirLinha(visita);

        boolean ok = visita.processIt(DocAction.ACTION_Complete);
        visita.saveEx();

        assertTrue(ok, visita.getProcessMsg());
        assertEquals(DocAction.STATUS_Completed, visita.getDocStatus(), "Status");
        assertTrue(visita.isEDU_IsConcluida(), "Concluída");
    }

    @Test
    public void urgenteAguardaAprovacao() throws Exception {
        MVisita visita = novaVisita(true);
        incluirLinha(visita);

        visita.processIt(DocAction.ACTION_Complete);

        assertEquals(DocAction.STATUS_InProgress, visita.getDocStatus(), "Urgente sem aprovação fica em processo");
    }
}
```

O que cada teste protege:

- **`naoCompletaSemLinhas`**: a regra que já mudou de lugar duas vezes (módulos 04, 08 e 09). Se alguém mexer de novo, o teste acusa.
- **`completaComLinha`**: o caminho feliz. Parece óbvio, até que uma mudança num event handler quebre o complete sem ninguém perceber.
- **`urgenteAguardaAprovacao`**: a regra de negócio mais cara de errar. Visita urgente completa sem aprovação é um problema de controle interno, não um bug visual.

Repare que nenhum teste abre tela: eles chamam a `MVisita` direto. É mais um motivo para as regras morarem nela.

Para rodar: em **Run > Run Configurations > JUnit Plug-in Test**, duplique a configuração `idempiere.unit.test` do core. Na aba **Test**, aponte para o projeto `com.gardenworld.visitas.test`. Na aba **Plug-ins**, marque os seus dois plugins. Rode: os três devem passar, contra o seu banco de desenvolvimento.

### 6. O que testar

Testar tudo não é o objetivo. Teste o que dói quando quebra:

| Teste | Exemplo no guia |
|---|---|
| Regra que protege dinheiro, estoque ou controle | Aprovação de urgentes |
| Regra que já mudou de lugar ou de forma | Visita precisa de linha |
| Integração com efeito externo | Notificação do módulo 11 (teste que monta o JSON certo) |
| Correção de bug | Todo bug corrigido ganha um teste que o reproduz |

## Parte 4: desempenho e diagnóstico

### 7. Os erros que mais pesam em produção

| Erro | Sintoma | Como evitar |
|---|---|---|
| Consulta dentro de laço (N+1) | Processo lento que piora a cada mês | Uma consulta com `IN` ou `JOIN`; ou carregue a lista uma vez antes do laço |
| Buscar o registro inteiro para ler um número | Lentidão em telas e callouts | `DB.getSQLValue` para um valor só; `Query.count()` ou `.match()` para existência |
| Consulta sem índice | Tudo lento com poucos usuários | Índice pelo Dicionário (módulo 08) nas colunas dos filtros frequentes |
| Transação longa | Usuários travados esperando | Processos longos em lotes; nunca espere resposta de outro sistema dentro da transação (módulo 11) |
| Ignorar `get_TrxName()` | Dados inconsistentes, travamentos | Toda consulta e todo `new MAlgo(...)` recebem a transação corrente |
| Ler configuração ou cadastro sem cache | Milhares de consultas iguais | `MSysConfig` e os `get()` estáticos do core já usam cache; para tabelas suas, `CCache` |
| `System.out.println` | Log perdido, sem nível | `CLogger`, com `log.fine()` para depuração e `log.warning()` para o que alguém precisa ver |

### 8. Encontrando uma consulta travada

Quando "o sistema travou", quase sempre é uma transação esperando outra. No PostgreSQL:

```sql
SELECT pid, state, wait_event_type, now() - query_start AS tempo, left(query, 120) AS consulta
FROM pg_stat_activity
WHERE datname = 'idempiere' AND state <> 'idle'
ORDER BY query_start;
```

As linhas com `wait_event_type = 'Lock'` estão esperando. A mais antiga sem espera é, em geral, a que segura o lock. Com o `pid` dela, a consulta mostra o que ela está fazendo e há quanto tempo.

> `pg_terminate_backend(pid)` derruba a sessão, mas é a última opção: desfaz o que ela fazia. Antes, descubra **por que** a transação está aberta. Em plugins, a causa mais comum é uma chamada externa ou um laço longo dentro da transação.

### 9. Medindo antes de otimizar

Não otimize por intuição. O JDK traz um profiler de graça, o **Java Flight Recorder**:

```bash
jcmd <pid-do-idempiere> JFR.start duration=120s filename=/tmp/idempiere.jfr
```

Reproduza a lentidão durante os dois minutos e abra o arquivo no **JDK Mission Control**. Ele mostra onde o tempo foi gasto: CPU, espera por banco ou por lock. VisualVM e JProfiler fazem o mesmo, com outras interfaces.

## Parte 5: contribuindo com o iDempiere

### 10. Do bug ao pull request

Você passou doze módulos lendo e usando o código do iDempiere. Quando encontrar um bug ou uma melhoria no core, o caminho é este (detalhes no `CONTRIBUTING.md` do repositório):

1. **Confira se já existe.** Procure no [JIRA do projeto](https://idempiere.atlassian.net/issues/) e reproduza num dos [servidores de teste](https://www.idempiere.org/test-sites), de preferência com a GardenWorld.
2. **Para melhoria, converse antes.** Proponha no [fórum](https://www.idempiere.org/forums) ou no [Mattermost](https://mattermost.idempiere.org/). Ticket de melhoria sem conversa prévia costuma ficar parado.
3. **Abra o ticket** no JIRA: título claro, passos para reproduzir, o esperado e o obtido. Você recebe um número, como `IDEMPIERE-1234`.
4. **Corrija num fork**, numa branch a partir da `master`. Se mexer no Dicionário, o script de migração gerado com o **Log Migration Script** (módulo 01) vai junto: troque `PlaceholderForTicket` no nome do arquivo pelo número do ticket e gere a versão Oracle também.
5. **Abra o pull request** com o ticket no título (`IDEMPIERE-1234 Descrição curta`) e preencha o checklist do template: testes, documentação, revisão própria.
6. **Responda à revisão.** Todo código do core é revisado por outros contribuidores. É a parte mais valiosa: você aprende com quem mantém o projeto há anos.

> Contribuir não é só código. Traduzir mensagens para o português, melhorar a documentação no wiki, reproduzir e detalhar bugs de outras pessoas e revisar pull requests também movem o projeto. E costumam ser a porta de entrada.

### 11. Commit final

```bash
cd ~/sources/guia-idempiere-projeto
git add .
git commit -m "Build com Tycho e testes da MVisita"
git tag m12
```

## Checkpoint

- [ ] O mvnw verify gera o jar do plugin, com OSGI-INF, os 2Packs e os relatórios dentro
- [ ] Instalei o jar num servidor pelo Felix Web Console e ele ficou ACTIVE
- [ ] Os 2Packs foram aplicados sozinhos no servidor
- [ ] Os três testes da MVisita passam
- [ ] Sei encontrar uma consulta travada no PostgreSQL
- [ ] Sei o caminho de um bug até um pull request no iDempiere

## Erros comuns

| Sintoma | Causa provável | Correção |
|---|---|---|
| `mvnw verify` não resolve `org.adempiere.base` | Repositório p2 do core não existe ou caminho errado | Rode `./mvnw verify` no core e confira `idempiere.core.repository.url` |
| Erro de versão entre pom e manifesto | `1.0.0-SNAPSHOT` × `1.0.0.qualifier` diferentes | Alinhe os dois |
| Jar sem `OSGI-INF` ou sem os 2Packs | Pasta fora do `bin.includes` | Passo 1 |
| Bundle fica **Installed** no servidor | Dependência que existe no Eclipse mas não no servidor (por exemplo, o plugin REST) | Clique no bundle no console: a página mostra a dependência que falta |
| 2Pack não aplicado no servidor | Plugin não iniciou, ou falhou no meio | Log do servidor e aba **Package Installation** |
| Teste passa sozinho e falha em conjunto | Teste dependendo de dado criado por outro | Cada teste cria o que precisa; o rollback limpa |
| Teste grava de verdade no banco | Objeto criado sem `getTrxName()` | Sempre a transação do `AbstractTestCase` |

## Desafio

Configure um workflow do GitHub Actions que, a cada push, baixa o core, roda `./mvnw verify` nele e depois no seu plugin, e publica o jar como artefato. Um pipeline de build que não depende da máquina de ninguém é o que separa um plugin de uma customização.

## Referência

### Antes de cada versão em produção

- [ ] Build limpo pelo `mvnw verify`, e não pelo Eclipse
- [ ] Testes passando
- [ ] 2Pack novo com versão maior que a anterior, e nenhum 2Pack antigo alterado
- [ ] Instalado e testado em homologação com cópia do banco de produção
- [ ] Backup do banco de produção antes de instalar
- [ ] Mudanças em objetos do core listadas no README do plugin (módulo 10)
- [ ] Tag no Git com a versão

### Para continuar

- [Documentação oficial do iDempiere](https://docs.idempiere.org/) e o [wiki](https://wiki.idempiere.org/)
- O próprio código do core: quando a documentação não responder, a classe `M` da tabela e os testes em `org.idempiere.test` respondem
- A comunidade no [Mattermost](https://mattermost.idempiere.org/) e no [fórum](https://www.idempiere.org/forums)

## Fim da trilha

Você começou sem nada instalado e terminou com um plugin que tem tabelas, janelas, regras de negócio, documento com aprovação, relatórios, integração, testes e build reproduzível. É o mesmo caminho de qualquer customização séria no iDempiere, do tamanho que for.

O próximo passo é seu: aplicar isso num problema real, e devolver ao projeto um pouco do que ele te deu.
