Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ CONTAINER_ENV=true
TZ=America/Sao_Paulo

# Certificates
CERT_PASSWORD=your-cert-password
AccessTokenSettings__Password=your-access-token-secret
RefreshTokenSettings__Password=your-refresh-token-secret
CERT_PASSWORD=
AccessTokenSettings__Password=
RefreshTokenSettings__Password=
AccessTokenSettings__Key=/app/Common/Certificates/access-token-jwt-key.pfx
RefreshTokenSettings__Key=/app/Common/Certificates/refresh-token-jwt-key.pfx

Expand All @@ -17,4 +17,9 @@ DB_USER=postgres
DB_PASSWORD=your-database-password

# Connection String
ConnectionStrings__DefaultConnection=Host=postgres;Database=snackflow_db;Username=postgres;Password=your-password;Port=5432
ConnectionStrings__DefaultConnection=Host=postgres;Database=snackflow_db;Username=postgres;Password=your-password;Port=5432

# AWS Configure
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=
7 changes: 7 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<ItemGroup>
<PackageVersion Include="Mediator.Abstractions" Version="3.0.0-preview.65" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
<PackageVersion Include="Quartz.AspNetCore" Version="3.14.0" />
<PackageVersion Include="Quartz.Jobs" Version="3.14.0" />
<PackageVersion Include="Quartz.Plugins" Version="3.14.0" />
</ItemGroup>
<!-- Pacotes específicos da camada Application -->
<ItemGroup>
Expand All @@ -30,6 +33,10 @@
</PackageVersion>
<PackageVersion Include="AspNetCore.HealthChecks.NpgSql" Version="9.0.0" />
<PackageVersion Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
<PackageVersion Include="Quartz.Extensions.DependencyInjection" Version="3.14.0" />
<PackageVersion Include="Quartz.Extensions.Hosting" Version="3.14.0" />
<PackageVersion Include="Quartz.Serialization.Json" Version="3.14.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<!-- Pacotes Pacotes específicos da camada Api -->
<ItemGroup>
Expand Down
1 change: 0 additions & 1 deletion src/SnackFlow.Api/Controllers/CompanyController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ namespace SnackFlow.Api.Controllers;
public class CompanyController(IMediator mediator) : ControllerBase
{
[HttpPost]
[Authorize]
[RequestTimeout(("standard"))]
[ProducesResponseType<Result<CreateCompanyCommandResponse>>(StatusCodes.Status201Created)]
[ProducesResponseType<Result<CreateCompanyCommandResponse>>(StatusCodes.Status400BadRequest)]
Expand Down
1 change: 0 additions & 1 deletion src/SnackFlow.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"DefaultConnection": "Host=localhost;Database=snackflow_db;Username=postgres;Password=root;Port=5432"
},
"AWS": {
"Profile": "default",
"Region": "us-east-1"
},
"AccessTokenSettings": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using SnackFlow.Domain.Enums;

namespace SnackFlow.Application.Abstractions.Schedulers;

