# Nova Funcionalidade — Guia Prático > Para desenvolvedores e IA. Toda funcionalidade nova entra neste formato. > Referência: [modulos.md](modulos.md) | [adr-001-modular-monolith.md](adr-001-modular-monolith.md) --- ## Onde criar o arquivo Cada funcionalidade vive dentro do seu módulo: ``` src/ Donc.Modulos.{Modulo}/ Comandos/ {NomeDaAcao}/ {NomeDaAcao}Command.cs {NomeDaAcao}Handler.cs {NomeDaAcao}Validator.cs Consultas/ {NomeDaConsulta}/ {NomeDaConsulta}Query.cs {NomeDaConsulta}Handler.cs Dominio/ {Entidade}.cs Status{Entidade}.cs Contratos/ IServico{Modulo}.cs README.md ``` **Exemplos reais:** - `Donc.Modulos.OS/Comandos/CriarOS/CriarOSCommand.cs` - `Donc.Modulos.OS/Consultas/BuscarOSPorId/BuscarOSPorIdQuery.cs` - `Donc.Modulos.Profissional/Comandos/CriarProfissionalComParceiro/CriarProfissionalComParceiroCommand.cs` --- ## Passo a Passo: Criar um Command ### 1. Definir o Command (intenção de escrita) ```csharp // CriarOSCommand.cs public record CriarOSCommand( int TipoJobId, string Titulo, string Descricao, DateTime DataExecucao, int FormaPagamentoId, decimal ValorCobrar, EnderecoCommand Endereco, ClienteCommand Cliente ) : IRequest; public record EnderecoCommand( string Logradouro, string Numero, string Bairro, string Cidade, string Estado, string CEP ); public record ClienteCommand( string Nome, string Telefone, string? Email = null, string? CPF = null ); public record ResultadoCriacaoOS(int OrdemServicoId, string CodigoOS); ``` ### 2. Criar o Validator (regras de negócio da entrada) ```csharp // CriarOSValidator.cs public class CriarOSValidator : AbstractValidator { public CriarOSValidator() { RuleFor(c => c.TipoJobId) .GreaterThan(0).WithMessage("Tipo de serviço é obrigatório."); RuleFor(c => c.DataExecucao) .GreaterThan(DateTime.Now).WithMessage("Data de execução deve ser futura."); RuleFor(c => c.FormaPagamentoId) .GreaterThan(0).WithMessage("Forma de pagamento é obrigatória."); RuleFor(c => c.ValorCobrar) .GreaterThanOrEqualTo(0).WithMessage("Valor não pode ser negativo."); RuleFor(c => c.Cliente.Nome) .NotEmpty().MaximumLength(200).WithMessage("Nome do cliente é obrigatório."); RuleFor(c => c.Cliente.Telefone) .NotEmpty().MaximumLength(14).WithMessage("Telefone do cliente é obrigatório."); RuleFor(c => c.Endereco.CEP) .NotEmpty().Length(8).WithMessage("CEP deve ter 8 dígitos."); } } ``` ### 3. Criar o Handler (execução) ```csharp // CriarOSHandler.cs public class CriarOSHandler : IRequestHandler { private readonly IRepositorioOrdemServico _repositorio; private readonly IContextoTenant _contextoTenant; private readonly ILogger _logger; public CriarOSHandler( IRepositorioOrdemServico repositorio, IContextoTenant contextoTenant, ILogger logger) { _repositorio = repositorio; _contextoTenant = contextoTenant; _logger = logger; } public async Task Handle( CriarOSCommand comando, CancellationToken cancellationToken) { var tipoServico = await _repositorio.BuscarTipoServicoAsync( comando.TipoJobId, _contextoTenant.ContratoSaasId, // ← tenant sempre do IContextoTenant cancellationToken); if (tipoServico is null) throw new DomainException("Tipo de serviço não encontrado."); var ordemServico = OrdemServico.Criar( tipoServicoId: comando.TipoJobId, titulo: comando.Titulo, descricao: comando.Descricao, dataExecucao: comando.DataExecucao, estabelecimentoId: _contextoTenant.EstabelecimentoId, contratoSaasId: _contextoTenant.ContratoSaasId); var ordemServicoId = await _repositorio.InserirAsync(ordemServico, cancellationToken); _logger.LogInformation( "OS criada - Id: {OrdemServicoId} Tipo: {TipoServicoId} Tenant: {ContratoSaasId}", ordemServicoId, comando.TipoJobId, _contextoTenant.ContratoSaasId); return new ResultadoCriacaoOS(ordemServicoId, ordemServico.Codigo); } } ``` ### 4. Registrar no DI ```csharp // Em Donc.Modulos.OS/Extensions/OSModuloExtensions.cs public static class OSModuloExtensions { public static IServiceCollection AdicionarModuloOS(this IServiceCollection servicos) { servicos.AddScoped(); servicos.AddScoped(); // MediatR registra handlers automaticamente via assembly scanning return servicos; } } ``` --- ## Passo a Passo: Criar uma Query (Dapper) ```csharp // BuscarOSPorIdQuery.cs public record BuscarOSPorIdQuery(int OrdemServicoId) : IRequest; // BuscarOSPorIdHandler.cs public class BuscarOSPorIdHandler : IRequestHandler { private readonly IFabricaConexao _fabricaConexao; private readonly IContextoTenant _contextoTenant; public BuscarOSPorIdHandler( IFabricaConexao fabricaConexao, IContextoTenant contextoTenant) { _fabricaConexao = fabricaConexao; _contextoTenant = contextoTenant; } public async Task Handle( BuscarOSPorIdQuery consulta, CancellationToken cancellationToken) { const string sql = @" SELECT p.Id AS OrdemServicoId, p.Titulo, p.Descricao, p.StatusCalculadoId AS StatusId, p.DataInicioRealizacao AS DataExecucao, p.ValorCobrarPeloServico AS ValorCobrar FROM Pedido p INNER JOIN Saida s ON s.Id = p.SaidaId WHERE p.Id = @OrdemServicoId AND s.EstabelecimentoId = @EstabelecimentoId"; using var conexao = _fabricaConexao.CriarConexao(); await conexao.OpenAsync(cancellationToken); return await conexao.QuerySingleOrDefaultAsync(sql, new { consulta.OrdemServicoId, _contextoTenant.EstabelecimentoId // ← isolamento automático por tenant }); } } ``` --- ## Controller — Casca Fina O controller não tem lógica de negócio. Só recebe, chama o mediator e retorna. ```csharp [ApiController] [Route("ordens-servico")] public class OrdemServicoController : ControllerBase { private readonly IMediator _mediator; public OrdemServicoController(IMediator mediator) { _mediator = mediator; } [HttpPost] public async Task Criar([FromBody] CriarOSCommand comando) { var resultado = await _mediator.Send(comando); return Ok(resultado); } [HttpGet("{ordemServicoId:int}")] public async Task BuscarPorId(int ordemServicoId) { var resultado = await _mediator.Send(new BuscarOSPorIdQuery(ordemServicoId)); return resultado is null ? NotFound() : Ok(resultado); } } ``` --- ## Checklist de Entrega Antes de marcar a task como concluída, confirme: - [ ] Command ou Query criado com nome em português, sufixo correto - [ ] Validator criado com mensagens em português - [ ] Handler não acessa banco diretamente — usa repositório por interface - [ ] Handler obtém `ContratoSaasId` via `IContextoTenant`, nunca via parâmetro - [ ] Logs usam structured logging: `_logger.LogInformation("Msg {Param}", valor)` - [ ] Nenhuma abreviação proibida (`qtd`, `vlr`, `desc`, `obs`, `repo`, `svc`) - [ ] Controller não tem lógica de negócio - [ ] Handler registrado no DI do módulo (não no `Program.cs` diretamente) - [ ] Nenhum módulo acessa tabela de outro módulo diretamente --- ## Anti-Patterns — O Que Nunca Fazer ### Lógica no controller ```csharp // ERRADO public async Task Criar([FromBody] CriarOSCommand comando) { if (comando.TipoJobId <= 0) return BadRequest("Tipo inválido"); // validação no controller var os = new Pedido { TipoJobId = comando.TipoJobId }; // entidade no controller await _db.SaveChangesAsync(); // banco no controller return Ok(); } // CORRETO — só chama o mediator public async Task Criar([FromBody] CriarOSCommand comando) { var resultado = await _mediator.Send(comando); return Ok(resultado); } ``` ### Tenant como parâmetro manual ```csharp // ERRADO public record CriarOSCommand(int TipoJobId, int ContratoSaasId) : IRequest<...>; // ContratoSaasId não pertence ao command — pertence ao contexto da requisição // CORRETO public record CriarOSCommand(int TipoJobId) : IRequest<...>; // No handler: _contextoTenant.ContratoSaasId ``` ### Acesso direto a tabela de outro módulo ```csharp // ERRADO — módulo OS lendo tabela do módulo Profissional const string sql = "SELECT * FROM Motoqueiro WHERE ContratoSaasId = @id"; // CORRETO — módulo OS usa interface do módulo Profissional var profissional = await _servicoProfissional.BuscarPorCodigoManualAsync(codigo, ct); ``` ### Abreviações ```csharp // ERRADO var qtd = 10; var vlrTotal = 350m; var descOS = "Instalação"; var obsCliente = "Portão azul"; // CORRETO var quantidade = 10; var valorTotal = 350m; var descricaoOS = "Instalação"; var observacaoCliente = "Portão azul"; ``` ### Nome em inglês ```csharp // ERRADO public class CreateOSCommand { } public class OSRepository { } public async Task GetById(int id) { } // CORRETO public class CriarOSCommand { } public class RepositorioOrdemServico { } public async Task BuscarPorIdAsync(int ordemServicoId) { } ```