⚡ ASYCUDA Cloud Framework v5 (ACF) • Quarkus Monorepo

Criação de Módulos Backend no ASY5

Manual técnico exaustivo e profissional para desenvolvimento de novos serviços de negócio de raiz, consolidando os padrões do módulo de Inspeção Aduaneira (angola/inspection) e dos módulos centrais da plataforma (assignment, declaration, accounting).

01 Visão Geral da Arquitetura ASY5

O backend do ASY5 não é uma aplicação monolítica convencional nem um conjunto de microsserviços desacoplados via REST comum. Ele opera sobre o ASYCUDA Cloud Framework (ACF) em cima do ecossistema Quarkus, estruturado como um monorepo gerido por Gradle Kotlin DSL.

O padrão primordial do sistema é o Document Workflow Engine. Um documento aduaneiro (Declaração, Exit Note, Agendamento de Inspeção, Liquidação) é modelado como uma máquina de estados estrita, em que:

🔒 Segurança por Design

O acesso não é gerido por rotas HTTP simples, mas sim por @Unit e @Role vinculados ao SSO/Keycloak, combinados com @DocumentRestriction em tempo de execução para estâncias e NIFs.

⚙️ Geração em Compilação

Anotações como @AutoModel e @Document disparam processadores de código que constroem DTOs, mappers, interfaces de fluxo e atividades antes mesmo da compilação dos serviços.

📦 Fluxos Reutilizáveis

As operações são encapsuladas em FlowInstance encadeando classes unitárias de validação (DocumentValidationActivity), garantindo manutenibilidade e isolamento de testes.

02 Anatomia Completa de um Módulo

Os módulos residem em diretórios definidos pelo escopo de domínio:

Subcamada Nome Gradle Convenção Plugin Gradle Responsabilidade Principal
raiz (Aplicação) :angola:inspection plugins-as-application Ponto de arranque Quarkus, injeção de dependências CDI, rotas REST, consumidores Kafka.
dm :angola:inspection:inspection-dm plugins-as-library Modelos puros de domínio, enums agnósticos, DTOs partilháveis sem dependências pesadas.
entities :angola:inspection:inspection-entities plugins-as-library Entidades JPA estendendo DocumentEntity, anotações @AutoModel para gerar DTOs.
spec :angola:inspection:inspection-spec plugins-as-library Contrato funcional: @Document, @Status, @Operation, @Unit e @Role.
api :angola:inspection:inspection-api plugins-as-library Interfaces Java e clientes partilhados consumidos por outros módulos no monorepo.
grpc :angola:inspection:inspection-grpc plugins-as-proto Definições .proto e stubs gRPC de alta performance inter-módulo.
liquibase N/A (pasta de recursos) N/A Changelogs de base de dados em formato JSON versionado.

03 Matriz Comparativa: O Que Outros Módulos Nos Ensinam

Para além do módulo de Inspeção, outros módulos fornecem peças vitais de arquitetura:

Módulo Pontos Fortes & Padrões Únicos Quando Seguir Como Exemplo
angola/inspection Workflow com negociação de estados (Reagendamentos), validação de NIF OEA, restrições combinadas por estância e operador declarante, integração com contentores e itens. Para qualquer módulo que requer máquina de estados bidirecional com despachantes e técnicos, auditoria rigorosa e formulários com anexos.
customs/assignment Integração profunda de gRPC (assignment-grpc com Assignment.proto), escalonador de tarefas (useScheduler(true)), algoritmos puros desacoplados. Quando o seu módulo precisa de ser invocado sincronicamente em alta frequência por outros serviços (ex: cálculo de atribuição, quotas).
customs/declaration Integração abrangente com eventos Kafka (@Incoming("risk"), @Incoming("settlements")), relatórios Jasper, catálogo de tarifas. Para módulos reativos assíncronos que processam eventos emitidos por outros departamentos ou pelo motor de risco aduaneiro.
customs/accounting Liquidações complexas, regras financeiras estritas, reversão e cancelamento de transações. Para fluxos com operações financeiras e conciliações bancárias.

04 Passo a Passo Prático de Criação do Módulo

1
Estrutura de Diretórios e Gradle Build Files