/// <summary>
/// Define um contrato para agendar tarefas de email dentro da aplicação.
/// </summary>
public interface IEmailScheduler
{
/// <summary>
/// Define um agendamento de email para ser enviado de forma assíncrona com base nos dados fornecidos.
/// </summary>
/// <param name="emailAddress">O endereço de email do remetente a ser utilizado no envio.</param>
/// <param name="emailData">Um objeto contendo as informações necessárias para compor e agendar o email.</param>
/// <param name="cancellationToken">Um token para monitorar solicitações de cancelamento, permitindo cancelar a operação se necessário.</param>
/// <returns>Uma tarefa representando a operação assíncrona.</returns>
Task ScheduleEmailAsync(EmailAddress emailAddress, object emailData, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public interface IEmailService
/// <param name="to">O endereço de e-mail do destinatário.</param>
/// <param name="subject">O assunto do e-mail.</param>
/// <param name="body">O conteúdo do corpo do e-mail.</param>
/// <param name="emailAddress">O endereço de e-mail do remetente, representado como uma enumeração.</param>
/// <param name="emailAddress">O endereço de e-mail do remetente como uma string.</param>
/// <returns>Uma tarefa que representa a operação assíncrona de envio do e-mail.</returns>
Task SendAsync(string to, string subject, string body, EmailAddress emailAddress);
Task SendAsync(string to, string subject, string body, string emailAddress);
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
using SnackFlow.Domain.Enums;

using Newtonsoft.Json.Linq;
namespace SnackFlow.Application.Abstractions.Services;

/// <summary>
/// Define um serviço para gerenciar e gerar templates de e-mail dinamicamente com base nos dados e tipo de template especificados.
/// Interface de serviço para manipulação de templates de email.
/// Fornece métodos para recuperar e processar templates de email com base em parâmetros especificados.
/// </summary>
/// <typeparam name="T">O tipo do modelo de dados a ser usado para renderizar o template.</typeparam>
public interface IEmailTemplateService<in T> where T : class
public interface IEmailTemplateService
{
/// <summary>
/// Recupera e processa de forma assíncrona um template de email com base no público-alvo, tipo de template e modelo de dados especificados.
/// Substitui os placeholders no template pelos valores correspondentes fornecidos nos dados.
/// Recupera um template de email com base no público-alvo, tipo de template e contexto de dados especificados.
/// </summary>
/// <param name="audience">O público-alvo do template de email, especificado como <see cref="EmailAudience"/>.</param>
/// <param name="template">O tipo de template de email a ser recuperado, especificado como <see cref="EmailTemplate"/>.</param>
/// <param name="data">O modelo de dados contendo as propriedades para substituir os placeholders no template.</param>
/// <returns>Uma task que representa a operação assíncrona. O resultado da task contém o template de email processado como uma string.</returns>
Task<string> GetTemplateAsync(EmailAudience audience, EmailTemplate template, T data);
/// <param name="audience">O público-alvo do template de email (ex: Usuário ou Empresa).</param>
/// <param name="template">O tipo de template de email a ser recuperado (ex: Boas-vindas, Redefinição de Senha).</param>
/// <param name="data">Os dados dinâmicos a serem populados no template de email.</param>
/// <returns>Uma task que representa a operação assíncrona. O resultado da task contém o template de email populado como uma string.</returns>
Task<string> GetTemplateAsync(JObject data);
}
9 changes: 9 additions & 0 deletions src/SnackFlow.Application/DTOs/Email/BaseEmailDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace SnackFlow.Application.DTOs.Email;

public abstract class BaseEmailDTO
{
public required string To { get; set; }
public required string Subject { get; set; }
public required string Audience { get; set; }
public required string Template { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace SnackFlow.Application.DTOs.Email;

public class WelcomeEmailDTO
public sealed class WelcomeBaseEmailDTO : BaseEmailDTO
{
public required string Name { get; set; } = string.Empty;
}
5 changes: 2 additions & 3 deletions src/SnackFlow.Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ public static void AddApplication(this IServiceCollection services)
{
services.AddMediator(options =>
{
options.ServiceLifetime = ServiceLifetime.Singleton;

options.Assemblies = [typeof(DependencyInjection).Assembly];
options.ServiceLifetime = ServiceLifetime.Scoped;
options.NotificationPublisherType = typeof(Mediator.TaskWhenAllPublisher);
options.PipelineBehaviors = [
typeof(ValidationBehavior<,>),
typeof(LoggingBehavior<,>),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using SnackFlow.Application.Abstractions.Events;
using SnackFlow.Application.Abstractions.Schedulers;
using SnackFlow.Application.DTOs.Email;
using SnackFlow.Application.Extensions;
using SnackFlow.Domain.Enums;
using SnackFlow.Domain.Events;

namespace SnackFlow.Application.Events.Handlers;

public sealed class CompanyWelcomeEmailRequestedEventHandler(
IEmailScheduler emailScheduler)
: IDomainEventHandler<CompanyWelcomeEmailRequestedEvent>
{
public async ValueTask Handle(CompanyWelcomeEmailRequestedEvent notification, CancellationToken cancellationToken)
=> await emailScheduler.ScheduleEmailAsync(
EmailAddress.NoReply,
new WelcomeBaseEmailDTO
{
Audience = EmailAudience.Company.GetDescription(),
Template = EmailTemplate.Welcome.GetDescription(),
Name = notification.Name,
Subject = "Seja bem-vindo ao SnackFlow!",
To = "contact@samuelzedec.tech"
},
cancellationToken
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using SnackFlow.Application.Extensions;
using SnackFlow.Domain.Constants;
using SnackFlow.Domain.Entities;
using SnackFlow.Domain.Events;
using SnackFlow.Domain.Repositories;
using SnackFlow.Domain.ValueObjects.Email;
using SnackFlow.Domain.ValueObjects.Phone;
Expand All @@ -29,6 +30,11 @@ public async ValueTask<Result<CreateCompanyCommandResponse>> Handle(
type: request.Type
);

companyEntity.RaiseEvent(new CompanyWelcomeEmailRequestedEvent(
companyEntity.Name,
companyEntity.Email
));

await companyRepository.CreateAsync(companyEntity);
await unitOfWork.SaveChangesAsync(cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ public CreateCompanyCommandValidator()
RuleFor(x => x.TaxId)
.NotEmpty()
.WithMessage(ErrorMessage.Cpf.IsNullOrEmpty)
.Matches(@"^\d{11}$")
.Matches(@"^(?!^(\d)\1{10}$)(\d{3}\.?\d{3}\.?\d{3}-?\d{2})$")
.WithMessage(ErrorMessage.Cpf.LengthIsInvalid)
.When(x => x.Type == TaxIdType.IndividualWithCpf);

RuleFor(x => x.TaxId)
.NotEmpty()
.WithMessage(ErrorMessage.Cnpj.IsNullOrEmpty)
.Matches(@"^\d{14}$")
.Matches(@"^(?!^(\d)\1{13}$)(\d{2}\.?\d{3}\.?\d{3}/?\d{4}-?\d{2})$")
.WithMessage(ErrorMessage.Cnpj.LengthIsInvalid)
.When(x => x.Type == TaxIdType.LegalEntityWithCnpj);
}
Expand Down
2 changes: 2 additions & 0 deletions src/SnackFlow.Application/SnackFlow.Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SnackFlow.Domain\SnackFlow.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="DTOs\" />
<Folder Include="Events\" />
</ItemGroup>
</Project>
3 changes: 2 additions & 1 deletion src/SnackFlow.Domain/Constants/ErrorMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,11 @@ public static class NotFound
}

/// <summary>
/// Contém mensagens de erro relacionadas à invalidez de valores ou estados específicos do domínio.
/// Contém mensagens de erro relacionadas à invalidez de valores ou estados específicos.
/// </summary>
public static class Invalid
{
public const string IdIsNull = "O ID não pode ser nulo";
public const string Casting = "Não foi possível fazer o conversão";
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using SnackFlow.Domain.Abstractions;

namespace SnackFlow.Domain.Events;

public sealed record CompanyWelcomeEmailRequestedEvent(
string Name,
string Email
) : IDomainEvent;
66 changes: 64 additions & 2 deletions src/SnackFlow.Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,24 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Quartz;
using Serilog;
using Serilog.Events;
using SnackFlow.Application.Abstractions.Schedulers;
using SnackFlow.Application.Abstractions.Services;
using SnackFlow.Domain.Repositories;
using SnackFlow.Infrastructure.Jobs;
using SnackFlow.Infrastructure.Schedulers;
using SnackFlow.Infrastructure.Services;
using SnackFlow.Infrastructure.Services.EmailTemplateService;

namespace SnackFlow.Infrastructure;

/// <summary>
/// Fornece métodos e configurações para configurar o container de injeção de dependência da camada de Infrastructure.
/// Esta classe tem como objetivo simplificar o processo de registro de serviços
/// Esta classe pretende simplificar o processo de registro de serviços
/// e dependências necessárias para o funcionamento da aplicação.
/// </summary>
public static class DependencyInjection
Expand All @@ -30,7 +36,10 @@ public static void AddInfrastructure(this IServiceCollection services, IConfigur
services.AddRepositories();
services.AddServices();
services.AddAwsServices(configuration);
services.AddBackgroundJobs(configuration);
services.AddJsonConfiguration();
services.AddHealthChecksConfiguration(configuration);
services.AddSchedulersAndJobs();
}

private static void AddLogging(this ILoggingBuilder logging)
Expand Down Expand Up @@ -83,7 +92,7 @@ private static void AddRepositories(this IServiceCollection services)
private static void AddServices(this IServiceCollection services)
{
services.AddTransient<ICertificateService, CertificateService>();
services.AddTransient(typeof(IEmailTemplateService<>), typeof(EmailTemplateService<>));
services.AddTransient<IEmailTemplateService, EmailTemplateService>();
services.AddTransient<IEmailService, EmailService>();
}

Expand All @@ -93,6 +102,12 @@ private static void AddAwsServices(this IServiceCollection services, IConfigurat
services.AddAWSService<IAmazonSimpleEmailService>();
}

private static void AddSchedulersAndJobs(this IServiceCollection services)
{
services.AddTransient<SendingEmailJob>();
services.AddScoped<IEmailScheduler, QuartzEmailScheduler>();
}

private static void AddHealthChecksConfiguration(this IServiceCollection services, IConfiguration configuration)
{
var connectionString =
Expand All @@ -103,4 +118,51 @@ private static void AddHealthChecksConfiguration(this IServiceCollection service
.AddHealthChecks()
.AddNpgSql(connectionString, name: "DefaultConnection");
}

private static void AddBackgroundJobs(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<QuartzOptions>(options =>
{
options.SchedulerName = "SnackFlowScheduler";
options.Scheduling.IgnoreDuplicates = true;
options.Scheduling.OverWriteExistingData = false;
});

services.AddQuartz(q =>
{
q.UsePersistentStore(configure =>
{
configure.PerformSchemaValidation = false;
configure.UseProperties = true;
configure.RetryInterval = TimeSpan.FromSeconds(15);
configure.UseNewtonsoftJsonSerializer();
configure.UsePostgres(postgres =>
postgres.ConnectionString =
configuration.GetConnectionString("DefaultConnection")
?? throw new ArgumentNullException(nameof(configuration),
"QUARTZ - Connection string is null"));
});

q.AddJob<SendingEmailJob>(options => options
.WithIdentity(new JobKey(nameof(SendingEmailJob)))
.WithDescription("Envia de e-mails para notificar o usuário")
.StoreDurably());
});

services.AddQuartzHostedService(options =>
options.WaitForJobsToComplete = true);
}

private static void AddJsonConfiguration(this IServiceCollection _)
{
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver(),
TypeNameHandling = TypeNameHandling.Auto
};
}
}
Loading