### 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!