Crie a árvore de diretórios (exemplo com o módulo customs/valuation ou angola/meumodulo):

Terminal (Criação de Pastas)
mkdir -p customs/meumodulo/{dm,entities,spec,api,grpc,liquibase/v1.0}
mkdir -p customs/meumodulo/src/main/{java/org/asycuda/cloud/customs/meumodulo,resources}
mkdir -p customs/meumodulo/src/test/java/org/asycuda/cloud/customs/meumodulo

Configure o customs/meumodulo/build.gradle.kts principal da aplicação:

customs/meumodulo/build.gradle.kts
plugins {
    id("org.asycuda.cloud.framework.plugins-as-application")
}

group = "org.asycuda.cloud.customs"
description = "ASYCUDA v5 Customs Valuation Module"

dependencies {
    asycuda.layers {
        useLayerDocumentWorkflow() // Ativa motor documental e anotações ACF
        useLayerGrasp()            // Regras de validação avançadas
        mergeLayers()
    }
    asycuda.modules {
        api(serialApi())           // Gerador de número de série oficial
    }
    asycuda.platforms {
        implementation(quarkusSmallryeReactiveMessagingKafka())
        implementation(quarkusSmallryeStork())
        implementation(smallryeStorkServiceDiscoveryStaticList())
    }

    // Submódulos locais do próprio módulo
    api(project(":customs:meumodulo:meumodulo-dm"))
    api(project(":customs:meumodulo:meumodulo-entities"))
    api(project(":customs:meumodulo:meumodulo-spec"))

    // Bibliotecas partilhadas do core ASY5
    api(project(":shared:data-model"))
    api(project(":shared:utils:decimals"))
    api(project(":core:catalogs")) {
        capabilities {
            requireCapability("org.asycuda.cloud.customs:catalogs-dm")
        }
    }
}

E configure o entities/build.gradle.kts e spec/build.gradle.kts com plugins-as-library:

customs/meumodulo/entities/build.gradle.kts
plugins {
    id("org.asycuda.cloud.framework.plugins-as-library")
}
group = "org.asycuda.cloud.customs"
description = "ASYCUDA v5 Customs Valuation - Entities"

dependencies {
    asycuda.layers {
        useLayerDocumentWorkflow()
        useConfigurator(true)
        mergeLayers()
    }
    api(project(":shared:data-model"))
    api(project(":customs:meumodulo:meumodulo-dm"))
}
ℹ️ Descoberta Automática do Gradle
Não precisa de editar o ficheiro settings.gradle.kts na raiz! O método getProjectPath detecta qualquer pasta que contenha um ficheiro build.gradle.kts válido.
2
Definição do Esquema e Migração Liquibase

O ASY5 padronizou as migrações em JSON compatíveis com Oracle e PostgreSQL:

customs/meumodulo/liquibase/master-changelog.json
{
  "databaseChangeLog": [
    {
      "include": {
        "file": "v1.0/001-VALUATION-init-schema.json",
        "relativeToChangelogFile": true
      }
    }
  ]
}
customs/meumodulo/liquibase/v1.0/001-VALUATION-init-schema.json
{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "INIT-VALUATION-1",
        "author": "ASY5-Team",
        "changes": [
          {
            "createTable": {
              "tableName": "VALUATION_DOCUMENT",
              "columns": [
                {
                  "column": {
                    "name": "INSTANCE_ID",
                    "type": "VARCHAR2(255 CHAR)",
                    "constraints": { "primaryKey": true, "nullable": false }
                  }
                },
                {
                  "column": {
                    "name": "DOCUMENT_STATUS",
                    "type": "VARCHAR2(50 CHAR)",
                    "constraints": { "nullable": false }
                  }
                },
                {
                  "column": {
                    "name": "DECLARATION_REF",
                    "type": "VARCHAR2(100 CHAR)"
                  }
                },
                {
                  "column": {
                    "name": "CUSTOMS_OFFICE",
                    "type": "VARCHAR2(10 CHAR)"
                  }
                },
                {
                  "column": {
                    "name": "DECLARANT_CODE",
                    "type": "VARCHAR2(50 CHAR)"
                  }
                },
                {
                  "column": {
                    "name": "CREATED_DATE",
                    "type": "TIMESTAMP(6)"
                  }
                }
              ]
            }
          }
        ]
      }
    }
  ]
}
3
Modelagem de Entidades com AutoModel

