Skip to content

Commit

Permalink
Merge pull request #246 from DentallApp/patch-21
Browse files Browse the repository at this point in the history
Validate if the identity document is Ecuadorian
  • Loading branch information
MrDave1999 committed Mar 23, 2024
2 parents b93d161 + 60aaa93 commit 1a46173
Show file tree
Hide file tree
Showing 24 changed files with 413 additions and 40 deletions.
26 changes: 16 additions & 10 deletions DentallApp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.ChatBot", "src\Plugi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.ChatBot.IntegrationTests", "tests\ChatBot\Plugin.ChatBot.IntegrationTests.csproj", "{3897DDB9-5C4F-4664-94D7-756A5E673951}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.Twilio.WhatsApp", "src\Plugins\TwilioWhatsApp\Plugin.Twilio.WhatsApp.csproj", "{6B8BD4C1-00A7-429B-A22F-25851E7296B5}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.Twilio.WhatsApp", "src\Plugins\TwilioWhatsApp\Plugin.Twilio.WhatsApp.csproj", "{6B8BD4C1-00A7-429B-A22F-25851E7296B5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.SendGrid", "src\Plugins\SendGrid\Plugin.SendGrid.csproj", "{6917AD6C-E337-4DE6-88D8-B0990E9B8D56}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.SendGrid", "src\Plugins\SendGrid\Plugin.SendGrid.csproj", "{6917AD6C-E337-4DE6-88D8-B0990E9B8D56}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.IdentityDocument.Ecuador", "src\Plugins\IdentityDocumentEcuador\Plugin.IdentityDocument.Ecuador.csproj", "{390B0477-73DB-4C71-A1BE-4539E93EF55D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Elements", "Solution Elements", "{D66CD840-8EE9-4697-9FFA-B4E5B0F25D80}"
ProjectSection(SolutionItems) = preProject
Expand All @@ -42,14 +44,6 @@ Global
{E354A181-1E6C-4F76-8D99-96E5A6824FC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E354A181-1E6C-4F76-8D99-96E5A6824FC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E354A181-1E6C-4F76-8D99-96E5A6824FC1}.Release|Any CPU.Build.0 = Release|Any CPU
{EACB8B54-AAFE-4A46-A1D9-1FB23ADDA41C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EACB8B54-AAFE-4A46-A1D9-1FB23ADDA41C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EACB8B54-AAFE-4A46-A1D9-1FB23ADDA41C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EACB8B54-AAFE-4A46-A1D9-1FB23ADDA41C}.Release|Any CPU.Build.0 = Release|Any CPU
{BFD8D9F8-AEF4-4661-B0DC-6D79F84F718E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BFD8D9F8-AEF4-4661-B0DC-6D79F84F718E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFD8D9F8-AEF4-4661-B0DC-6D79F84F718E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFD8D9F8-AEF4-4661-B0DC-6D79F84F718E}.Release|Any CPU.Build.0 = Release|Any CPU
{1B8756B1-A59A-4724-B757-01AAECADF43C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B8756B1-A59A-4724-B757-01AAECADF43C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B8756B1-A59A-4724-B757-01AAECADF43C}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -66,6 +60,14 @@ Global
{BE6318A1-900B-4E8E-8AAE-C391F881C844}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE6318A1-900B-4E8E-8AAE-C391F881C844}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE6318A1-900B-4E8E-8AAE-C391F881C844}.Release|Any CPU.Build.0 = Release|Any CPU
{EACB8B54-AAFE-4A46-A1D9-1FB23ADDA41C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EACB8B54-AAFE-4A46-A1D9-1FB23ADDA41C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EACB8B54-AAFE-4A46-A1D9-1FB23ADDA41C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EACB8B54-AAFE-4A46-A1D9-1FB23ADDA41C}.Release|Any CPU.Build.0 = Release|Any CPU
{BFD8D9F8-AEF4-4661-B0DC-6D79F84F718E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BFD8D9F8-AEF4-4661-B0DC-6D79F84F718E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFD8D9F8-AEF4-4661-B0DC-6D79F84F718E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFD8D9F8-AEF4-4661-B0DC-6D79F84F718E}.Release|Any CPU.Build.0 = Release|Any CPU
{3897DDB9-5C4F-4664-94D7-756A5E673951}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3897DDB9-5C4F-4664-94D7-756A5E673951}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3897DDB9-5C4F-4664-94D7-756A5E673951}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -78,6 +80,10 @@ Global
{6917AD6C-E337-4DE6-88D8-B0990E9B8D56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6917AD6C-E337-4DE6-88D8-B0990E9B8D56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6917AD6C-E337-4DE6-88D8-B0990E9B8D56}.Release|Any CPU.Build.0 = Release|Any CPU
{390B0477-73DB-4C71-A1BE-4539E93EF55D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{390B0477-73DB-4C71-A1BE-4539E93EF55D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{390B0477-73DB-4C71-A1BE-4539E93EF55D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{390B0477-73DB-4C71-A1BE-4539E93EF55D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ COPY ["src/Plugins/AppointmentReminders/*.csproj", "src/Plugins/AppointmentRemin
COPY ["src/Plugins/ChatBot/*.csproj", "src/Plugins/ChatBot/"]
COPY ["src/Plugins/SendGrid/*.csproj", "src/Plugins/SendGrid/"]
COPY ["src/Plugins/TwilioWhatsApp/*.csproj", "src/Plugins/TwilioWhatsApp/"]
COPY ["src/Plugins/IdentityDocumentEcuador/*.csproj", "src/Plugins/IdentityDocumentEcuador/"]
COPY ["src/Plugins/*.props", "src/Plugins/"]
WORKDIR /app/src/Plugins/AppointmentReminders
RUN dotnet restore
Expand All @@ -29,6 +30,8 @@ WORKDIR /app/src/Plugins/SendGrid
RUN dotnet restore
WORKDIR /app/src/Plugins/TwilioWhatsApp
RUN dotnet restore
WORKDIR /app/src/Plugins/IdentityDocumentEcuador
RUN dotnet restore

# Copy everything else and build plugins
COPY ["src/Shared/", "/app/src/Shared/"]
Expand All @@ -41,6 +44,8 @@ WORKDIR /app/src/Plugins/SendGrid
RUN dotnet build -c Release --no-restore
WORKDIR /app/src/Plugins/TwilioWhatsApp
RUN dotnet build -c Release --no-restore
WORKDIR /app/src/Plugins/IdentityDocumentEcuador
RUN dotnet build -c Release --no-restore

# Copy everything else and build app
COPY ["src/", "/app/src/"]
Expand Down
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,15 @@ Request body:
### General architecture

<details>
<summary><b>More details</b></summary>
<summary><b>Show diagram</b></summary>

![general-architecture](https://github.com/DentallApp/back-end/blob/dev/diagrams/general-architecture.png)

</details>

#### Overview of each component
<details>
<summary><b>More details</b></summary>

- **Host Application.** Contains everything needed to run the application. It represents the entry point of the application.
This layer performs other tasks such as:
- Load plugins from a configuration file (.env) using the library called [CPlugin.Net](https://github.com/MrDave1999/CPlugin.Net).
Expand Down Expand Up @@ -256,24 +258,33 @@ Request body:
- **Twilio WhatsApp.** It is a plugin that allows to send messages by whatsapp in cases such as:
- When an appointment is scheduled from the chatbot, the user is sent the appointment information to whatsapp.
- When an employee needs to cancel an appointment, he/she should notify patients by whatsapp.
- **IdentityDocument.Ecuador.** It is a plugin that allows to validate identity documents registered in Ecuador.
This plugin uses an [algorithm](https://www.skypack.dev/view/udv-ec) to verify if the identity document is valid or not.

</details>

### Core layer

<details>
<summary><b>More details</b></summary>
<summary><b>Show diagram</b></summary>

![core-layer](https://github.com/DentallApp/back-end/blob/dev/diagrams/core-layer.png)

</details>

<details>
<summary><b>More details</b></summary>

The above diagram describes in more detail which feature modules are contained in the core layer.

In the presented diagram it can be identified that the feature modules are not coupled to each other, the purpose of this is not to cause a [dependency hell](https://en.wikipedia.org/wiki/Dependency_hell), in order to maintain a dependency graph that is as simple as possible. The purpose is to make it easier to understand the parts of the backend application.

</details>

### Relational model

<details>
<summary><b>More details</b></summary>
<summary><b>Show diagram</b></summary>

![relational-model](https://github.com/DentallApp/back-end/blob/dev/diagrams/relational-model.png)

Expand Down
Binary file modified diagrams/general-architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions src/Core/Dependents/UseCases/Create.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ public Dependent MapToDependent(int userId)

public class CreateDependentValidator : AbstractValidator<CreateDependentRequest>
{
public CreateDependentValidator()
public CreateDependentValidator(IIdentityDocumentValidator documentValidator)
{
RuleFor(request => request.Document).NotEmpty();
RuleFor(request => request.Document)
.MustBeValidIdentityDocument(documentValidator);
RuleFor(request => request.Names).NotEmpty();
RuleFor(request => request.LastNames).NotEmpty();
RuleFor(request => request.CellPhone).NotEmpty();
Expand Down
5 changes: 3 additions & 2 deletions src/Core/Persons/UseCases/Create.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ public class CreatePersonRequest

public class CreatePersonValidator : AbstractValidator<CreatePersonRequest>
{
public CreatePersonValidator()
public CreatePersonValidator(IIdentityDocumentValidator documentValidator)
{
RuleFor(request => request.Document).NotEmpty();
RuleFor(request => request.Document)
.MustBeValidIdentityDocument(documentValidator);
RuleFor(request => request.Names).NotEmpty();
RuleFor(request => request.LastNames).NotEmpty();
RuleFor(request => request.CellPhone).NotEmpty();
Expand Down
5 changes: 3 additions & 2 deletions src/Core/Security/Employees/UseCases/Create.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@ public Employee MapToEmployee(string password)

public class CreateEmployeeValidator : AbstractValidator<CreateEmployeeRequest>
{
public CreateEmployeeValidator()
public CreateEmployeeValidator(IIdentityDocumentValidator documentValidator)
{
RuleFor(request => request.UserName)
.NotEmpty()
.EmailAddress();
RuleFor(request => request.Password).MustBeSecurePassword();
RuleFor(request => request.Document).NotEmpty();
RuleFor(request => request.Document)
.MustBeValidIdentityDocument(documentValidator);
RuleFor(request => request.Names).NotEmpty();
RuleFor(request => request.LastNames).NotEmpty();
RuleFor(request => request.CellPhone).NotEmpty();
Expand Down
5 changes: 3 additions & 2 deletions src/Core/Security/Employees/UseCases/UpdateAnyEmployee.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ public void MapToEmployee(Employee employee)

public class UpdateAnyEmployeeValidator : AbstractValidator<UpdateAnyEmployeeRequest>
{
public UpdateAnyEmployeeValidator()
public UpdateAnyEmployeeValidator(IIdentityDocumentValidator documentValidator)
{
RuleFor(request => request.Email)
.NotEmpty()
.EmailAddress();
RuleFor(request => request.Password).MustBeSecurePassword();
RuleFor(request => request.Document).NotEmpty();
RuleFor(request => request.Document)
.MustBeValidIdentityDocument(documentValidator);
RuleFor(request => request.Names).NotEmpty();
RuleFor(request => request.LastNames).NotEmpty();
RuleFor(request => request.CellPhone).NotEmpty();
Expand Down
5 changes: 3 additions & 2 deletions src/Core/Security/Users/UseCases/Create.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ public User MapToUser(string password)

public class CreateBasicUserValidator : AbstractValidator<CreateBasicUserRequest>
{
public CreateBasicUserValidator()
public CreateBasicUserValidator(IIdentityDocumentValidator documentValidator)
{
RuleFor(request => request.UserName)
.NotEmpty()
.EmailAddress();
RuleFor(request => request.Password).MustBeSecurePassword();
RuleFor(request => request.Document).NotEmpty();
RuleFor(request => request.Document)
.MustBeValidIdentityDocument(documentValidator);
RuleFor(request => request.Names).NotEmpty();
RuleFor(request => request.LastNames).NotEmpty();
RuleFor(request => request.CellPhone).NotEmpty();
Expand Down
1 change: 1 addition & 0 deletions src/HostApplication/PluginStartup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ public static void ConfigureServices(WebApplicationBuilder builder)
// These services are only added when no plugin registers its own implementation.
builder.Services.TryAddSingleton<IEmailService, FakeEmailService>();
builder.Services.TryAddSingleton<IInstantMessaging, FakeInstantMessaging>();
builder.Services.TryAddSingleton<IIdentityDocumentValidator, FakeIdentityDocument>();
}
}
6 changes: 6 additions & 0 deletions src/Infrastructure/Services/FakeIdentityDocument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace DentallApp.Infrastructure.Services;

public class FakeIdentityDocument : IIdentityDocumentValidator
{
public Result IsValid(string document) => Result.Success();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[assembly: Plugin(typeof(DependencyServicesRegisterer))]

namespace Plugin.IdentityDocument.Ecuador;

public class DependencyServicesRegisterer : IDependencyServicesRegisterer
{
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IIdentityDocumentValidator, IdentityDocumentValidator>();
}
}
5 changes: 5 additions & 0 deletions src/Plugins/IdentityDocumentEcuador/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
global using DentallApp.Shared.Interfaces;
global using DentallApp.Shared.Resources.ApiResponses;
global using SimpleResults;
global using CPlugin.Net;
global using Plugin.IdentityDocument.Ecuador;
68 changes: 68 additions & 0 deletions src/Plugins/IdentityDocumentEcuador/IdentityDocumentValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace Plugin.IdentityDocument.Ecuador;

/// <summary>
/// Represents a validator for Ecuadorian identity documents.
/// </summary>
/// <remarks>
/// Link to the algorithm: <see href="https://www.skypack.dev/view/udv-ec"/>
/// </remarks>
public class IdentityDocumentValidator : IIdentityDocumentValidator
{
public Result IsValid(string document)
{
if (string.IsNullOrWhiteSpace(document))
return Result.Invalid(Messages.DocumentIsEmpty);

if (document.Length != 10)
return Result.Invalid(string.Format(Messages.DocumentMaxCharacters, 10));

if(IsNotNumeric(document))
return Result.Invalid(Messages.DocumentMustBeNumeric);

if(HasInvalidRegionDigit(document))
return Result.Invalid(Messages.DocumentIsInvalid);

int verificationDigit = document[^1].ToInt();
int total = 0;
bool isEvenPosition = false;
// The verification digit is not considered.
int len = document.Length - 1;
for (int position = 0; position < len; position++)
{
isEvenPosition = !isEvenPosition;
int coefficient = isEvenPosition ? 2 : 1;
int digit = document[position].ToInt() * coefficient;
int result = digit > 9 ? digit - 9 : digit;
total += result;
}

bool isValidDocument =
(total % 10 == 0 && verificationDigit == 0) ||
(10 - (total % 10) == verificationDigit);

return isValidDocument ?
Result.Success() :
Result.Invalid(Messages.DocumentIsInvalid);
}

private static bool IsNotNumeric(string document)
=> document.Where(c => c is < '0' or > '9').Any();

private static bool HasInvalidRegionDigit(string document)
=> !HasValidRegionDigit(document);

private static bool HasValidRegionDigit(string document)
{
// The digit of the region is obtained, which are the first two digits.
int regionDigit = int.Parse(document[..2]);

// It is validated if the region digit exists in Ecuador, which is divided into 24 regions.
// 30 is assigned for Ecuadorians abroad.
return regionDigit is (>= 1 and <= 24) or 30;
}
}

public static class CharExtensions
{
public static int ToInt(this char c) => c - '0';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<OutputType>Library</OutputType>
<RootNamespace>Plugin.IdentityDocument.Ecuador</RootNamespace>
</PropertyGroup>

</Project>
13 changes: 13 additions & 0 deletions src/Shared/Interfaces/IIdentityDocumentValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace DentallApp.Shared.Interfaces;

/// <summary>
/// Represents a validator to validate identity documents.
/// </summary>
public interface IIdentityDocumentValidator
{
/// <summary>
/// Checks if an identity document is valid.
/// </summary>
/// <param name="document">The document to validate.</param>
Result IsValid(string document);
}
36 changes: 36 additions & 0 deletions src/Shared/Resources/ApiResponses/Messages.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/Shared/Resources/ApiResponses/Messages.resx
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,18 @@
<data name="DirectLineTokenFailed" xml:space="preserve">
<value>Error en la llamada a la API del token de Direct Line.</value>
</data>
<data name="DocumentIsEmpty" xml:space="preserve">
<value>El documento de identidad está vacío.</value>
</data>
<data name="DocumentIsInvalid" xml:space="preserve">
<value>El documento de identidad es inválido.</value>
</data>
<data name="DocumentMaxCharacters" xml:space="preserve">
<value>El documento de identidad debe tener {0} caracteres.</value>
</data>
<data name="DocumentMustBeNumeric" xml:space="preserve">
<value>El documento de identidad debe ser numérico.</value>
</data>
<data name="EmailNotConfirmed" xml:space="preserve">
<value>El correo electrónico no está confirmado.</value>
</data>
Expand Down
Loading

0 comments on commit 1a46173

Please sign in to comment.