diff --git a/src/.dockerignore b/src/.dockerignore
new file mode 100644
index 0000000..cd967fc
--- /dev/null
+++ b/src/.dockerignore
@@ -0,0 +1,25 @@
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/.idea
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
\ No newline at end of file
diff --git a/src/.gitinore b/src/.gitinore
new file mode 100644
index 0000000..3bb4d67
--- /dev/null
+++ b/src/.gitinore
@@ -0,0 +1,2 @@
+**/bin/
+**/obj/
\ No newline at end of file
diff --git a/src/Application.Tests/Application.Tests.csproj b/src/Application.Tests/Application.Tests.csproj
new file mode 100644
index 0000000..6300111
--- /dev/null
+++ b/src/Application.Tests/Application.Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Application.Tests/Factories/CreateUserCommand.cs b/src/Application.Tests/Factories/CreateUserCommand.cs
new file mode 100644
index 0000000..6d4212e
--- /dev/null
+++ b/src/Application.Tests/Factories/CreateUserCommand.cs
@@ -0,0 +1,74 @@
+using Application.Messages.Commands;
+using Bogus;
+using Domain.Enums;
+
+namespace Application.Tests.Factories;
+
+public class CreateUserCommandFactory
+{
+ private readonly Faker _faker;
+ public CreateUserCommandFactory()
+ {
+ _faker = new Faker()
+ .RuleFor(u => u.Email, f => f.Internet.Email())
+ .RuleFor(u => u.Password, f => f.Internet.Password())
+ .RuleFor(u => u.Country, f => f.Address.Country())
+ .RuleFor(u => u.AccessType, f => f.PickRandom(new []{AccessTypeEnum.DTC, AccessTypeEnum.Employer}))
+ .RuleFor(u => u.FullName, f => f.Name.FullName())
+ .RuleFor(u => u.EmployerId, f => f.Random.Guid().ToString())
+ .RuleFor(u => u.BirthDate, f => f.Date.Past(30))
+ .RuleFor(u => u.Salary, f => f.Random.Decimal(30000, 100000));
+ }
+
+ public CreateUserCommandFactory WithEmail(string email)
+ {
+ _faker.RuleFor(x => x.Email, email);
+ return this;
+ }
+ public CreateUserCommand Create()
+ {
+ return _faker.Generate();
+ }
+
+ public CreateUserCommandFactory WithPassword(string password)
+ {
+ _faker.RuleFor(x => x.Password, password);
+ return this;
+ }
+ public CreateUserCommandFactory WithCountry(string country)
+ {
+ _faker.RuleFor(x => x.Country, country);
+ return this;
+ }
+
+ public CreateUserCommandFactory WithAccessType(string accessType)
+ {
+ _faker.RuleFor(x => x.AccessType, accessType);
+ return this;
+ }
+
+ public CreateUserCommandFactory WithFullName(string fullName)
+ {
+ _faker.RuleFor(x => x.FullName, fullName);
+ return this;
+ }
+
+ public CreateUserCommandFactory WithEmployerId(string employerId)
+ {
+ _faker.RuleFor(x => x.EmployerId, employerId);
+ return this;
+ }
+
+ public CreateUserCommandFactory WithBirthDate(DateTime? birthDate)
+ {
+ _faker.RuleFor(x => x.BirthDate, birthDate);
+ return this;
+ }
+
+ public CreateUserCommandFactory WithSalary(decimal? salary)
+ {
+ _faker.RuleFor(x => x.Salary, salary);
+ return this;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Application.Tests/Factories/CsvContentFactory.cs b/src/Application.Tests/Factories/CsvContentFactory.cs
new file mode 100644
index 0000000..3cb45c2
--- /dev/null
+++ b/src/Application.Tests/Factories/CsvContentFactory.cs
@@ -0,0 +1,29 @@
+using Application.DTOs;
+using Bogus;
+
+namespace Application.Tests.Factories;
+
+
+public static class CsvContentFactory
+{
+ public static (string, List) GenerateCsvContent(int numberOfLines)
+ {
+ var faker = new Faker()
+ .RuleFor(u => u.Email, f => f.Internet.Email())
+ .RuleFor(u => u.FullName, f => f.Name.FullName())
+ .RuleFor(u => u.Country, f => f.Address.Country())
+ .RuleFor(u => u.BirthDate, f => f.Date.Past(30).ToString("MM/dd/yyyy"))
+ .RuleFor(u => u.Salary, f => f.Random.Decimal(30000, 100000));
+
+ var csvLines = new List { "Email,FullName,Country,BirthDate,Salary" };
+ var models = new List();
+ for (int i = 0; i < numberOfLines; i++)
+ {
+ var line = faker.Generate();
+ models.Add(line);
+ csvLines.Add($"{line.Email},{line.FullName},{line.Country},{line.BirthDate},{line.Salary}");
+ }
+
+ return (string.Join("\n", csvLines), models);
+ }
+}
\ No newline at end of file
diff --git a/src/Application.Tests/Messages/Handlers/Commands/CreateUserCommandHandlerTests.cs b/src/Application.Tests/Messages/Handlers/Commands/CreateUserCommandHandlerTests.cs
new file mode 100644
index 0000000..5a56052
--- /dev/null
+++ b/src/Application.Tests/Messages/Handlers/Commands/CreateUserCommandHandlerTests.cs
@@ -0,0 +1,62 @@
+using Application.Messages.Commands;
+using Application.Messages.Handlers.Commands;
+using Infrastructure.Records;
+using Infrastructure.Services.Interfaces;
+using Moq;
+
+namespace Application.Tests.Messages.Handlers.Commands;
+
+
+[Trait("Category", "Unit")]
+public class CreateUserCommandHandlerTests
+{
+ [Fact]
+ public async Task Handle_GivenValidUser_CreatesUserSuccessfully()
+ {
+ // Arrange
+ var userServiceClientMock = new Mock();
+ var handler = new CreateUserCommandHandler(userServiceClientMock.Object);
+ var command = new CreateUserCommand(email: "test@example.com",
+ fullName: "FullName",
+ password: "Password",
+ country: "Country",
+ accessType: "AccessType",
+ employerId: "EmployerId",
+ birthDate: DateTime.Now,
+ salary: 50000);
+
+ userServiceClientMock.Setup(client => client.CreateUserAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true);
+
+ // Act
+ var result = await handler.Handle(command, CancellationToken.None);
+
+ // Assert
+ Assert.True(result);
+ userServiceClientMock.Verify(client => client.CreateUserAsync(It.IsAny(), It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task Handle_GivenInvalidUser_ReturnsFailure()
+ {
+ // Arrange
+ var userServiceClientMock = new Mock();
+ var handler = new CreateUserCommandHandler(userServiceClientMock.Object);
+ var command = new CreateUserCommand(email: "test@example.com",
+ fullName: "FullName",
+ password: "Password",
+ country: "Country",
+ accessType: "AccessType",
+ employerId: "EmployerId",
+ birthDate: DateTime.Now,
+ salary: 50000);
+
+ userServiceClientMock.Setup(client => client.CreateUserAsync(It.IsAny(), It.IsAny())).ReturnsAsync(false);
+
+ // Act
+ var result = await handler.Handle(command, CancellationToken.None);
+
+ // Assert
+ Assert.False(result);
+ userServiceClientMock.Verify(client => client.CreateUserAsync(It.IsAny(), It.IsAny()), Times.Once);
+ }
+}
\ No newline at end of file
diff --git a/src/Application.Tests/Messages/Handlers/Commands/ProcessEligibilityFileCommandHandlerTests.cs b/src/Application.Tests/Messages/Handlers/Commands/ProcessEligibilityFileCommandHandlerTests.cs
new file mode 100644
index 0000000..78ae36d
--- /dev/null
+++ b/src/Application.Tests/Messages/Handlers/Commands/ProcessEligibilityFileCommandHandlerTests.cs
@@ -0,0 +1,145 @@
+using System.Net;
+using System.Text;
+using Application.Messages.Commands;
+using Application.Messages.Handlers.Commands;
+using Application.Messages.Queries;
+using Application.Tests.Factories;
+using Domain.Enums;
+using Infrastructure.Records;
+using MediatR;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Moq.Protected;
+
+namespace Application.Tests.Messages.Handlers.Commands;
+
+[Trait("Category", "Unit")]
+public class ProcessEligibilityFileCommandHandlerTests
+{
+ [Fact]
+ public async Task Handle_EmptyFile_ReturnsEmptyResult()
+ {
+ // Arrange
+ var httpClientFactoryMock = new Mock();
+ var httpMessageHandlerMock = new Mock();
+ var httpClient = new HttpClient(httpMessageHandlerMock.Object);
+ httpClientFactoryMock.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient);
+
+ var emptyStream = new MemoryStream(Encoding.UTF8.GetBytes("")); // Empty content
+ httpMessageHandlerMock.Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny()
+ )
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StreamContent(emptyStream)
+ });
+
+ var loggerMock = new Mock>();
+ var mediatorMock = new Mock();
+
+ var handler = new ProcessEligibilityFileCommandHandler(httpClientFactoryMock.Object, loggerMock.Object, mediatorMock.Object);
+
+ // Act
+ var result = await handler.Handle(new ProcessEligibilityFileCommand("http://example.com/empty.csv", "employerName"), CancellationToken.None);
+
+ // Assert
+ Assert.Empty(result.ProcessedLines);
+ Assert.Empty(result.NonProcessedLines);
+ Assert.Empty(result.Errors);
+ }
+
+
+ [Fact]
+ public async Task Handle_DownloadFails_ThrowsException()
+ {
+ // Arrange
+ var httpClientFactoryMock = new Mock();
+ var httpMessageHandlerMock = new Mock();
+ var httpClient = new HttpClient(httpMessageHandlerMock.Object);
+ httpClientFactoryMock.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient);
+
+ httpMessageHandlerMock.Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny()
+ )
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.NotFound // Simulate failure
+ });
+
+ var loggerMock = new Mock>();
+ var mediatorMock = new Mock();
+
+ var handler = new ProcessEligibilityFileCommandHandler(httpClientFactoryMock.Object, loggerMock.Object, mediatorMock.Object);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() => handler.Handle(new ProcessEligibilityFileCommand("http://example.com/nonexistent.csv", "employerName"), CancellationToken.None));
+ }
+
+ [Theory]
+ [InlineData(5)]
+ [InlineData(15)]
+ [InlineData(54)]
+ [InlineData(13)]
+ public async Task Handle_NonEmptyFile_ProcessesDataCorrectly(int countOfRecords)
+ {
+ // Arrange
+ var httpClientFactoryMock = new Mock();
+ var httpMessageHandlerMock = new Mock();
+ var loggerMock = new Mock>();
+ var mediatorMock = new Mock();
+ var httpClient = new HttpClient(httpMessageHandlerMock.Object);
+ httpClientFactoryMock.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient);
+
+ var (csvContent, lineModels) = CsvContentFactory.GenerateCsvContent(countOfRecords);
+ foreach (var lineModel in lineModels)
+ {
+ mediatorMock.Setup(x => x.Send(It.Is(q => q.Email == lineModel.Email), It.IsAny()))
+ .ReturnsAsync(new UserDto()
+ {
+ Email = lineModel.Email,
+ Country = lineModel.Country,
+ Salary = lineModel.Salary,
+ AccessType = AccessTypeEnum.Employer,
+ Id = Guid.NewGuid().ToString(),
+ BirthDate = DateTime.Now.AddYears(-30),
+ FullName = lineModel.FullName,
+ EmployerId = Guid.NewGuid().ToString()
+ });
+ }
+ var csvStream = new MemoryStream(Encoding.UTF8.GetBytes(csvContent));
+ httpMessageHandlerMock.Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny()
+ )
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StreamContent(csvStream)
+ });
+
+
+ var handler = new ProcessEligibilityFileCommandHandler(httpClientFactoryMock.Object, loggerMock.Object, mediatorMock.Object);
+
+ // Act
+ var result = await handler.Handle(new ProcessEligibilityFileCommand("http://example.com/nonempty.csv", "employerName"), CancellationToken.None);
+
+ // Assert
+ Assert.NotEmpty(result.ProcessedLines);
+ Assert.Empty(result.NonProcessedLines);
+ Assert.Empty(result.Errors);
+ mediatorMock.Verify(x => x.Send(
+ It.IsAny(),
+ It.IsAny()),
+ Times.Once);
+
+ }
+}
\ No newline at end of file
diff --git a/src/Application.Tests/Messages/Handlers/Commands/ProcessSignupCommandHandlerTests.cs b/src/Application.Tests/Messages/Handlers/Commands/ProcessSignupCommandHandlerTests.cs
new file mode 100644
index 0000000..d5e6c26
--- /dev/null
+++ b/src/Application.Tests/Messages/Handlers/Commands/ProcessSignupCommandHandlerTests.cs
@@ -0,0 +1,159 @@
+using Application.Messages.Commands;
+using Application.Messages.Handlers.Commands;
+using Application.Messages.Queries;
+using MediatR;
+using Microsoft.Extensions.Logging;
+using Moq;
+using FluentAssertions;
+using Infrastructure.Records;
+
+namespace Application.Tests.Messages.Handlers.Commands;
+
+[Trait("Category", "Unit")]
+public class ProcessSignupCommandHandlerTests
+{
+ [Fact]
+ public async Task Handle_EmployerDataIsNull_CreatesUser()
+ {
+ // Arrange
+ var mediatorMock = new Mock();
+ var loggerMock = new Mock>();
+ var handler = new ProcessSignupCommandHandler(mediatorMock.Object, loggerMock.Object);
+ var command = new ProcessSignupCommand("test@example.com", "Password123!", "Country");
+
+ mediatorMock.Setup(x => x.Send(It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync((EmployerIdRecord)null);
+
+ // Act
+ await handler.Handle(command, CancellationToken.None);
+
+ // Assert
+ mediatorMock.Verify(x => x.Send(It.IsAny(), It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task Handle_UserAlreadyExists_LogsError()
+ {
+ // Arrange
+ var mediatorMock = new Mock();
+ var loggerMock = new Mock>();
+ var handler = new ProcessSignupCommandHandler(mediatorMock.Object, loggerMock.Object);
+ var command = new ProcessSignupCommand("existing@example.com", "Password123!", "Country");
+
+ mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())).ReturnsAsync(new EmployerIdRecord("1234"));
+ mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())).ReturnsAsync(new UserDto());
+
+ var capturedLogs = new List<(LogLevel logLevel, Exception exception, string message)>();
+ MockLogger(loggerMock, capturedLogs);
+
+ // Act
+ await Assert.ThrowsAsync(() => handler.Handle(command, CancellationToken.None));
+
+ // Assert
+ capturedLogs.Should().ContainSingle(x => x.message == "User with email existing@example.com already exists.");
+ }
+
+ private static void MockLogger(Mock> loggerMock, List<(LogLevel logLevel, Exception exception, string message)> capturedLogs)
+ {
+ loggerMock.Setup(
+ x => x.Log(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ (Func)It.IsAny