Defina o pacote de destino dos DTOs em ModelNames.java:

entities/src/main/java/org/asycuda/cloud/customs/meumodulo/entities/ModelNames.java
package org.asycuda.cloud.customs.meumodulo.entities;

public interface ModelNames {
  // O prefixo ":" avisa o processador de modelos a criar a classe neste pacote
  String PACKAGE = ":org.asycuda.cloud.customs.meumodulo.dtos";
  String M_CREATE = "create";
  String M_UPDATE = "update";
}

Crie a entidade que representa o documento estendendo DocumentEntity<String>:

entities/src/main/java/org/asycuda/cloud/customs/meumodulo/entities/ValuationDocumentEntity.java
package org.asycuda.cloud.customs.meumodulo.entities;

import jakarta.persistence.*;
import lombok.*;
import org.asycuda.cloud.framework.commons.modelgen.annotations.AutoModel;
import org.asycuda.cloud.framework.extensions.document.entities.DocumentEntity;
import org.asycuda.cloud.framework.extensions.document.annotations.Document.properties;
import org.asycuda.cloud.framework.extensions.metadata.annotations.MetadataModel;
import org.asycuda.cloud.framework.extensions.metadata.annotations.MetadataProperty;

import static org.asycuda.cloud.customs.meumodulo.entities.ModelNames.*;
import static org.asycuda.cloud.framework.commons.modelgen.annotations.AutoModel.Policy.EXCLUDE;

@Entity
@Table(name = "VALUATION_DOCUMENT")
@AttributeOverride(
    name = "id",
    column = @Column(name = "INSTANCE_ID", updatable = false, nullable = false))
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)

// GERAÇÃO AUTOMÁTICA DOS DTOs PELO FRAMEWORK:
@AutoModel(copyAnnotations = false, keys = {"id", "version"}, targetPackageName = PACKAGE)
@AutoModel.NewField(name = "id", type = String.class)
@AutoModel.NewField(name = "version", type = Integer.class)
@AutoModel(ref = M_CREATE, targetPackageName = PACKAGE, policy = EXCLUDE, root = true)
@AutoModel(ref = M_UPDATE, targetPackageName = PACKAGE, policy = EXCLUDE, root = true)

@MetadataModel(
    root = true,
    properties = {
        @MetadataProperty(key = properties.PROPERTY_DOCUMENT_TYPE, value = "valuation"),
        @MetadataProperty(key = properties.PROPERTY_DOCUMENT_OPERATIONS, value = "L|ALL,STORE,SUBMIT,VALIDATE,REJECT")
    })
public class ValuationDocumentEntity extends DocumentEntity<String> {

    @Column(name = "DECLARATION_REF", nullable = false)
    private String declarationReference;

    @Column(name = "CUSTOMS_OFFICE", nullable = false)
    private String customsOffice;

    @Column(name = "DECLARANT_CODE", nullable = false)
    private String declarantCode;

    @Column(name = "ASSESSED_VALUE")
    private Double assessedValue;
}
4
Especificação Documental (spec) & Máquina de Estados

Na camada spec, declaram-se os estados, operações, perfis de segurança e o gestor documental.

spec/src/main/java/org/asycuda/cloud/customs/meumodulo/spec/ValuationSecurityDefinition.java
package org.asycuda.cloud.customs.meumodulo.spec;

import org.asycuda.cloud.framework.extensions.security.annotations.ConnectId;
import org.asycuda.cloud.framework.extensions.security.annotations.Role;
import org.asycuda.cloud.framework.extensions.security.annotations.Unit;

import static org.asycuda.cloud.customs.meumodulo.spec.ValuationSecurityDefinition.*;

@Unit(
    value = "valuation-officer",
    description = "Oficial de Valoração Aduaneira",
    roles = { VIEWING, PROCESSING, VALIDATING }
)
@Unit(
    value = "valuation-declarant",
    description = "Despachante que submete pedidos",
    roles = { VIEWING, SUBMITTING }
)
@ConnectId
public interface ValuationSecurityDefinition {
  @Role("Pesquisa e leitura de documentos")
  String VIEWING = "valuation-viewing";

