# 🤖 CoreSync — Agent Guidelines Este documento define a arquitetura, as regras de negócio e os padrões de código obrigatórios para o desenvolvimento do backend do **CoreSync**. Todo código gerado ou modificado deve obedecer estritamente às diretrizes abaixo. --- ## 🛠 Stack Tecnológico | Camada | Tecnologia | |---|---| | Linguagem | Groovy (Closures, Records, sintaxe enxuta) | | Runtime | Java 26 (toolchain configurado no Gradle) | | Framework | Spring Boot (Web, Security, Data MongoDB) | | Banco de Dados | MongoDB | | Cliente | React Native — comunicação exclusiva via JSON/REST + JWT stateless | --- ## 🏛 Arquitetura: Hexagonal (Ports & Adapters) + DDD O sistema é um **Monolito Modular** construído sobre **Arquitetura Hexagonal**. O objetivo é o isolamento absoluto da Regra de Negócio. ### Regra de Ouro > As camadas `domain` e `application` são **100% agnósticas de framework**. Elas não conhecem Spring Boot, MongoDB nem HTTP. Tudo que for infraestrutura ou framework vive exclusivamente na camada `adapter`. ### 📂 Estrutura de Pacotes Obrigatória Os pacotes são organizados por **módulo de negócio** (ex: `user`, `workout`, `session`). Dentro de cada módulo: ``` src/main/groovy/br/dev/jsilveira/coresync/[modulo]/ ├── domain/ │ ├── model/ # Records Groovy — Agregados, Entidades e Value Objects │ └── exception/ # Exceções de negócio do módulo │ ├── application/ │ ├── port/ │ │ ├── in/ # Interfaces de entrada (Casos de Uso chamados pela API) │ │ └── out/ # Interfaces de saída (o que o domínio precisa do mundo externo) │ └── service/ # Implementa ports/in; consome ports/out │ ├── adapter/ │ ├── in/web/ # Controllers REST + DTOs de entrada/saída │ └── out/persistence/ # Spring Data Repositories, Entities MongoDB, Mappers │ └── config/ # Classes @Configuration com definições de @Bean ``` --- ## 📜 Regras de Código ### 1. Modelagem com Records (Imutabilidade) - Todas as Entidades, Agregados e Value Objects do domínio **devem ser `record` do Groovy**. - O estado **nunca é mutado diretamente**. Toda transição de estado produz uma nova instância do record (padrão *copy-on-write*). ### 2. Validação no Construtor Compacto - Toda validação de invariantes de negócio ocorre **no construtor compacto do record**. - É **proibido** instanciar um objeto de domínio em estado inválido (IDs nulos, strings vazias, datas inconsistentes, dependências ausentes). - Violations devem lançar a **exceção de negócio específica do módulo** (nunca `IllegalArgumentException` genérica). ### 3. Identificadores (UUID) - O banco de dados **não gera IDs**. Cada entidade possui seu próprio `UUID id`. - A geração do ID ocorre na camada **`application/service`** via `UUID.randomUUID()`, antes do envio ao adaptador de persistência. - Referências entre Aggregate Roots são feitas **exclusivamente por ID** — nunca aninhe objetos raiz dentro de outros. ### 4. Zero Lombok - Lombok é **proibido**. Use os recursos nativos do Groovy: - Construtores de mapa (`new Foo(bar: "x")`) - Propriedades automáticas (`@groovy.transform.CompileStatic`) - `@Slf4j` nativo do Groovy para logging - `record` para imutabilidade ### 5. Domínio Rico (Rich Domain) - O domínio **não deve ser anêmico**. Lógicas de cálculo, transições de estado, formatações e validações pertinentes à entidade ficam **dentro do próprio record**. - O `Service` orquestra; ele **não executa regras de negócio**. ### 6. Services são POJOs - Classes de `application/service` **não recebem** `@Service` nem qualquer anotação do Spring. - São classes Groovy puras instanciadas como `@Bean` via uma classe `@Configuration` no módulo. - Dependências injetadas **exclusivamente pelo construtor**. ### 7. Nomenclatura | Artefato | Convenção | |---|---| | Porta de entrada | `[Ação][Entidade]UseCase` (ex: `FinishSessionUseCase`) | | Porta de saída — leitura | `Load[Entidade]Port` | | Porta de saída — escrita | `Save[Entidade]Port` | | Porta de saída — remoção | `Delete[Entidade]Port` | | Serviço | `[Ação][Entidade]Service` | | Controller | `[Entidade]Controller` | | Entity (Mongo) | `[Entidade]Entity` | | DTO de entrada | `[Entidade]Request` | | DTO de saída | `[Entidade]Response` | ### 8. Mapeamento entre Camadas - Conversão entre `DomainModel ↔ Entity` e `DomainModel ↔ DTO` ocorre em classes **Mapper dedicadas** em `adapter/out/persistence/` e `adapter/in/web/`, respectivamente. - Nunca exponha uma `Entity` fora do adaptador de persistência. - Nunca exponha um objeto de domínio diretamente em uma resposta HTTP. ### 9. Tratamento de Erros - Exceções de negócio definidas em `domain/exception/` são capturadas por um `@ControllerAdvice` global no adaptador web. - O `@ControllerAdvice` traduz exceções para respostas HTTP adequadas (ex: `SessionException` → `400 Bad Request`). - O domínio **nunca conhece** códigos HTTP. ### 10. Segurança e Autenticação - Toda autenticação é **stateless via JWT**. - O token é validado no adaptador (`adapter/in/web/`) antes de qualquer chamada ao caso de uso. - O `userId` extraído do token deve ser passado explicitamente como parâmetro ao caso de uso — nunca acessado diretamente de um contexto de segurança dentro da camada `application`. --- ## 💻 Exemplos de Código ### Modelo de Domínio — `record` com Construtor Compacto **Local:** `session/domain/model/UserWorkoutSession.groovy` ```groovy package br.dev.jsilveira.coresync.session.domain.model import br.dev.jsilveira.coresync.session.domain.exception.SessionException import java.time.LocalDateTime import java.util.UUID record UserWorkoutSession( UUID id, UUID userId, UUID workoutDayId, LocalDateTime startedAt, LocalDateTime completedAt ) { // Construtor compacto: validação de invariantes de negócio public UserWorkoutSession { if (!id) throw new SessionException("O ID não pode ser nulo") if (!userId) throw new SessionException("O ID do usuário é obrigatório") if (!workoutDayId) throw new SessionException("O ID do treino é obrigatório") if (!startedAt) throw new SessionException("A data de início é obrigatória") if (completedAt != null && completedAt.isBefore(startedAt)) { throw new SessionException("A data de conclusão não pode ser anterior à data de início") } } // Domínio Rico: transição de estado gera uma nova instância imutável UserWorkoutSession finish(LocalDateTime time = LocalDateTime.now()) { if (completedAt != null) throw new SessionException("A sessão já foi finalizada") return new UserWorkoutSession(id, userId, workoutDayId, startedAt, time) } boolean isFinished() { return completedAt != null } } ``` --- ### Porta de Entrada (API) **Local:** `session/application/port/in/FinishSessionUseCase.groovy` ```groovy package br.dev.jsilveira.coresync.session.application.port.in import java.util.UUID interface FinishSessionUseCase { void execute(UUID sessionId, UUID requestingUserId) } ``` --- ### Porta de Saída (SPI) **Local:** `session/application/port/out/SaveSessionPort.groovy` ```groovy package br.dev.jsilveira.coresync.session.application.port.out import br.dev.jsilveira.coresync.session.domain.model.UserWorkoutSession // O domínio declara o contrato; a implementação vive no adaptador interface SaveSessionPort { void save(UserWorkoutSession session) } ``` **Local:** `session/application/port/out/LoadSessionPort.groovy` ```groovy package br.dev.jsilveira.coresync.session.application.port.out import br.dev.jsilveira.coresync.session.domain.model.UserWorkoutSession import java.util.UUID interface LoadSessionPort { UserWorkoutSession load(UUID sessionId) } ``` --- ### Serviço (Caso de Uso) — POJO Groovy **Local:** `session/application/service/FinishWorkoutSessionService.groovy` ```groovy package br.dev.jsilveira.coresync.session.application.service import br.dev.jsilveira.coresync.session.application.port.in.FinishSessionUseCase import br.dev.jsilveira.coresync.session.application.port.out.LoadSessionPort import br.dev.jsilveira.coresync.session.application.port.out.SaveSessionPort import br.dev.jsilveira.coresync.session.domain.exception.SessionException import java.util.UUID // Sem @Service — instanciado via @Bean em SessionConfig class FinishWorkoutSessionService implements FinishSessionUseCase { private final LoadSessionPort loadPort private final SaveSessionPort savePort FinishWorkoutSessionService(LoadSessionPort loadPort, SaveSessionPort savePort) { this.loadPort = loadPort this.savePort = savePort } @Override void execute(UUID sessionId, UUID requestingUserId) { def session = loadPort.load(sessionId) // Autorização de negócio: o usuário só pode finalizar a própria sessão if (session.userId() != requestingUserId) { throw new SessionException("Usuário não autorizado a finalizar esta sessão") } // Regra de negócio encapsulada no domínio def finishedSession = session.finish() savePort.save(finishedSession) } } ``` --- ### Entity de Banco (Adaptador de Persistência) **Local:** `session/adapter/out/persistence/entity/SessionEntity.groovy` ```groovy package br.dev.jsilveira.coresync.session.adapter.out.persistence.entity import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document import java.time.LocalDateTime import java.util.UUID // Anotações de framework são permitidas apenas na camada adapter @Document(collection = "workout_sessions") record SessionEntity( @Id UUID id, UUID userId, UUID workoutDayId, LocalDateTime startedAt, LocalDateTime completedAt ) {} ``` --- ### Configuração do Módulo (Bean Wiring) **Local:** `session/config/SessionConfig.groovy` ```groovy package br.dev.jsilveira.coresync.session.config import br.dev.jsilveira.coresync.session.application.port.out.LoadSessionPort import br.dev.jsilveira.coresync.session.application.port.out.SaveSessionPort import br.dev.jsilveira.coresync.session.application.service.FinishWorkoutSessionService import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class SessionConfig { @Bean FinishWorkoutSessionService finishWorkoutSessionService( LoadSessionPort loadPort, SaveSessionPort savePort ) { return new FinishWorkoutSessionService(loadPort, savePort) } } ``` --- ## ✅ Checklist por Artefato Antes de submeter qualquer código, verifique: **Domain Model (record)** - [ ] Validações completas no construtor compacto - [ ] Sem anotações de Spring ou Mongo - [ ] Transições de estado retornam nova instância (imutabilidade) - [ ] Lógica de negócio pertinente está no record, não no service **Application Service** - [ ] Sem `@Service` ou qualquer anotação do Spring - [ ] Injeção de dependência apenas por construtor - [ ] Consome somente `ports/in` e `ports/out` - [ ] Não contém regras de negócio (delega ao domínio) **Adapter (Controller / Repository)** - [ ] Controller recebe e retorna apenas DTOs (`*Request` / `*Response`) - [ ] Mapeamento domínio ↔ DTO em Mapper dedicado - [ ] Repository implementa a interface `port/out` correspondente - [ ] Mapeamento domínio ↔ Entity em Mapper dedicado **Geral** - [ ] Nenhum import do Lombok em qualquer camada - [ ] IDs gerados na camada `application`, nunca no banco - [ ] Referências entre Aggregate Roots feitas apenas por UUID - [ ] Exceções de negócio definidas em `domain/exception/`