diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cc7cf24 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# Etapa de build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +# Copiar todos os arquivos do repositório para dentro do container +COPY . ./ + +# Restaurar as dependências a partir do arquivo de solução +RUN dotnet restore FrioAPI.sln + +# Buildar o projeto no modo Release +RUN dotnet publish FrioAPI.sln -c Release -o /publish + +# Etapa de runtime (imagem mais leve para executar) +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +WORKDIR /app + +# Copiar o conteúdo publicado da etapa anterior para o container +COPY --from=build /publish . + +# Expor as portas que serão usadas pela aplicação +EXPOSE 5000 +EXPOSE 5001 + +# Configurar o comando de entrada para iniciar sua API +ENTRYPOINT ["dotnet", "FrioAPI.Api.dll"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc1ed9e --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# 🧾 Gerador de Relatórios e Recibos API + +[![.NET](https://img.shields.io/badge/.NET-8.0-blueviolet.svg)](https://dotnet.microsoft.com/) +[![Swagger](https://img.shields.io/badge/Swagger-API-green.svg)](https://swagger.io/) +[![SQL Server](https://img.shields.io/badge/SQL%20Server-MS%20SQL-blue.svg)](https://www.microsoft.com/pt-br/sql-server) + +Bem-vindo à API **FrioAPI**! Esta aplicação foi desenvolvida com **DDD (Domain-Driven Design)** e os **princípios SOLID** para garantir alta escalabilidade, manutenibilidade e qualidade. O objetivo principal é permitir que você gere relatórios mensais e recibos personalizados para clientes em formatos **PDF** e **Excel**, usando as bibliotecas **ClosedXML** e **PdfSharp-MigraDoc**. Além disso, a documentação da API está acessível através do **Swagger**. + +## 🛠️ Tecnologias Utilizadas +- **.NET 8.0** +- **ClosedXML** (para gerar relatórios em Excel) +- **PdfSharp-MigraDoc** (para relatórios e recibos em PDF) +- **MS SQL Server** (banco de dados) +- **Swagger** (para documentação da API) + +## 🚀 Principais Funcionalidades +- Geração de recibos de clientes em **PDF**. +- Relatórios mensais em **PDF** e **Excel**. +- Arquitetura robusta baseada em **DDD** e **SOLID**. + +## 🎨 Visual do Projeto + ![image](https://github.com/user-attachments/assets/9727aecc-3e28-40a8-b4d6-6ae2f7025230) + + +## 📖 Como Usar + +### 📥 Clonar o Repositório +1. Clone o repositório: + ```sh + git clone https://github.com/ArthurTorres1/FrioAPI.git + ``` +2. Preencha as informações `appsettings.Development.json`; diff --git a/images/image-recibo.png b/images/image-recibo.png new file mode 100644 index 0000000..5ec0327 Binary files /dev/null and b/images/image-recibo.png differ diff --git a/src/FrioAPI.Api/Program.cs b/src/FrioAPI.Api/Program.cs index 9e34ad6..53f0bdf 100644 --- a/src/FrioAPI.Api/Program.cs +++ b/src/FrioAPI.Api/Program.cs @@ -1,15 +1,21 @@ using FrioAPI.Api.Filters; using FrioAPI.Application; +using FrioAPI.Application.UseCases.ViaCep; using FrioAPI.Infrastructure; var builder = WebApplication.CreateBuilder(args); + +// Configurao de CORS builder.Services.AddCors(options => { options.AddPolicy("AllowAll", policy => { - policy.AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader(); + policy.WithOrigins( + "https://frio-front.vercel.app", + "http://localhost:3000" + ) + .AllowAnyMethod() + .AllowAnyHeader(); }); }); @@ -19,9 +25,11 @@ builder.Services.AddMvc(options => options.Filters.Add(typeof(ExceptionFilter))); -//Injeo de dependencias +// Injeo de dependncias builder.Services.AddInfrastructure(builder.Configuration); builder.Services.AddApplication(); +builder.Services.AddHttpClient(); + var app = builder.Build(); app.UseCors("AllowAll"); @@ -30,15 +38,16 @@ { app.UseSwagger(); app.UseSwaggerUI(); + app.UseHttpsRedirection(); // HTTPS ativo apenas no ambiente de desenvolvimento } - else { - app.UseHttpsRedirection(); + // Desabilitar HTTPS para ambientes Docker/produo + app.Urls.Add("http://0.0.0.0:5000"); // Aceita conexes HTTP } app.UseAuthorization(); app.MapControllers(); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/src/FrioAPI.Api/appsettings.Development.json b/src/FrioAPI.Api/appsettings.Development.json index 6b62bb5..d553b95 100644 --- a/src/FrioAPI.Api/appsettings.Development.json +++ b/src/FrioAPI.Api/appsettings.Development.json @@ -1,6 +1,5 @@ { "ConnectionStrings": { - "Connection-dev": "", - "Connection-prod": "" + "Connection-dev": "Server=localhost,1433;Database=friodatabase;User Id=sa;Password=12345;TrustServerCertificate=True;" } } \ No newline at end of file diff --git a/src/FrioAPI.Application/DependencyInjectionExtension.cs b/src/FrioAPI.Application/DependencyInjectionExtension.cs index aaf0152..d2596c5 100644 --- a/src/FrioAPI.Application/DependencyInjectionExtension.cs +++ b/src/FrioAPI.Application/DependencyInjectionExtension.cs @@ -6,6 +6,7 @@ using FrioAPI.Application.UseCases.Recibos.Reports.Excel; using FrioAPI.Application.UseCases.Recibos.Reports.Pdf; using FrioAPI.Application.UseCases.Recibos.Update; +using FrioAPI.Application.UseCases.ViaCep; using Microsoft.Extensions.DependencyInjection; namespace FrioAPI.Application @@ -32,7 +33,7 @@ private static void AddUseCases(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - + services.AddScoped(); } } } diff --git a/src/FrioAPI.Application/UseCases/Recibos/Register/RegisterReciboUseCase.cs b/src/FrioAPI.Application/UseCases/Recibos/Register/RegisterReciboUseCase.cs index ffe6f43..1fbdef0 100644 --- a/src/FrioAPI.Application/UseCases/Recibos/Register/RegisterReciboUseCase.cs +++ b/src/FrioAPI.Application/UseCases/Recibos/Register/RegisterReciboUseCase.cs @@ -1,6 +1,8 @@ using System.Linq; +using System.Runtime.ConstrainedExecution; using AutoMapper; using FrioAPI.Application.UseCases.Recibos.Reports.Pdf; +using FrioAPI.Application.UseCases.ViaCep; using FrioAPI.Communication.Requests; using FrioAPI.Communication.Responses; using FrioAPI.Domain.Entities; @@ -17,21 +19,39 @@ public class RegisterReciboUseCase : IRegisterReciboUseCase private readonly IUnidadeDeTrabalho _unidadeDeTrabalho; private readonly IMapper _mapper; private readonly IGenerateRecibosReportPdfUseCase _pdfRecibo; + private readonly IBuscaEnderecoViaCep _viacepUseCase; public RegisterReciboUseCase( IRecibosWriteOnlyRepository repository, IUnidadeDeTrabalho unidadeDeTrabalho, IMapper mapper, - IGenerateRecibosReportPdfUseCase pdfRecibo + IGenerateRecibosReportPdfUseCase pdfRecibo, + IBuscaEnderecoViaCep viaCepUseCase ) { _recibosRepository = repository; _unidadeDeTrabalho = unidadeDeTrabalho; _mapper = mapper; _pdfRecibo = pdfRecibo; + _viacepUseCase = viaCepUseCase; } public async Task Execute(RequestReciboJson request) { + if (!string.IsNullOrWhiteSpace(request.CEP)) + { + var cepLimpo = new string(request.CEP.Where(char.IsDigit).ToArray()); + var endereco = await _viacepUseCase.BuscaCep(cepLimpo); + + if (endereco == null) + throw new ArgumentException("CEP inválido ou não encontrado. Por favor, informe um CEP válido ou preencha os campos manualmente."); + + //preenche automaticamente apenas se os campos estiverem vazios + request.Logradouro = string.IsNullOrWhiteSpace(request.Logradouro) ? endereco.Logradouro : request.Logradouro; + request.Bairro = string.IsNullOrWhiteSpace(request.Bairro) ? endereco.Bairro : request.Bairro; + request.Cidade = string.IsNullOrWhiteSpace(request.Cidade) ? endereco.Localidade : request.Cidade; + request.UF = string.IsNullOrWhiteSpace(request.UF) ? endereco.Uf : request.UF; + } + Validate(request); var entity = _mapper.Map(request); diff --git a/src/FrioAPI.Application/UseCases/Recibos/Reports/Pdf/GenerateRecibosReportPdfUseCase.cs b/src/FrioAPI.Application/UseCases/Recibos/Reports/Pdf/GenerateRecibosReportPdfUseCase.cs index dcc9bda..5d18fa7 100644 --- a/src/FrioAPI.Application/UseCases/Recibos/Reports/Pdf/GenerateRecibosReportPdfUseCase.cs +++ b/src/FrioAPI.Application/UseCases/Recibos/Reports/Pdf/GenerateRecibosReportPdfUseCase.cs @@ -8,6 +8,7 @@ using MigraDoc.DocumentObjectModel.Tables; using MigraDoc.Rendering; using PdfSharp.Fonts; +using System.Globalization; using System.Reflection; namespace FrioAPI.Application.UseCases.Recibos.Reports.Pdf @@ -42,7 +43,9 @@ public async Task ReciboClientePdf(Recibo recibo) row = table.AddRow(); row.Height = HEIGHT_ROW_TABLE_RECIBOS; - row.Cells[0].AddParagraph($"{recibo.Data:ddd dd MMM yyyy}"); + var cultura = new CultureInfo("pt-BR"); + row.Cells[0].AddParagraph($"{recibo.Data.ToString("ddd dd MMM yyyy", cultura)}"); + EstiloBaseParaInformacoesRecibo(row.Cells[0]); row.Cells[0].Format.LeftIndent = 10; diff --git a/src/FrioAPI.Application/UseCases/ViaCep/BuscaEnderecoViaCep.cs b/src/FrioAPI.Application/UseCases/ViaCep/BuscaEnderecoViaCep.cs new file mode 100644 index 0000000..6dd362d --- /dev/null +++ b/src/FrioAPI.Application/UseCases/ViaCep/BuscaEnderecoViaCep.cs @@ -0,0 +1,36 @@ +using FrioAPI.Communication.Responses; +using FrioAPI.Exception; +using FrioAPI.Exception.ExceptionsBase; +using System.Text.Json; + +namespace FrioAPI.Application.UseCases.ViaCep +{ + public class BuscaEnderecoViaCep : IBuscaEnderecoViaCep + { + private readonly HttpClient _httpClient; + + public BuscaEnderecoViaCep(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public async Task BuscaCep(string cep) + { + var response = await _httpClient.GetAsync($"https://viacep.com.br/ws/{cep}/json/"); + + if (!response.IsSuccessStatusCode) + return null; + + var content = await response.Content.ReadAsStringAsync(); + + var endereco = JsonSerializer.Deserialize(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (endereco?.Erro == true) + { + var errorMessages = new List{ ResourceErrorMessages.CEP_NAO_ENCONTRADO }; + throw new ErrorOnValidationException(errorMessages); + } + return endereco; + } + } +} diff --git a/src/FrioAPI.Application/UseCases/ViaCep/IBuscaEnderecoViaCep.cs b/src/FrioAPI.Application/UseCases/ViaCep/IBuscaEnderecoViaCep.cs new file mode 100644 index 0000000..5ca23b4 --- /dev/null +++ b/src/FrioAPI.Application/UseCases/ViaCep/IBuscaEnderecoViaCep.cs @@ -0,0 +1,9 @@ +using FrioAPI.Communication.Responses; + +namespace FrioAPI.Application.UseCases.ViaCep +{ + public interface IBuscaEnderecoViaCep + { + Task BuscaCep(string cep); + } +} diff --git a/src/FrioAPI.Communication/Responses/ResponseViaCep.cs b/src/FrioAPI.Communication/Responses/ResponseViaCep.cs new file mode 100644 index 0000000..2111833 --- /dev/null +++ b/src/FrioAPI.Communication/Responses/ResponseViaCep.cs @@ -0,0 +1,11 @@ +namespace FrioAPI.Communication.Responses +{ + public class ResponseViaCep + { + public string Logradouro { get; set; } = string.Empty; + public string Bairro { get; set; } = string.Empty; + public string Localidade { get; set; } = string.Empty; + public string Uf { get; set; } = string.Empty; + public bool Erro { get; set; } + } +} diff --git a/src/FrioAPI.Exception/ResourceErrorMessages.Designer.cs b/src/FrioAPI.Exception/ResourceErrorMessages.Designer.cs index 75efe17..f51a7ed 100644 --- a/src/FrioAPI.Exception/ResourceErrorMessages.Designer.cs +++ b/src/FrioAPI.Exception/ResourceErrorMessages.Designer.cs @@ -60,6 +60,15 @@ internal ResourceErrorMessages() { } } + /// + /// Looks up a localized string similar to O CEP não foi encontrado.. + /// + public static string CEP_NAO_ENCONTRADO { + get { + return ResourceManager.GetString("CEP_NAO_ENCONTRADO", resourceCulture); + } + } + /// /// Looks up a localized string similar to O campo (Cidade) é obrigatório.. /// diff --git a/src/FrioAPI.Exception/ResourceErrorMessages.resx b/src/FrioAPI.Exception/ResourceErrorMessages.resx index 92a5263..ad48145 100644 --- a/src/FrioAPI.Exception/ResourceErrorMessages.resx +++ b/src/FrioAPI.Exception/ResourceErrorMessages.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + O CEP não foi encontrado. + O campo (Cidade) é obrigatório. diff --git a/src/FrioAPI.Infrastructure/DepedencyInjectionExtension.cs b/src/FrioAPI.Infrastructure/DepedencyInjectionExtension.cs index 2357a2c..bb07d95 100644 --- a/src/FrioAPI.Infrastructure/DepedencyInjectionExtension.cs +++ b/src/FrioAPI.Infrastructure/DepedencyInjectionExtension.cs @@ -25,7 +25,14 @@ private static void AddRepostories(IServiceCollection services) } private static void AddDbContext(IServiceCollection services, IConfiguration configuration) { - var connectionString = configuration.GetConnectionString("Connection-prod"); + // Busca a connection string da variável de ambiente configurada no Render + var connectionString = Environment.GetEnvironmentVariable("CONNECTION_PROD") + ?? configuration.GetConnectionString("Connection-dev"); + + if (string.IsNullOrEmpty(connectionString)) + { + throw new InvalidOperationException("A Connection String não foi configurada."); + } services.AddDbContext(options => options.UseSqlServer(connectionString));