  @Role("Submissão de declarações para valoração")
  String SUBMITTING = "valuation-submitting";

  @Role("Trabalho técnico de verificação")
  String PROCESSING = "valuation-processing";

  @Role("Aprovação e liquidação")
  String VALIDATING = "valuation-validating";
}
spec/src/main/java/org/asycuda/cloud/customs/meumodulo/spec/ValuationStatus.java
package org.asycuda.cloud.customs.meumodulo.spec;

import org.asycuda.cloud.framework.extensions.document.annotations.Status;
import org.asycuda.cloud.framework.extensions.document.annotations.Status.StartStatus;

@Status
public enum ValuationStatus {
  @StartStatus
  NIL,        // Estado inicial antes do primeiro save
  STORED,     // Rascunho
  SUBMITTED,  // Submetido para análise
  IN_REVIEW,  // Em análise pelo oficial
  APPROVED,   // Valor aprovado
  REJECTED    // Rejeitado
}
spec/src/main/java/org/asycuda/cloud/customs/meumodulo/spec/ValuationOperation.java
package org.asycuda.cloud.customs.meumodulo.spec;

import org.asycuda.cloud.customs.meumodulo.dtos.ValuationDocumentDtoView;
import org.asycuda.cloud.framework.extensions.document.annotations.Operation;
import org.asycuda.cloud.framework.extensions.document.annotations.OperationSecurity;

import static org.asycuda.cloud.framework.extensions.document.components.DocumentOperation.OperationType.CREATE;
import static org.asycuda.cloud.framework.extensions.document.components.DocumentOperation.OperationType.UPDATE;
import static org.asycuda.cloud.customs.meumodulo.spec.ValuationSecurityDefinition.*;

public enum ValuationOperation {
  @Operation(
      ordinal = 1,
      type = CREATE,
      alias = "submit",
      sourceStatus = "NIL",
      destinationStatus = "SUBMITTED",
      startOperation = "NEW",
      security = @OperationSecurity(roles = {SUBMITTING}),
      icon = "$acf-granted",
      output = ValuationDocumentDtoView.class,
      summary = "Submeter pedido de valoração"
  )
  SUBMIT,

  @Operation(
      ordinal = 2,
      type = UPDATE,
      alias = "approve",
      sourceStatus = "SUBMITTED",
      destinationStatus = "APPROVED",
      startOperation = "RETRIEVE",
      security = @OperationSecurity(roles = {VALIDATING}),
      icon = "$acf-done",
      output = ValuationDocumentDtoView.class,
      summary = "Aprovar valoração"
  )
  APPROVE
}
spec/src/main/java/org/asycuda/cloud/customs/meumodulo/spec/ValuationDocument.java
package org.asycuda.cloud.customs.meumodulo.spec;

import jakarta.persistence.EntityManager;
import org.asycuda.cloud.customs.meumodulo.dtos.ValuationDocumentDtoView;
import org.asycuda.cloud.customs.meumodulo.entities.ValuationDocumentEntity;
import org.asycuda.cloud.framework.extensions.document.UuidHibernateDocumentManager;
import org.asycuda.cloud.framework.extensions.document.annotations.Document;
import org.asycuda.cloud.framework.extensions.document.annotations.DocumentSecurity;
import org.asycuda.cloud.framework.extensions.document.annotations.DocumentTracker;

import static org.asycuda.cloud.customs.meumodulo.spec.ValuationSecurityDefinition.VIEWING;

@Document(
    module = "valuation",
    document = "valuationdoc",
    name = "valuation",
    description = "Gestor documental de valoração aduaneira",
    gwEnums = {ValuationOperation.class, ValuationStatus.class},
    useShortSubselect = true,
    security = @DocumentSecurity(searchRoles = VIEWING, auditRoles = VIEWING, trackerRoles = VIEWING),
    tracker = @DocumentTracker
)
public class ValuationDocument extends UuidHibernateDocumentManager<
    ValuationStatus,
    ValuationStartOperation,
    ValuationOperation,
    ValuationDocumentEntity,
    ValuationContextData,
    ValuationDocumentDtoView> {

  public ValuationDocument(EntityManager entityManager) {
    super(entityManager);
  }

  @Override
  protected void declareTypes(
      ValuationStatus status,
      ValuationStartOperation startOperation,
      ValuationOperation operation,
      String id,
      ValuationDocumentEntity entity,
      ValuationContextData flowData,
      ValuationDocumentDtoView mappedEntity) {
  }
}
5
Compilação Inicial para Geração de Modelos

