### Histórico para PreAtendimento Aqui está uma implementação completa de auditoria manual para a entidade `PreAtendimento`, seguindo a arquitetura hexagonal (Ports & Adapters). --- ### 1. Esquema SQL das Tabelas ```sql -- Tabela de Revisões (equivalente à REVINFO) -- Sintaxe para InterSystems IRIS/Caché CREATE TABLE preatendimento.REVINFO ( REV BIGINT NOT NULL, REVTSTMP TIMESTAMP NOT NULL, Usuario VARCHAR(100) NULL, CONSTRAINT PK_REVINFO PRIMARY KEY (REV) ); -- Tabela de Auditoria do PreAtendimento (<ENTITY>_AUD) CREATE TABLE preatendimento.PreAtendimento_AUD ( ID BIGINT NOT NULL, REV BIGINT NOT NULL, REVTYPE VARCHAR(50) NOT NULL, -- INSERT, UPDATE, DELETE IDExterno VARCHAR(255) NULL, Status VARCHAR(50) NULL, TelefoneOrigem VARCHAR(50) NULL, TelefoneLaboratorio VARCHAR(50) NULL, DataCriacao TIMESTAMP NULL, DataAtualizacao TIMESTAMP NULL, PacienteId BIGINT NULL, CONSTRAINT PK_PreAtendimento_AUD PRIMARY KEY (ID, REV), CONSTRAINT FK_PreAtendimento_AUD_REVINFO FOREIGN KEY (REV) REFERENCES preatendimento.REVINFO (REV) ); ``` --- ### 2. Modelos de Domínio #### 2.1 RevInfo (Domínio) ```java package br.com.shift.preatendimento.core.domain.model; import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; import lombok.Setter; @Getter @Setter @Builder public class RevInfo { private Long rev; private LocalDateTime revtstmp; private String usuario; } ``` #### 2.2 PreAtendimentoAudId (Domínio) ```java public class PreAtendimentoAudId implements Serializable { @Column(name = "ID") private Long id; @Column(name = "REV") private Long rev; } ``` #### 2.3 PreAtendimentoAud (Domínio) ```java public class PreAtendimentoAud { private PreAtendimentoAudId id; private RevType revType; private String idExterno; private StatusEnum status; private String telefoneOrigem; private String telefoneLaboratorio; private LocalDateTime dataCriacao; private LocalDateTime dataAtualizacao; private Long pacienteId; private RevInfo revInfo; public static PreAtendimentoAud criar(PreAtendimento preAtendimento, RevInfo revInfo) { return audit(preAtendimento, revInfo, RevType.INSERT); } public static PreAtendimentoAud atualizar(PreAtendimento preAtendimento, RevInfo revInfo) { return audit(preAtendimento, revInfo, RevType.UPDATE); } public static PreAtendimentoAud remover(PreAtendimento preAtendimento, RevInfo revInfo) { return audit(preAtendimento, revInfo, RevType.DELETE); } public static PreAtendimentoAud audit(PreAtendimento preAtendimento, RevInfo revInfo, RevType revType) { return PreAtendimentoAud.builder() .id(PreAtendimentoAudId.builder().id(preAtendimento.getId()).rev(revInfo.getRev()).build()) .revType(revType).idExterno(preAtendimento.getIdExterno()).status(preAtendimento.getStatus()) .telefoneOrigem(preAtendimento.getTelefoneOrigem()).telefoneLaboratorio(preAtendimento.getTelefoneLaboratorio()) .dataCriacao(preAtendimento.getDataCriacao()).dataAtualizacao(preAtendimento.getDataAtualizacao()) .pacienteId(preAtendimento.getPacienteId()).revInfo(revInfo).build(); } } ``` --- ### 3. Enum RevType ```java public enum RevType { INSERT, UPDATE, DELETE } ``` --- ### 4. Entidades JPA (Adapter Out) #### 4.1 Classe de Chave Composta (PreAtendimentoAudIdEntity) ```java public class PreAtendimentoAudIdEntity implements Serializable { @Column(name = "ID") private Long id; @Column(name = "REV") private Long rev; public static PreAtendimentoAudIdEntity fromDomain(PreAtendimentoAudId preAtendimentoAudId) { return new PreAtendimentoAudIdEntity(preAtendimentoAudId.getId(), preAtendimentoAudId.getRev()); } public PreAtendimentoAudId toDomain() { return new PreAtendimentoAudId(this.id, this.rev); } } ``` #### 4.2 Entidade de Auditoria (PreAtendimentoAudEntity) ```java public class PreAtendimentoAudEntity { @EmbeddedId private PreAtendimentoAudIdEntity id; @Column(name = "REVTYPE") @Enumerated(EnumType.STRING) private RevType revType; @Column(name = "IDExterno") private String idExterno; @Column(name = "Status") @Enumerated(EnumType.STRING) private StatusEnum status; @Column(name = "TelefoneOrigem") private String telefoneOrigem; @Column(name = "TelefoneLaboratorio") private String telefoneLaboratorio; @Column(name = "DataCriacao") private LocalDateTime dataCriacao; @Column(name = "DataAtualizacao") private LocalDateTime dataAtualizacao; @Column(name = "PacienteId") private Long pacienteId; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "REV", insertable = false, updatable = false, foreignKey = @ForeignKey(name = "FK_PreAtendimento_AUD_REVINFO")) private RevInfoEntity revInfo; public static PreAtendimentoAudEntity fromDomain(PreAtendimentoAud preAtendimentoAud) { return PreAtendimentoAudEntity.builder() .id(PreAtendimentoAudIdEntity.fromDomain(preAtendimentoAud.getId())) .revType(preAtendimentoAud.getRevType()) .idExterno(preAtendimentoAud.getIdExterno()) .status(preAtendimentoAud.getStatus()) .telefoneOrigem(preAtendimentoAud.getTelefoneOrigem()) .telefoneLaboratorio(preAtendimentoAud.getTelefoneLaboratorio()) .dataCriacao(preAtendimentoAud.getDataCriacao()) .dataAtualizacao(preAtendimentoAud.getDataAtualizacao()) .pacienteId(preAtendimentoAud.getPacienteId()) .build(); } public PreAtendimentoAud toDomain() { return PreAtendimentoAud.builder() .id(this.id.toDomain()) .revType(this.revType) .idExterno(this.idExterno) .status(this.status) .telefoneOrigem(this.telefoneOrigem) .telefoneLaboratorio(this.telefoneLaboratorio) .dataCriacao(this.dataCriacao) .dataAtualizacao(this.dataAtualizacao) .pacienteId(this.pacienteId) .build(); } } ``` #### 4.3 Entidade RevInfo (RevInfoEntity) ```java public class RevInfoEntity { @Id @Column(name = "REV") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long rev; @CreationTimestamp @Column(name = "REVTSTMP") private LocalDateTime revtstmp; @Column(name = "Usuario") private String usuario; public static RevInfoEntity fromDomain(RevInfo revInfo) { return RevInfoEntity.builder().rev(revInfo.getRev()).revtstmp(revInfo.getRevtstmp()).usuario(revInfo.getUsuario()) .build(); } public RevInfo toDomain() { return RevInfo.builder().rev(this.rev).revtstmp(this.revtstmp).usuario(this.usuario).build(); } } ``` --- ### 5. Ports (Interfaces de Saída) #### 5.1 RevInfoRepositoryPort ```java public interface RevInfoRepositoryPort { RevInfo salvar(RevInfo revInfo); } ``` #### 5.2 PreAtendimentoAudRepositoryPort ```java public interface PreAtendimentoAudRepositoryPort { void persistir(PreAtendimentoAud preAtendimentoAud); } ``` --- ### 6. Adapters (Implementações dos Ports) #### 6.1 RevInfoAdapter ```java @ApplicationScoped public class RevInfoAdapter implements RevInfoRepositoryPort, PanacheRepositoryBase<RevInfoEntity, Long> { @Override public RevInfo salvar(RevInfo revInfo) { RevInfoEntity entity = RevInfoEntity.fromDomain(revInfo); persist(entity); return entity.toDomain(); } } ``` #### 6.2 PreAtendimentoAudDbAdapter ```java @ApplicationScoped public class PreAtendimentoAudDbAdapter implements PreAtendimentoAudRepositoryPort, PanacheRepositoryBase<PreAtendimentoAudEntity, PreAtendimentoAudIdEntity> { public List<PreAtendimentoAud> findById(Long entityId) { return list("id.id", entityId).stream().map(PreAtendimentoAudEntity::toDomain).toList(); } public List<PreAtendimentoAud> findByRev(Long rev) { return list("id.rev", rev).stream().map(PreAtendimentoAudEntity::toDomain).toList(); } public List<PreAtendimentoAud> findByIdOrderByRevDesc(Long entityId) { return list("id.id = ?1 ORDER BY id.rev DESC", entityId).stream().map(PreAtendimentoAudEntity::toDomain).toList(); } @Override public void persistir(PreAtendimentoAud preAtendimento) { persist(PreAtendimentoAudEntity.fromDomain(preAtendimento)); } } ``` --- ### 7. AuditService (Serviço de Aplicação) ```java @ApplicationScoped public class PreAtendimentoAuditService { @Inject RevInfoRepositoryPort revInfoRepository; @Inject PreAtendimentoAudRepositoryPort preAtendimentoAudRepository; @Transactional public void criar(PreAtendimento preAtendimento, String usuario) { RevInfo revInfo = revInfoRepository.salvar(RevInfo.builder().usuario(usuario).build()); PreAtendimentoAud preAtendimentoAud = PreAtendimentoAud.criar(preAtendimento, revInfo); preAtendimentoAudRepository.persistir(preAtendimentoAud); } @Transactional public void atualizar(PreAtendimento preAtendimento, String usuario) { RevInfo revInfo = revInfoRepository.salvar(RevInfo.builder().usuario(usuario).build()); PreAtendimentoAud preAtendimentoAud = PreAtendimentoAud.atualizar(preAtendimento, revInfo); preAtendimentoAudRepository.persistir(preAtendimentoAud); } @Transactional public void remover(PreAtendimento preAtendimento, String usuario) { RevInfo revInfo = revInfoRepository.salvar(RevInfo.builder().usuario(usuario).build()); PreAtendimentoAud preAtendimentoAud = PreAtendimentoAud.remover(preAtendimento, revInfo); preAtendimentoAudRepository.persistir(preAtendimentoAud); } } ``` --- ### 8. Exemplo de Uso no Serviço de Negócio (PreAtendimentoService) ```java @Slf4j @ApplicationScoped public class PreAtendimentoService implements IdentificacaoUseCase, ConfirmacaoPreAtendimentoUseCase { @Inject PreAtendimentoRepositoryPort preAtendimentoRepositoryPort; @Inject PacienteRepositoryPort pacienteRepositoryPort; @Inject PreAtendimentoAuditService preAtendimentoAuditService; @Override @WithSpan public void registrarIdentificacao(IdentificacaoPreAtendimento identificacaoPreAtendimento) { Optional<Long> pacienteId = pacienteRepositoryPort .findByDocumentoIdentificacaoAndDataNascimento( identificacaoPreAtendimento.getDocumentoIdentificacaoPaciente(), identificacaoPreAtendimento.getDataNascimentoPaciente()); LocalDateTime dataHoraAgora = LocalDateTime.now(); PreAtendimento preAtendimento = PreAtendimento.criar(identificacaoPreAtendimento, pacienteId, dataHoraAgora); preAtendimentoRepositoryPort.salvarPreAtendimento(preAtendimento); preAtendimentoAuditService.criar(preAtendimento, "username"); } @Override @WithSpan @Transactional public void confirmar(String idExterno) { PreAtendimento preAtendimento = buscarPorIdExterno(idExterno); preAtendimento.confirmar(); preAtendimentoRepositoryPort.atualizar(preAtendimento); preAtendimentoAuditService.atualizar(preAtendimento, "username"); } private PreAtendimento buscarPorIdExterno(String idExterno) { return preAtendimentoRepositoryPort .buscarPorIdExterno(idExterno) .orElseThrow(() -> { log.error("Pré-atendimento não encontrado pelo idExterno: {}", idExterno); return new PreAtendimentoInexistenteException(); }); } } ``` --- ### 9. Como Estender para Outras Entidades Para adicionar auditoria a uma nova entidade (ex: `Paciente`), siga estes passos: #### Passo 1: Criar a tabela de auditoria SQL ```sql CREATE TABLE preatendimento.Paciente_AUD ( ID BIGINT NOT NULL, REV BIGINT NOT NULL, REVTYPE VARCHAR(50) NOT NULL, -- INSERT, UPDATE, DELETE -- Adicione aqui as colunas da entidade Paciente Nome VARCHAR(255) NULL, CPF VARCHAR(14) NULL, DataNascimento DATE NULL, -- ... outras colunas CONSTRAINT PK_Paciente_AUD PRIMARY KEY (ID, REV), CONSTRAINT FK_Paciente_AUD_REVINFO FOREIGN KEY (REV) REFERENCES preatendimento.REVINFO(REV) ); ``` #### Passo 2: Criar os modelos de domínio ```java // PacienteAudId.java (domínio) @Getter @Setter @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode @Builder(toBuilder = true) public class PacienteAudId implements Serializable { private Long id; private Long rev; } // PacienteAud.java (domínio) @Data @Builder(toBuilder = true) public class PacienteAud { private PacienteAudId id; private RevType revType; private String nome; private String cpf; private LocalDate dataNascimento; private RevInfo revInfo; public static PacienteAud criar(Paciente paciente, RevInfo revInfo) { return audit(paciente, revInfo, RevType.INSERT); } public static PacienteAud atualizar(Paciente paciente, RevInfo revInfo) { return audit(paciente, revInfo, RevType.UPDATE); } public static PacienteAud remover(Paciente paciente, RevInfo revInfo) { return audit(paciente, revInfo, RevType.DELETE); } private static PacienteAud audit(Paciente paciente, RevInfo revInfo, RevType revType) { return PacienteAud.builder() .id(PacienteAudId.builder().id(paciente.getId()).rev(revInfo.getRev()).build()) .revType(revType) .nome(paciente.getNome()) .cpf(paciente.getCpf()) .dataNascimento(paciente.getDataNascimento()) .revInfo(revInfo) .build(); } } ``` #### Passo 3: Criar as entidades JPA ```java // PacienteAudIdEntity.java @Embeddable public class PacienteAudIdEntity implements Serializable { @Column(name = "ID") private Long id; @Column(name = "REV") private Long rev; public static PacienteAudIdEntity fromDomain(PacienteAudId pacienteAudId) { return new PacienteAudIdEntity(pacienteAudId.getId(), pacienteAudId.getRev()); } public PacienteAudId toDomain() { return new PacienteAudId(this.id, this.rev); } } // PacienteAudEntity.java @Entity @Table(schema = "preatendimento", name = "Paciente_AUD") public class PacienteAudEntity { @EmbeddedId private PacienteAudIdEntity id; @Column(name = "REVTYPE") @Enumerated(EnumType.STRING) private RevType revType; private String nome; private String cpf; private LocalDate dataNascimento; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "REV", insertable = false, updatable = false) private RevInfoEntity revInfo; public static PacienteAudEntity fromDomain(PacienteAud pacienteAud) { // ... conversão de domínio para entidade } public PacienteAud toDomain() { // ... conversão de entidade para domínio } } ``` #### Passo 4: Criar o Port (interface) ```java public interface PacienteAudRepositoryPort { void persistir(PacienteAud pacienteAud); } ``` #### Passo 5: Criar o Adapter (implementação) ```java @ApplicationScoped public class PacienteAudDbAdapter implements PacienteAudRepositoryPort, PanacheRepositoryBase<PacienteAudEntity, PacienteAudIdEntity> { public List<PacienteAud> findById(Long entityId) { return list("id.id", entityId).stream().map(PacienteAudEntity::toDomain).toList(); } @Override public void persistir(PacienteAud pacienteAud) { persist(PacienteAudEntity.fromDomain(pacienteAud)); } } ``` #### Passo 6: Criar o AuditService específico ```java @ApplicationScoped public class PacienteAuditService { @Inject RevInfoRepositoryPort revInfoRepository; @Inject PacienteAudRepositoryPort pacienteAudRepository; @Transactional public void criar(Paciente paciente, String usuario) { RevInfo revInfo = revInfoRepository.salvar(RevInfo.builder().usuario(usuario).build()); PacienteAud pacienteAud = PacienteAud.criar(paciente, revInfo); pacienteAudRepository.persistir(pacienteAud); } @Transactional public void atualizar(Paciente paciente, String usuario) { RevInfo revInfo = revInfoRepository.salvar(RevInfo.builder().usuario(usuario).build()); PacienteAud pacienteAud = PacienteAud.atualizar(paciente, revInfo); pacienteAudRepository.persistir(pacienteAud); } @Transactional public void remover(Paciente paciente, String usuario) { RevInfo revInfo = revInfoRepository.salvar(RevInfo.builder().usuario(usuario).build()); PacienteAud pacienteAud = PacienteAud.remover(paciente, revInfo); pacienteAudRepository.persistir(pacienteAud); } } ``` #### Passo 7: Chamar no serviço de negócio ```java @Transactional public Paciente criar(Paciente paciente, String usuario) { pacienteRepositoryPort.salvar(paciente); pacienteAuditService.criar(paciente, usuario); return paciente; } ``` --- ### Exemplos de Saída JSON #### Exemplo 1: RevInfo ```json { "rev": 1, "revtstmp": "2025-12-10T01:00:00", "usuario": "admin" } ``` #### Exemplo 2: PreAtendimentoAudId ```json { "id": 100, "rev": 1 } ``` #### Exemplo 3: PreAtendimentoAud (INSERT) ```json { "id": { "id": 100, "rev": 1 }, "revType": "INSERT", "idExterno": "ext-12345", "status": "PENDENTE", "telefoneOrigem": "11999998888", "telefoneLaboratorio": "1133334444", "dataCriacao": "2025-12-10T01:00:00", "dataAtualizacao": "2025-12-10T01:00:00", "pacienteId": 50, "revInfo": { "rev": 1, "revtstmp": "2025-12-10T01:00:00", "usuario": "admin" } } ``` #### Exemplo 4: PreAtendimentoAud (UPDATE) ```json { "id": { "id": 100, "rev": 2 }, "revType": "UPDATE", "idExterno": "ext-12345", "status": "CONFIRMADO", "telefoneOrigem": "11999998888", "telefoneLaboratorio": "1133334444", "dataCriacao": "2025-12-10T01:00:00", "dataAtualizacao": "2025-12-10T01:05:00", "pacienteId": 50, "revInfo": { "rev": 2, "revtstmp": "2025-12-10T01:05:00", "usuario": "admin" } } ``` #### Exemplo 5: Lista de Histórico (obterHistorico) Retorno do método `obterHistorico(Long entityId)` ordenado por revisão decrescente: ```json [ { "id": { "id": 100, "rev": 2 }, "revType": "UPDATE", "idExterno": "ext-12345", "status": "CONFIRMADO", "telefoneOrigem": "11999998888", "telefoneLaboratorio": "1133334444", "dataCriacao": "2025-12-10T01:00:00", "dataAtualizacao": "2025-12-10T01:05:00", "pacienteId": 50, "revInfo": { "rev": 2, "revtstmp": "2025-12-10T01:05:00", "usuario": "admin" } }, { "id": { "id": 100, "rev": 1 }, "revType": "INSERT", "idExterno": "ext-12345", "status": "PENDENTE", "telefoneOrigem": "11999998888", "telefoneLaboratorio": "1133334444", "dataCriacao": "2025-12-10T01:00:00", "dataAtualizacao": "2025-12-10T01:00:00", "pacienteId": 50, "revInfo": { "rev": 1, "revtstmp": "2025-12-10T01:00:00", "usuario": "admin" } } ] ``` --- ### Resumo da Estrutura de Pastas ``` src/main/java/br/com/shift/preatendimento/core/ ├── adapter/out/db/ │ ├── entity/ │ │ ├── PreAtendimentoEntity.java │ │ ├── PreAtendimentoAudEntity.java │ │ ├── PreAtendimentoAudIdEntity.java │ │ └── RevInfoEntity.java │ ├── PreAtendimentoAudDbAdapter.java │ └── RevInfoAdapter.java ├── application/ │ ├── port/ │ │ └── out/ │ │ ├── PreAtendimentoAudRepositoryPort.java │ │ └── RevInfoRepositoryPort.java │ └── service/ │ ├── PreAtendimentoService.java │ └── PreAtendimentoAuditService.java └── domain/model/ ├── PreAtendimento.java ├── PreAtendimentoAud.java ├── PreAtendimentoAudId.java ├── RevInfo.java └── enums/ ├── StatusEnum.java └── RevType.java src/main/resources/db/migrations/ └── 8.6/ ├── PROD-91621-create-table-revinfo.sql └── PROD-91621-create-table-preatendimento_aud.sql ``` --- ### Vantagens desta Abordagem 1. **Arquitetura Hexagonal**: Separação clara entre domínio, aplicação e infraestrutura 2. **Controle total**: Você decide exatamente quando e o que auditar 3. **Flexibilidade**: Pode adicionar campos extras (usuário, IP, etc.) 4. **Performance**: Sem overhead de interceptors automáticos 5. **Testabilidade**: Fácil de mockar os ports para testes unitários 6. **Desacoplamento**: O domínio não conhece detalhes de persistência ### Desvantagens 1. **Código manual**: Precisa lembrar de chamar o audit em cada operação 2. **Manutenção**: Ao alterar a entidade, deve alterar também a tabela _AUD e os modelos de domínio/entidade --- Este código está pronto para ser usado no seu projeto Quarkus com Panache seguindo arquitetura hexagonal!