Antes de implementar a lógica de negócio, os processadores de anotações precisam gerar os ficheiros auxiliares:

Terminal (Executar na raiz do backend)
./gradlew :customs:meumodulo:meumodulo-entities:compileJava
./gradlew :customs:meumodulo:meumodulo-spec:compileJava
💡 O Que Foi Gerado Automaticamente?
  • ValuationDocumentDtoView, ValuationDocumentDtoCreate, ValuationDocumentDtoUpdate
  • ValuationDocumentFlowSupplier (interface para os Flows)
  • ValuationDocumentValidationActivity (interface para validações)
  • ValuationDocumentOperation com constantes das operações
6
Implementação de Fluxos e Atividades (Flows & Activities)

Crie uma classe Flow para cada operação documental:

src/main/java/org/asycuda/cloud/customs/meumodulo/flows/FlowSubmit.java
package org.asycuda.cloud.customs.meumodulo.flows;

import jakarta.inject.Singleton;
import org.asycuda.cloud.customs.meumodulo.activities.validation.ValidateDeclarationDetails;
import org.asycuda.cloud.customs.meumodulo.activities.validation.ValidateAssessedValue;
import org.asycuda.cloud.customs.meumodulo.dtos.ValuationDocumentDtoView;
import org.asycuda.cloud.customs.meumodulo.entities.ValuationDocumentEntity;
import org.asycuda.cloud.customs.meumodulo.spec.*;
import org.asycuda.cloud.framework.extensions.document.FlowInstance;
import org.asycuda.cloud.framework.extensions.document.annotations.Flow;

import static org.asycuda.cloud.customs.meumodulo.spec.ValuationStatus.SUBMITTED;
import static org.asycuda.cloud.customs.meumodulo.spec.constants.ValuationDocumentOperation.SUBMIT;

public class FlowSubmit implements ValuationDocumentFlowSupplier {

  @Flow(operation = SUBMIT)
  @Singleton
  public FlowInstance<
      ValuationStatus,
      ValuationStartOperation,
      ValuationOperation,
      String,
      ValuationDocumentEntity,
      ValuationContextData,
      ValuationDocumentDtoView> produceFlow() {

    return createFlow()
        .activity(ValidateDeclarationDetails.class)
        .activity(ValidateAssessedValue.class)
        .status(SUBMITTED)
        .end();
  }
}

E a atividade de validação correspondente:

src/main/java/org/asycuda/cloud/customs/meumodulo/activities/validation/ValidateAssessedValue.java
package org.asycuda.cloud.customs.meumodulo.activities.validation;

import org.asycuda.cloud.customs.meumodulo.dtos.ValuationDocumentDtoView;
import org.asycuda.cloud.customs.meumodulo.entities.ValuationDocumentEntity;
import org.asycuda.cloud.customs.meumodulo.spec.*;
import org.asycuda.cloud.customs.meumodulo.spec.activities.ValuationDocumentValidationActivity;
import org.asycuda.cloud.framework.extensions.core.exceptions.ASYCUDADomainException;
import org.asycuda.cloud.framework.extensions.document.context.DocumentContext;
import org.asycuda.cloud.framework.extensions.document.transactions.DocumentTransaction;

import static org.asycuda.cloud.framework.extensions.core.exceptions.models.ASYCUDADomainErrorDetail.errorPath;

public class ValidateAssessedValue implements ValuationDocumentValidationActivity {

  @Override
  public boolean validate(
      DocumentTransaction<
          ValuationStatus,
          ValuationStartOperation,
          ValuationOperation,
          String,
          ValuationDocumentEntity,
          ValuationContextData,
          ValuationDocumentDtoView> transaction) {

    var entity = transaction.entity();
    var ctx = transaction.ctx();

    if (entity.getAssessedValue() == null || entity.getAssessedValue() <= 0) {
      var error = errorPath("assessedValue").build();
      ctx.addError(new ASYCUDADomainException("O valor avaliado deve ser superior a zero.", error));
    }
    return true;
  }
}
7
Mapeamentos MapStruct com CDI

Converta DTOs em Entidades respeitando as convenções de injeção CDI e coleções JPA:

src/main/java/org/asycuda/cloud/customs/meumodulo/mappers/ValuationDocumentEntityMapper.java
package org.asycuda.cloud.customs.meumodulo.mappers;

import jakarta.persistence.EntityManager;
import org.asycuda.cloud.customs.meumodulo.dtos.*;
import org.asycuda.cloud.customs.meumodulo.entities.ValuationDocumentEntity;
import org.asycuda.cloud.framework.extensions.authentication.context.UserContext;
import org.mapstruct.*;
import org.mapstruct.MappingConstants.ComponentModel;
import org.mapstruct.control.DeepClone;

@Mapper(
    componentModel = ComponentModel.JAKARTA_CDI,
    builder = @Builder(disableBuilder = true),
    mappingControl = DeepClone.class
)
public interface ValuationDocumentEntityMapper {

  @Mapping(target = "id", ignore = true)
  void map(
      ValuationDocumentDtoCreate dto,
      @MappingTarget ValuationDocumentEntity entity,
      @Context EntityManager entityManager,
      @Context UserContext ctx);

  ValuationDocumentDtoView map(ValuationDocumentEntity entity);
  ValuationDocumentEntity mapEntity(ValuationDocumentDtoView view);
}
8
Restrições de Acesso Aduaneiras (@DocumentRestriction)

Isole os dados automaticamente entre estâncias aduaneiras e despachantes (como feito no módulo de Inspeção):

src/main/java/org/asycuda/cloud/customs/meumodulo/restrictions/RestrictionClaimOffice.java
package org.asycuda.cloud.customs.meumodulo.restrictions;

import org.asycuda.cloud.customs.meumodulo.dtos.ValuationDocumentDtoView;
import org.asycuda.cloud.customs.meumodulo.spec.*;
import org.asycuda.cloud.customs.meumodulo.spec.restrictions.ValuationDocumentClaimRestriction;
import org.asycuda.cloud.framework.extensions.document.annotations.DocumentRestriction;
import org.asycuda.cloud.framework.extensions.document.context.DocumentContext;

@DocumentRestriction(ValuationDocument.class)
public class RestrictionClaimOffice extends ValuationDocumentClaimRestriction {

  @Override
  public String name() { return "valuation-office-restriction"; }

  @Override
  public String claimName(DocumentContext<ValuationContextData> ctx) {
    return "office"; // Claim da estância no JWT
  }

  @Override
  public String searchPath(DocumentContext<ValuationContextData> ctx) {
    return "data.customsOffice"; // Campo indexado no Elasticsearch / Search Engine
  }
}
9
Comunicação Inter-Módulos: REST, gRPC & Kafka

O ASY5 suporta 3 modalidades de comunicação entre módulos:

A. Endpoints REST JAX-RS / OpenAPI adicionais

src/main/java/org/asycuda/cloud/customs/meumodulo/resources/ValuationResource.java
@Path("/api/v1/valuation")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ApplicationScoped
public class ValuationResource {

  @Inject ValuationService valuationService;

  @GET
  @Path("/declaration/{ref}")
  @RolesAllowed(ValuationSecurityDefinition.VIEWING)
  public Response getAssessment(@PathParam("ref") String ref) {
    return Response.ok(valuationService.findByReference(ref)).build();
  }
}

B. Contratos gRPC de Alta Performance (Exemplo assignment-grpc)

Defina o ficheiro .proto sob grpc/src/main/proto/:

grpc/src/main/proto/Valuation.proto
syntax = "proto3";
option java_multiple_files = true;
package org.asycuda.cloud.customs.meumodulo.grpc.api;

service ValuationServiceGrpc {
  rpc evaluateRisk (ValuationRiskRequest) returns (ValuationRiskResponse) {}
}

message ValuationRiskRequest {
  string declarationRef = 1;
  double declaredValue = 2;
}

message ValuationRiskResponse {
  bool requiresInspection = 1;
  string riskLevel = 2;
}

C. Mensagens Assíncronas Kafka (Exemplo customs/cargo & declaration)

src/main/java/org/asycuda/cloud/customs/meumodulo/services/RiskKafkaConsumer.java
@ApplicationScoped
public class RiskKafkaConsumer {

  @Incoming("risk")
  public void processRiskEvent(RiskAssessmentEvent event) {
    // Processa a resposta do canal de risco da alfândega
  }
}
10
Internacionalização (i18n) e application.properties

As traduções dos campos residem em ficheiros JSON localizados por classe:

shared/translation/.../i18n/org.asycuda.cloud.customs.meumodulo.entities.ValuationDocumentEntity.json
{
  "declarationReference": { "en": "Declaration Reference", "pt": "Referência da Declaração" },
  "customsOffice": { "en": "Customs Office", "pt": "Estância Aduaneira" },
  "assessedValue": { "en": "Assessed Value", "pt": "Valor Avaliado" }
}

E configure o ficheiro de runtime:

src/main/resources/application.properties
asycuda.module.name=valuation
quarkus.package.output-name=asy5-valuation

# Logging & SQL
quarkus.log.level=INFO
quarkus.hibernate-orm.log.format-sql=true

# Portas (Escolha portas livres, ex: 8099 para HTTP e 7189 para gRPC)
quarkus.http.port=8099
quarkus.http.root-path=/valuation
quarkus.grpc.server.port=7189
quarkus.grpc.server.enable-reflection-service=true

# Desabilitar integração automática de validação com Hibernate
quarkus.hibernate-orm.validation.enabled=false

# Validação do Token JWT SSO
mp.jwt.verify.audiences=sso
mp.jwt.verify.issuer=https://sso.asy5.local/realms/sso
mp.jwt.verify.publickey.location=https://sso.asy5.local/realms/sso/protocol/openid-connect/certs
mp.jwt.verify.requireiss=true

# Acordo de Licença UNCTAD Obrigatório
asycuda.core.license-agreement=My agency or/and I have obtained a proper license from UNCTAD/DTL/ASYCUDA Programme, and I carefully, with full understanding, read the license terms available at https://www.asycuda.org/licenses/LICENSE-1.0, and agree to it without any objection, and we shall be responsible for any legal obligations violating the license terms
11
Testes Automatizados & Execução

Crie testes unitários e de integração utilizando JUnit 5 e Quarkus:

src/test/java/org/asycuda/cloud/customs/meumodulo/ValuationServiceTest.java
package org.asycuda.cloud.customs.meumodulo;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

@QuarkusTest
public class ValuationServiceTest {

  @Test
  public void testValidationRules() {
    // Validação das regras de negócio
    assertTrue(true);
  }
}

Comandos úteis de execução:

Terminal (Comandos Gradle)
# 1. Validar se o Gradle reconhece todos os subprojetos
./gradlew projects | grep meumodulo

# 2. Compilar e executar testes
./gradlew :customs:meumodulo:build

# 3. Arrancar em modo de desenvolvimento contínuo (Live Reload)
./gradlew :customs:meumodulo:quarkusDev

05 Boas Práticas & Erros Frequentes ("Gotchas")

⚠️ 1. Nunca altere um Changeset já aplicado do Liquibase
No ASY5, os changesets são imutáveis. Se precisar de adicionar uma coluna ou modificar uma constraint, crie SEMPRE um novo changeset sequencial (ex: 002-VALUATION-add-column.json) e inclua-o no master-changelog.json.
⚠️ 2. Ignorar Coleções no MapStruct
Entidades JPA com relações @OneToMany ou @ManyToMany geram erros graves de ciclo ou sobrescrita acidental se mapeadas automaticamente pelo MapStruct. Ignore-as na assinatura do mapper (@Mapping(target = "itens", ignore = true)) e faça o tratamento no método @AfterMapping persistindo os elementos filhos explicitamente com o EntityManager.
💡 3. Resolução de NIF de Operador
Ao implementar RestrictionClaimOperator, verifique sempre token.isServer() || isOperatorUnrestricted(token). Oficiais de alfândega e chamadas internas do sistema devem ter acesso irrestrito; a filtragem estrita por NIF aplica-se exclusivamente a utilizadores externos (despachantes e importadores).